From 57ac289a9a36942839f9b94325d8b7fb42a478a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Thu, 7 Mar 2013 19:35:34 -0500
Subject: [PATCH] Issue #1741498 by jessebeach, Wim Leers: Add a mobile
 preview bar to Drupal core
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/modules/contextual/contextual.toolbar.js      |    9 +-
 .../config/responsive_preview.devices.yml          |   50 ++
 .../css/responsive-preview.base-rtl.css            |   32 +
 .../css/responsive-preview.base.css                |  105 +++
 .../css/responsive-preview.icons-rtl.css           |   21 +
 .../css/responsive-preview.icons.css               |   95 +++
 .../css/responsive-preview.theme-rtl.css           |   30 +
 .../css/responsive-preview.theme.css               |  179 ++++++
 .../images/responsive-preview-icons.png            |   10 +
 .../responsive_preview/js/responsive-preview.js    |  673 ++++++++++++++++++++
 .../responsive_preview/responsive_preview.info     |    7 +
 .../responsive_preview/responsive_preview.module   |  172 +++++
 .../testswarm/responsive_preview.admin.tests.js    |   27 +
 .../tests/testswarm/responsive_preview.tests.js    |   69 ++
 14 files changed, 1478 insertions(+), 1 deletion(-)
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.devices.yml
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.base-rtl.css
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.base.css
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.icons-rtl.css
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.icons.css
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.theme-rtl.css
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.theme.css
 create mode 100644 core/modules/responsive_preview/images/responsive-preview-icons.png
 create mode 100644 core/modules/responsive_preview/js/responsive-preview.js
 create mode 100644 core/modules/responsive_preview/responsive_preview.info
 create mode 100644 core/modules/responsive_preview/responsive_preview.module
 create mode 100644 core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js
 create mode 100644 core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js

diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index 45f9757..ada9f11 100644
--- a/core/modules/contextual/contextual.toolbar.js
+++ b/core/modules/contextual/contextual.toolbar.js
@@ -27,13 +27,20 @@ Drupal.behaviors.contextualToolbar = {
         model: model
       });
 
-      // Update the model based on overlay events.
       $(document)
+        // Update the model based on Overlay events.
         .on('drupalOverlayOpen.contextualToolbar', function () {
           model.set('isVisible', false);
         })
         .on('drupalOverlayClose.contextualToolbar', function () {
           model.set('isVisible', true);
+        })
+        // Update the model based on Responsive Preview events.
+        .on('drupalResponsivePreviewStarted.contextualToolbar', function () {
+          model.set('isVisible', false);
+        })
+        .on('drupalResponsivePreviewStopped.contextualToolbar', function () {
+          model.set('isVisible', true);
         });
 
       // Update the model to show the edit tab if there's >=1 contextual link.
diff --git a/core/modules/responsive_preview/config/responsive_preview.devices.yml b/core/modules/responsive_preview/config/responsive_preview.devices.yml
new file mode 100644
index 0000000..8f4e088
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.devices.yml
@@ -0,0 +1,50 @@
+# References:
+# - http://en.wikipedia.org/wiki/List_of_displays_by_pixel_density
+# - http://www.w3.org/blog/CSS/2012/06/14/unprefix-webkit-device-pixel-ratio/
+# - http://pieroxy.net/blog/2012/10/18/media_features_of_the_most_common_devices.html
+#
+# The device listing and specifications will be updated periodically through
+# minor releases of Drupal.
+
+iphone:
+  label: iPhone 5
+  dimensions:
+    width: 640
+    height: 1136
+    dppx: 2
+  orientation: portrait
+iphone4:
+  label: iPhone 4
+  dimensions:
+    width: 640
+    height: 960
+    dppx: 2
+  orientation: portrait
+ipad:
+  label: iPad
+  dimensions:
+    width: 1536
+    height: 2048
+    dppx: 2
+  orientation: portrait
+nexus4:
+  label: Nexus 4
+  dimensions:
+    width: 768
+    height: 1280
+    dppx: 2
+  orientation: portrait
+nexus7:
+  label: Nexus 7
+  dimensions:
+    width: 800
+    height: 1280
+    dppx: 1.325
+  orientation: portrait
+desktop:
+  label: Typical desktop
+  dimensions:
+    width: 1366
+    height: 768
+    dppx: 1
+  orientation: landscape
diff --git a/core/modules/responsive_preview/css/responsive-preview.base-rtl.css b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css
new file mode 100644
index 0000000..790cc0a
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css
@@ -0,0 +1,32 @@
+/**
+ * @file
+ * RTL base styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+
+/* At narrow screen widths, float the tab to the right so it falls in line with
+ * the rest of the toolbar tabs. */
+.js .toolbar .bar .toolbar-tab-responsive-preview.tab {
+  float: left;
+}
+.toolbar-tab-responsive-preview .responsive-preview-options {
+  left: 0.3em;
+  right: auto;
+}
+
+/**
+ * Preview container.
+ *
+ * The container is kept offscreen after it is built and has been disabled.
+ */
+.responsive-preview {
+  left: auto;
+  right: -200%;
+}
+.responsive-preview.active {
+  left: auto;
+  right: 0;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.base.css b/core/modules/responsive_preview/css/responsive-preview.base.css
new file mode 100644
index 0000000..cd82f1b
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.base.css
@@ -0,0 +1,105 @@
+/**
+ * @file
+ * Base styling for responsive preview.
+ */
+
+/**
+ * Constrain the window height to the client height when the preview is active.
+ */
+.responsive-preview-active {
+  height: 100%;
+  overflow: hidden;
+}
+
+/**
+ * Toolbar tab.
+ */
+.toolbar-tab-responsive-preview {
+  display: none;
+}
+/* At narrow screen widths, float the tab to the left so it falls in line with
+ * the rest of the toolbar tabs. */
+.js .toolbar .bar .toolbar-tab-responsive-preview.tab {
+  display: block;
+  float: right; /* LTR */
+  position: relative;
+}
+.toolbar-tab-responsive-preview .trigger {
+  display: block;
+}
+/* Device preview options. */
+.toolbar-tab-responsive-preview .item-list {
+  display: none;
+  position: absolute;
+  white-space: nowrap;
+  z-index: 1;
+}
+.toolbar-tab-responsive-preview.open .item-list {
+  display: block;
+}
+.js .toolbar-tab-responsive-preview.tab .options li {
+  float: none;
+}
+
+/**
+ * Preview container.
+ *
+ * The container is kept offscreen after it is built and has been disabled.
+ */
+.responsive-preview {
+  bottom: 0;
+  height: 100%;
+  left: -200%; /* LTR */
+  position: relative;
+  top: 0;
+  width: 100%;
+  z-index: 1050;
+}
+.responsive-preview.active {
+  left: 0; /* LTR */
+  position: fixed;
+}
+.responsive-preview .control {
+  position: absolute;
+}
+.responsive-preview .modal-background {
+  bottom: 0;
+  height: 100%;
+  left: 0;
+  position: static;
+  right: 0;
+  top: 0;
+  width: 100%;
+  z-index: 1;
+}
+.responsive-preview.active .modal-background {
+  position: fixed;
+}
+
+/**
+ * Preview iframe.
+ */
+.responsive-preview .frame-container {
+  position: absolute;
+  z-index: 100;
+}
+.responsive-preview .frame-container iframe {
+  position: relative;
+}
+
+/**
+ * Override Toolbar styling in the preview iframe.
+ */
+body.toolbar-tray-open.responsive-preview-frame {
+  margin-left: 0 !important;
+  margin-right: 0 !important;
+}
+.responsive-preview-frame {
+  overflow-x: hidden !important;
+}
+.responsive-preview-frame #toolbar-administration {
+  display: none !important;
+}
+.responsive-preview-frame .contextual {
+  display: none !important;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css b/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css
new file mode 100644
index 0000000..0c8d2b6
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css
@@ -0,0 +1,21 @@
+/**
+ * @file
+ * RTL icon styling for responsive preview.
+ */
+
+.toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: auto; /* LTR */
+  right: 1em;
+}
+
+/**
+ * Responsive preview controls icons.
+ */
+.responsive-preview .icon-close:before {
+  left: 9px;
+  right: auto;
+}
+.responsive-preview .icon-orientation:before {
+  left: auto;
+  right: 9px;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.icons.css b/core/modules/responsive_preview/css/responsive-preview.icons.css
new file mode 100644
index 0000000..e616c2c
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons.css
@@ -0,0 +1,95 @@
+/**
+ * @file
+ * Responsive preview icon styling.
+ */
+.toolbar-tab-responsive-preview .icon:before,
+.responsive-preview .icon:before {
+  background-attachment: scroll;
+  background-color: transparent;
+  background-image: url("../images/responsive-preview-icons.png");
+  background-repeat: no-repeat;
+  content: '';
+  display: block;
+  position: absolute;
+  z-index: 1;
+}
+.toolbar .bar .toolbar-tab-responsive-preview .icon:before {
+  width: 13px;
+}
+.responsive-preview button.icon {
+  background-color: transparent;
+  border: 0;
+  font-size: 1em;
+}
+
+/* Toolbar icon. */
+.toolbar .bar .icon.icon-responsive-preview {
+  margin-left: 0;
+  margin-right: 0;
+  padding-left: 0;
+  padding-right: 0;
+  width: 5em;
+}
+.toolbar-tab-responsive-preview .icon.icon-responsive-preview:before {
+  background-position: center top;
+}
+.toolbar-tab-responsive-preview.open .icon-responsive-preview:before,
+.toolbar-tab-responsive-preview .icon-responsive-preview.active:before,
+.toolbar-tab-responsive-preview .icon-responsive-preview:hover:before {
+  background-position: center -22px;
+}
+.toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: 1em; /* LTR */
+  height: 22px;
+  top: 0.6667em;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active {
+  padding-left: 2.25em;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before {
+  background-position: -999px -999px;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active.active:before {
+  background-position: center -110px;
+}
+@media only screen and (min-width: 16.5em) {
+  .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active {
+    padding: 0.5em 1.3333em 0.5em 2.25em;
+    text-indent: 0;
+    width: auto;
+  }
+  .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before {
+    left: 0.667em;
+  }
+}
+
+/**
+ * Responsive preview controls icons.
+ */
+.responsive-preview .control.icon:before {
+  height: 12px;
+  width: 12px;
+  top: 12px;
+}
+.responsive-preview .icon-close:before {
+  background-position: left -44px;
+  right: 9px; /* LTR */
+}
+.responsive-preview .icon-close:active:before,
+.responsive-preview .icon-close.active:before,
+.responsive-preview .icon-close:hover:before {
+  background-position: left -56px;
+}
+.responsive-preview .icon-orientation:before {
+  background-position: left -68px;
+  left: 9px; /* LTR */
+}
+.responsive-preview .icon-orientation:hover:before {
+  background-position: left -80px;
+}
+.responsive-preview .icon-orientation.rotated:before {
+  background-position: left -92px;
+}
+.responsive-preview .icon-orientation.rotated:hover:before {
+  background-position: left -104px;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css
new file mode 100644
index 0000000..b8892ab
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css
@@ -0,0 +1,30 @@
+/**
+ * @file
+ * RTL styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+
+/* Toolbar tab triangle toggle. */
+.toolbar-tab-responsive-preview .trigger:after {
+  left: 1em;
+  right: auto;
+}
+.toolbar-tab-responsive-preview.open:before {
+  left: 0;
+  right: auto;
+}
+.toolbar-tab-responsive-preview.open .trigger:after {
+  left: 0.7em;
+  right: auto;
+}
+.responsive-preview .control.close {
+  left: 0;
+  right: auto;
+}
+.responsive-preview .control.orientation {
+  left: auto;
+  right: 0;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.theme.css b/core/modules/responsive_preview/css/responsive-preview.theme.css
new file mode 100644
index 0000000..ae19d94
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme.css
@@ -0,0 +1,179 @@
+/**
+ * @file
+ * Styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+.toolbar-tab-responsive-preview .options {
+  background-color: #0f0f0f;
+}
+/* Device preview options. */
+.toolbar-tab-responsive-preview .options {
+  box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75);
+}
+.toolbar-tab-responsive-preview .options li {
+  background-color: white;
+  padding: 0;
+}
+.toolbar-tab-responsive-preview .trigger {
+  cursor: pointer;
+  line-height: 1;
+  height: 3em;
+}
+.toolbar-tab-responsive-preview .trigger:hover {
+  background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
+  background-image: linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
+}
+.toolbar-tab-responsive-preview .trigger.active,
+.toolbar-tab-responsive-preview .trigger.active:hover {
+  background-image: -webkit-linear-gradient(top, rgb(78,159,234) 0%, rgb(69,132,221) 100%);
+  background-image: linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%);
+}
+.toolbar-tab-responsive-preview .trigger,
+.toolbar-tab-responsive-preview .options .device {
+  padding-bottom: 1em;
+  padding-top: 1em;
+}
+.toolbar-tab-responsive-preview .options .device {
+  background: none;
+  border: none;
+  cursor: pointer;
+  font-family: inherit;
+  font-size: 1em;
+  padding: 0.5em 1.3333em
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device {
+  color: #777;
+}
+.toolbar .toolbar-tab-responsive-preview.tab .options .device:hover,
+.toolbar .toolbar-tab-responsive-preview.tab .options .device.active {
+  color: black;
+}
+
+/* Toolbar tab triangle toggle. */
+.toolbar-tab-responsive-preview .trigger:after {
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  border-style: solid;
+  border-width: 0.4545em 0.4em 0;
+  color: #a0a0a0;
+  content: ' ';
+  display: block;
+  height: 0;
+  line-height: 0;
+  overflow: hidden;
+  position: absolute;
+  right: 1.6667em; /* LTR */
+  top: 50%;
+  margin-top: -0.1666em;
+  width: 0;
+  z-index: 1
+}
+.toolbar-tab-responsive-preview.open:before {
+  background-color: white;
+  bottom: 0;
+  content: ' ';
+  display: block;
+  position: absolute;
+  right: 0; /* LTR */
+  top: 0;
+  width: 2em;
+  z-index: 1;
+}
+.toolbar-tab-responsive-preview.open .trigger:after {
+  border-bottom: 0.4545em solid;
+  border-top-color: transparent;
+  color: black;
+  right: 0.7em; /* LTR */
+  top: 1.25em;
+}
+.toolbar-tab-responsive-preview:hover .trigger:after,
+.toolbar-tab-responsive-preview .trigger.active:after,
+.toolbar-tab-responsive-preview:hover .trigger.active:after {
+  color: white;
+}
+.toolbar-tab-responsive-preview.open:hover .trigger:after {
+  color: black;
+}
+
+/**
+ * Preview container.
+ */
+.responsive-preview {
+  box-shadow: 0 0 10px 0 black;
+  opacity: 0;
+  -moz-transition: all 450ms;
+  -webkit-transition: all 450ms;
+  transition: all 450ms;
+}
+.responsive-preview.active {
+  opacity: 1;
+}
+.responsive-preview .modal-background {
+  background-color: black;
+  background-color: rgba(0,0,0,0.92);
+  background-image: -webkit-linear-gradient(left, black,rgb(20,20,20) 15%, rgb(45,45,45) 40%, rgb(45,45,45) 60%, rgb(20,20,20) 85%, black 100%);
+  background-image: linear-gradient(left, black, rgb(20,20,20) 15%, rgb(45,45,45) 40%, rgb(45,45,45) 60%, rgb(20,20,20) 85%, black 100%);
+}
+
+/**
+ * Responsive preview control placement.
+ */
+.responsive-preview .control {
+  cursor: pointer;
+  height: 40px;
+  position: absolute;
+  top: 0;
+  width: 40px;
+}
+.responsive-preview .control.close {
+  right: 0; /* LTR */
+}
+.responsive-preview .control.orientation {
+  left: 0; /* LTR */
+}
+.responsive-preview .device-label {
+  color: #e0e0e0;
+  font-family: sans-serif;
+  font-size: 0.75em;
+  font-weight: normal;
+  left: 40px;
+  letter-spacing: 0.25ex;
+  line-height: 2.6667;
+  margin: 0;
+  overflow: hidden;
+  position: absolute;
+  right: 40px;
+  text-overflow: ellipsis;
+  top: 0;
+  white-space: nowrap;
+  width: auto;
+}
+
+/**
+ * Responsive preview frame.
+ */
+.responsive-preview .frame-container {
+  background-color: #343434;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0px 1px #404040,
+    2px 2px 0 0px black;
+  -webkit-transition: all 150ms ease-out;
+  -moz-transition: all 150ms ease-out;
+  -o-transition: all 150ms ease-out;
+  transition: all 150ms ease-out;
+  top: 1em;
+}
+.responsive-preview .frame-container iframe {
+  box-shadow:
+    0 0 0 2px black,
+    0 0 0 3px #404040;
+  -webkit-transition: all 150ms ease-out;
+  -moz-transition: all 150ms ease-out;
+  -o-transition: all 150ms ease-out;
+  transition: all 150ms ease-out;
+}
diff --git a/core/modules/responsive_preview/images/responsive-preview-icons.png b/core/modules/responsive_preview/images/responsive-preview-icons.png
new file mode 100644
index 0000000..6eedbbe
--- /dev/null
+++ b/core/modules/responsive_preview/images/responsive-preview-icons.png
@@ -0,0 +1,10 @@
+PNG
+
+   IHDR         Bq   tEXtSoftware Adobe ImageReadyqe<  IDATxXO#G]/6}	:HH*>)]R:]
+8E4A#
+H`wwꄎD(/u"*MA"@m؛[~z(b|f{ofV+H	^z=
+#_cccj[[[(LⱺaA<n+݃Z-4o`id|(]D҇ry#ZʫXɈ,F,SC^٨yNhNI lǑ9c|l#p(^!gm:!?EN 1P$EJ%	 >cfN& x]㛯 1YᴗxyYi-g`zG>9NQfH"rZUU3E{+űXDNs-d1Y[ ɿҝt
+aC N
+B,VL巧<JD }*_4M#YVH~kZmZd077WRh* &R@g4 @hluu6;;nE̜fbu>hHzr:6uMnnnZߡ<w-¤,wvww	6 ?rJ/0^ǝ8??Ju hF٥6Dtt*B!PzPX|B`D+`6kEAb;...>O&ߛBn<2Ibķ(D"- 0nB PGjfe||<IY|!rʁG${	^N?OOO۬n)@cRURbrr'-oAA_1S\aw@fՕzȅ: C+L0m@333=|:P0WV5q_Yӡ#=\:a!I!)ve
+m
+qn5`%osR"Y VEa%ˍY>|Ay-}jy"S{GPI CXt`*'ANQ]iAj˩!ga)="' 2jHW3cV!?QE<_	b xe\@<X_P d_:^OpׂMCLxb J0 v]{q{Ȍ    IENDB`
\ No newline at end of file
diff --git a/core/modules/responsive_preview/js/responsive-preview.js b/core/modules/responsive_preview/js/responsive-preview.js
new file mode 100644
index 0000000..48349ee
--- /dev/null
+++ b/core/modules/responsive_preview/js/responsive-preview.js
@@ -0,0 +1,673 @@
+/**
+ * @file
+ * Provides a component that previews the page in various device dimensions.
+ */
+
+(function ($, Backbone, Drupal, drupalSettings, window, document) {
+
+"use strict";
+
+/**
+ * Attaches behaviors to the toolbar tab and preview containers.
+ */
+Drupal.behaviors.responsivePreview = {
+  attach: function (context) {
+    var defaults = this.defaults;
+    // once() returns a jQuery set. It will be empty if no unprocessed
+    // elements are found. window and window.parent are equivalent unless the
+    // Drupal page is itself wrapped in an iframe.
+    var $body = $(window.parent.document.body).once('responsive-preview');
+
+    if ($body.length) {
+      var options = $.extend(defaults, drupalSettings.responsivePreview || {});
+      // If this window is itself in an iframe it must be marked as processed.
+      // Its parent window will have been processed above.
+      // When attach() is called again for the preview iframe, it will check
+      // its parent window and find it has been processed. In most cases, the
+      // following code will have no effect.
+      $(window.document.body).once('responsive-preview');
+
+      var envModel = new Drupal.responsivePreview.models.EnvironmentModel({
+        dir: document.getElementsByTagName('html')[0].getAttribute('dir')
+      });
+      var tabModel = new Drupal.responsivePreview.models.TabStateModel();
+      var previewModel = new Drupal.responsivePreview.models.PreviewStateModel();
+
+      // The toolbar tab view.
+      var tabView = new Drupal.responsivePreview.views.TabView({
+        el: $(context).find('#responsive-preview-toolbar-tab'),
+        model: tabModel,
+        envModel: envModel,
+        // Gutter size around preview frame.
+        gutter: options.gutter,
+        // Preview device frame width.
+        bleed: options.bleed
+      });
+      // The preview container view.
+      var previewView = new Drupal.responsivePreview.views.PreviewView({
+        el: Drupal.theme('responsivePreviewContainer'),
+        model: previewModel,
+        tabModel: tabModel,
+        envModel: envModel,
+        // Gutter size around preview frame.
+        gutter: options.gutter,
+        // Preview device frame width.
+        bleed: options.bleed,
+        strings: options.strings
+      });
+
+      var setViewportWidth = function() {
+        envModel.set('viewportWidth', document.documentElement.clientWidth);
+      };
+
+      $(window)
+        // Update the viewport width whenever it is resized, but max 4 times/s.
+        .on('resize.responsivePreview', Drupal.debounce(setViewportWidth, 250));
+
+      // Allow other scripts to respond to responsive preview mode changes.
+      tabModel.on('change:isActive', function (model, isActive) {
+        $(document).trigger((isActive) ? 'drupalResponsivePreviewStarted' : 'drupalResponsivePreviewStopped');
+      });
+
+      // Initialization: set the current viewport width.
+      setViewportWidth();
+    }
+    // The main window is equivalent to window.parent and window.self. Inside,
+    // an iframe, these objects are not equivalent. If the parent window is
+    // itself in an iframe, check that the parent window has been processed.
+    // If it has been, this invocation of attach() is being called on the
+    // preview iframe, not its parent.
+    if ((window.parent !== window.self) && !$body.length) {
+      var $frameBody = $(window.self.document.body).once('responsive-preview');
+      if ($frameBody.length > 0) {
+        $frameBody.get(0).className += ' responsive-preview-frame';
+      }
+    }
+  },
+  defaults: {
+    gutter: 60,
+    // The width of the device border around the iframe. This value is critical
+    // to determine the size and placement of the preview iframe container,
+    // therefore it must be defined here instead of in the CSS file.
+    bleed: 30,
+    strings: {
+      close: Drupal.t('close'),
+      orientation: Drupal.t('Change orientation'),
+      portrait: Drupal.t('portrait'),
+      landscape: Drupal.t('landscape')
+    }
+  }
+};
+
+Drupal.responsivePreview = Drupal.responsivePreview || {models: {}, views: {}};
+
+/**
+ * Backbone Model for the environment in which the Responsive Preview operates.
+ */
+Drupal.responsivePreview.models.EnvironmentModel = Backbone.Model.extend({
+  defaults: {
+    // The viewport width, within which the preview will have to fit.
+    viewportWidth: null,
+    // Text direction of the document, affects some positioning.
+    dir: 'ltr'
+  }
+});
+
+/**
+ * Backbone Model for the Responsive Preview toolbar tab state.
+ */
+Drupal.responsivePreview.models.TabStateModel = Backbone.Model.extend({
+  defaults: {
+    // The state of the preview.
+    isActive: false,
+    // The state of toolbar list if available device previews.
+    isDeviceListOpen: false,
+    // The number of devices that fit the current viewport (i.e. previewable).
+    fittingDeviceCount: 0,
+    // Currently selected device link.
+    activeDevice: null,
+    // Dimensions of the currently selected device to preview.
+    dimensions: {
+      // The width of the device to preview.
+      width: null,
+      // The height of the device to preview.
+      height: null,
+      // The dots per pixel of the device to preview.
+      dppx: null
+    }
+  }
+});
+
+/**
+ * Backbone Model for the Responsive Preview preview state.
+ */
+Drupal.responsivePreview.models.PreviewStateModel = Backbone.Model.extend({
+  defaults: {
+    // Indicates whether the preview iframe has been built.
+    isBuilt: false,
+    // Indicates whether the device is portrait (false) or landscape (true).
+    isRotated: false
+  }
+});
+
+/**
+ * Handles responsive preview toolbar tab interactions.
+ */
+Drupal.responsivePreview.views.TabView = Backbone.View.extend({
+
+  events: {
+    'click': 'toggleDeviceList',
+    'mouseleave': 'toggleDeviceList',
+    'click .device': 'selectDevice'
+  },
+
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function () {
+    this.gutter = this.options.gutter;
+    this.bleed = this.options.bleed;
+    this.envModel = this.options.envModel;
+
+    this.model.on('change:isActive change:isDeviceListOpen change:fittingDeviceCount change:activeDevice', this.render, this);
+    this.envModel.on('change:viewportWidth', this.updateDeviceList, this);
+    this.envModel.on('change:viewportWidth', this.correctDeviceListEdgeCollision, this);
+  },
+
+  /**
+   * Implements Backbone.View.prototype.render().
+   */
+  render: function () {
+    var $deviceLink = $(this.model.get('activeDevice'));
+    var isActive = this.model.get('isActive');
+    var isDeviceListOpen = this.model.get('isDeviceListOpen');
+    this.$el
+      // Render the visibility of the toolbar tab.
+      .toggle(this.model.get('fittingDeviceCount') > 0)
+      // Toggle the display of the device list.
+      .toggleClass('open', isDeviceListOpen)
+      // Render the state of the toolbar tab button.
+      .find('> button')
+      .toggleClass('active', isActive)
+      .attr('aria-pressed', isActive)
+      // Return to $el.
+      .end()
+      .find('.device.active')
+      .removeClass('active')
+      // Return to $el.
+      .end()
+      .find($deviceLink)
+      .addClass('active');
+    // When the preview is active, a class on the body is necessary to impose
+    // styling to aid in the display of the preview element.
+    $('body').toggleClass('responsive-preview-active', isActive);
+    // The list of devices might render outside the window.
+    if (isDeviceListOpen) {
+      this.correctDeviceListEdgeCollision();
+    }
+    return this;
+  },
+
+  /**
+   * Toggles the list of devices available to preview from the toolbar tab.
+   *
+   * @param Object event
+   *   jQuery Event object.
+   */
+  toggleDeviceList: function (event) {
+    // Force the options list closed on mouseleave.
+    if (event.type === 'mouseleave') {
+      this.model.set('isDeviceListOpen', false);
+    }
+    else {
+      this.model.set('isDeviceListOpen', !this.model.get('isDeviceListOpen'));
+    }
+
+    event.preventDefault();
+    event.stopPropagation();
+  },
+
+  /**
+   * Model change handler; corrects possible device list window edge collision.
+   */
+  correctDeviceListEdgeCollision: function () {
+    // The position of the dropdown depends on the language direction.
+    var dir = this.envModel.get('dir');
+    var edge = (dir === 'rtl') ? 'left' : 'right';
+    this.$el
+      .find('.item-list')
+      .position({
+        'my': edge +' top',
+        'at': edge + ' bottom',
+        'of': this.$el,
+        'collision': 'flip fit'
+      });
+  },
+
+  /**
+   * Model change handler; hides devices that don't fit the current viewport.
+   */
+  updateDeviceList: function () {
+    var gutter = this.gutter;
+    var bleed = this.bleed;
+    var viewportWidth = this.envModel.get('viewportWidth');
+    var $devices = this.$el.find('.device');
+
+    // Remove devices whose previews won't fit the current viewport.
+    $devices.each(function (index, element) {
+      var $this = $(this);
+      var width = parseInt($this.data('responsive-preview-width'), 10);
+      var dppx = parseFloat($this.data('responsive-preview-dppx'), 10);
+      var previewWidth = width / dppx;
+      var fits = ((previewWidth + (gutter * 2) + (bleed * 2)) <= viewportWidth);
+      $this.parent('li').toggleClass('element-hidden', !fits);
+    });
+    // Set the number of devices that fit the current viewport.
+    this.model.set('fittingDeviceCount', $devices.parent('li').not('.element-hidden').length);
+  },
+
+  /**
+   * Updates the model to reflect the properties of the chosen device.
+   *
+   * @param Object event
+   *   A jQuery event object.
+   */
+  selectDevice: function (event) {
+    var $link = $(event.target);
+    // Update the device dimensions.
+    this.model.set({
+      'activeDevice': $link.get(0),
+      'dimensions': {
+        'width': parseInt($link.data('responsive-preview-width'), 10),
+        'height': parseInt($link.data('responsive-preview-height'), 10),
+        'dppx': parseFloat($link.data('responsive-preview-dppx'), 10)
+      }
+    });
+    // Toggle the preview on.
+    this.model.set('isActive', true);
+
+    event.preventDefault();
+  }
+
+});
+
+/**
+ * Handles the responsive preview element interactions.
+ */
+Drupal.responsivePreview.views.PreviewView = Backbone.View.extend({
+
+  events: {
+    'click #responsive-preview-close': 'onClose',
+    'click #responsive-preview-orientation': 'onRotate'
+  },
+
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function () {
+    this.gutter = this.options.gutter;
+    this.bleed = this.options.bleed;
+    this.strings = this.options.strings;
+    this.tabModel = this.options.tabModel;
+    this.envModel = this.options.envModel;
+
+    this.model.on('change:isRotated', this.render, this);
+
+    this.tabModel.on('change:isActive change:dimensions change:activeDevice', this.render, this);
+
+    // Recalculate the size of the preview container when the window resizes.
+    this.envModel.on('change:viewportWidth', this.render, this);
+  },
+
+  /**
+   * Implements Backbone.View.prototype.render().
+   */
+  render: function () {
+    var isActive = this.tabModel.get('isActive');
+
+    // Build the preview if it doesn't exist.
+    if (isActive && !this.model.get('isBuilt')) {
+      this._build();
+    }
+
+    // Early-return if inactive.
+    if (isActive) {
+      // Refresh the preview.
+      this._refresh();
+    }
+
+    // Render the state of the preview.
+    var that = this;
+    // Wrap the call in a setTimeout so that it invokes in the next compute
+    // cycle, causing the CSS animations to render in the first pass.
+    window.setTimeout(function () {
+      that.$el.toggleClass('active', isActive);
+    }, 0);
+
+    return this;
+  },
+
+  /**
+   * Closes the preview.
+   *
+   * @param Object event
+   *   A jQuery event object.
+   */
+  onClose: function (event) {
+    this.tabModel.set('isActive', false);
+  },
+
+  /**
+   * Responds to rotation button presses.
+   *
+   * @param Object event
+   *   A jQuery event object.
+   */
+  onRotate: function (event) {
+    this.model.set('isRotated', !this.model.get('isRotated'));
+  },
+
+  /**
+   * Builds the preview iframe.
+   */
+  _build: function () {
+    var $frameContainer = $(Drupal.theme('responsivePreviewFrameContainer'))
+      .find('#responsive-preview-close span')
+      .text(this.strings.close)
+      .end()
+      .find('#responsive-preview-orientation span')
+      .text(this.strings.orientation)
+      .end()
+      // The padding around the frame must be known in order to position it
+      // correctly, so the style property is defined in JavaScript rather than
+      // CSS.
+      .css('padding', this.bleed);
+    // Attach the iframe that will hold the preview.
+    var $frame = $(Drupal.theme('responsivePreviewFrame'))
+      .attr({
+        'data-loading': true,
+        src: drupalSettings.basePath + Drupal.encodePath(drupalSettings.currentPath),
+        width: '100%',
+        height: '100%'
+      })
+      // Load the current page URI into the preview iframe.
+      .on('load.responsivePreview', $.proxy(this._refresh, this))
+      // Add the frame to the preview container.
+      .appendTo($frameContainer);
+    // Adjust the placement of the preview container and insert it into the DOM.
+    this.$el
+      .css({ top: this._getDisplacement('top') })
+      // Displace the top of the container.
+      .attr('data-offset-top', this._getDisplacement('top'))
+      // Apend the frame container.
+      .append($frameContainer)
+      // Append the container to the body to initialize the iframe document.
+      .appendTo('body');
+    // Mark the preview element processed.
+    this.model.set('isBuilt', true);
+  },
+
+  /**
+   * Refreshes the preview based on the current state (device & viewport width).
+   */
+  _refresh: function () {
+    var isRotated = this.model.get('isRotated');
+    var $deviceLink = $(this.tabModel.get('activeDevice'));
+    var $container = this.$el.find('#responsive-preview-frame-container');
+    var $frame = $container.find('> iframe');
+
+    // Get the static state.
+    var edge = (this.envModel.get('dir') === 'rtl') ? 'right' : 'left';
+    var minGutter = this.gutter;
+
+    // Get current (dynamic) state.
+    var dimensions = this.tabModel.get('dimensions');
+    var isRotated = this.model.get('isRotated');
+    var viewportWidth = this.envModel.get('viewportWidth');
+
+    // Calculate preview width & height. If the preview is rotated, swap width
+    // and height.
+    var width = dimensions[(isRotated) ? 'height' : 'width'] / dimensions.dppx;
+    var height = dimensions[(isRotated) ? 'width' : 'height'] / dimensions.dppx;
+
+    // Get the container padding and border width for the left and right.
+    var bleed = this.bleed;
+    var spread = width + (bleed * 2);
+
+    // Calculate gutter.
+    var gutterPercent = (1 - (spread / viewportWidth)) / 2;
+    var gutter = gutterPercent * viewportWidth;
+    gutter = (gutter < minGutter) ? minGutter : gutter;
+
+    // The preview width plus gutters must fit within the viewport width.
+    width = (viewportWidth - (gutter * 2) < spread) ? viewportWidth - (gutter * 2) - (bleed * 2) : width;
+
+    // Updated the state of the rotated icon.
+    this.$el.find('.control.orientation').toggleClass('rotated', isRotated);
+
+    // Resize & reposition the iframe.
+    var position = {};
+    position[edge] = gutter; // Depends on text direction.
+    $frame
+      .css({
+        width: width,
+        height: height
+      });
+    $container
+      .css(position);
+
+    // Scale if not responsive.
+    this._scaleIfNotResponsive();
+
+    // Update the device label.
+    $container.find('.device-label').text(Drupal.t('@label (@widthpx by @heightpx, @dpidppx, @orientation)', {
+      '@label': $deviceLink.text(),
+      '@width': Math.ceil(width),
+      '@height': Math.ceil(height),
+      '@dpi': dimensions.dppx,
+      '@orientation': (isRotated) ? this.strings.landscape : this.strings.portrait
+    }));
+  },
+
+  /**
+   * Applies scaling in order to better approximate content display on a device.
+   */
+  _scaleIfNotResponsive: function () {
+    var scalingCSS = this._calculateScalingCSS();
+    if (scalingCSS === false) {
+      return;
+    }
+
+    // Step 0: find DOM nodes we'll need to modify.
+    var $frame = this.$el.find('#responsive-preview-frame');
+    var $html = $($frame[0].contentDocument || $frame[0].contentWindow.document).find('html');
+
+    // Step 1: When scaling (as we're about to do), the background (color and
+    // image) doesn't scale along. Fortunately, we can fix things in case of
+    // background color.
+    // @todo: figure out a work-around for background images, or somehow
+    // document this explicitly.
+    function isTransparent (color) {
+      // TRICKY: edge case for Firefox' "transparent" here; this is a
+      // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724
+      return (color === 'rgba(0, 0, 0, 0)' || color === 'transparent');
+    }
+    var htmlBgColor = $html.css('background-color');
+    var bodyBgColor = $html.find('body').css('background-color');
+    if (!isTransparent(htmlBgColor) || !isTransparent(bodyBgColor)) {
+      var bgColor = isTransparent(htmlBgColor) ? bodyBgColor : htmlBgColor;
+      $frame.css('background-color', bgColor);
+    }
+
+    // Step 2: apply scaling.
+    $html.css(scalingCSS);
+  },
+
+  /**
+   * Calculates scaling based on device dimensions and <meta name="viewport" />.
+   *
+   * Websites that don't indicate via <meta name="viewport" /> that their width
+   * is identical to the device width will be rendered at a larger size: at the
+   * layout viewport's default width. This width exceeds the visual viewport on
+   * the device, and causes it to scale it down.
+   *
+   * This function checks whether the underlying web page is responsive, and if
+   * it's not, then it will calculate a CSS scaling transformation, to closely
+   * approximate how an actual mobile device would render the web page.
+   *
+   * We assume all mobile devices' layout viewport's default width is 980px. It
+   * is the value used on all iOS and Android >=4.0 devices.
+   *
+   * Related reading:
+   *  - http://www.quirksmode.org/mobile/viewports.html
+   *  - http://www.quirksmode.org/mobile/viewports2.html
+   *  - https://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
+   *  - http://tripleodeon.com/2011/12/first-understand-your-screen/
+   *  - http://tripleodeon.com/wp-content/uploads/2011/12/table.html?r=android40window.innerw&c=980
+   */
+  _calculateScalingCSS: function () {
+    var settings = this._parseViewportMetaTag();
+    var defaultLayoutWidth = 980, initialScale = 1;
+    var layoutViewportWidth, layoutViewportHeight;
+    var visualViewPortWidth; // The visual viewport width === the preview width.
+
+    if (settings.width) {
+      if (settings.width === 'device-width') {
+        // Don't scale if the page is marked to be as wide as the device.
+        return false;
+      }
+      else {
+        layoutViewportWidth = parseInt(settings.width, 10);
+      }
+    }
+    else {
+      layoutViewportWidth = defaultLayoutWidth;
+    }
+
+    if (settings.height && settings.height !== 'device-height') {
+      layoutViewportHeight = parseInt(settings.height, 10);
+    }
+
+    if (settings['initial-scale']) {
+      initialScale = parseFloat(settings['initial-scale'], 10);
+      if (initialScale < 1) {
+        layoutViewportWidth = defaultLayoutWidth;
+      }
+    }
+
+    // Calculate the scale, prevent excesses (ensure the (0.25, 2) range).
+    var dimensions = this.tabModel.get('dimensions');
+    visualViewPortWidth = dimensions.width / dimensions.dppx;
+    var scale = initialScale * (100 / layoutViewportWidth) * (visualViewPortWidth / 100);
+    scale = Math.min(scale, 2);
+    scale = Math.max(scale, 0.25);
+
+    var transform = "scale(" + scale + ")";
+    var origin = "0 0";
+    return {
+        'min-width': layoutViewportWidth + 'px',
+        'min-height': layoutViewportHeight + 'px',
+        '-webkit-transform': transform,
+            '-ms-transform': transform,
+                'transform': transform,
+        '-webkit-transform-origin': origin,
+            '-ms-transform-origin': origin,
+                'transform-origin': origin
+    };
+  },
+
+  /**
+   * Parses <meta name="viewport" /> tag's "content" attribute, if any.
+   *
+   * Parses something like this:
+   *   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, minimum-scale=1, user-scalable=yes">
+   * into this:
+   *   {
+   *     width: 'device-width',
+   *     initial-scale: '1',
+   *     maximum-scale: '5',
+   *     minimum-scale: '1',
+   *     user-scalable: 'yes'
+   *   }
+   *
+   * @return Object
+   *   Parsed viewport settings, or {}.
+   */
+  _parseViewportMetaTag: function () {
+    var settings = {};
+    var $viewportMeta = $(document).find('meta[name=viewport][content]');
+    if ($viewportMeta.length > 0) {
+      $viewportMeta
+        .attr('content')
+        // Reduce multiple parts of whitespace to a single space.
+        .replace(/\s+/g, '')
+        // Split on comma (which separates the different settings).
+        .split(',')
+        .map(function (setting) {
+          setting = setting.split('=');
+          settings[setting[0]] = setting[1];
+        });
+    }
+    return settings;
+  },
+
+  /**
+   * Gets the total displacement of given region.
+   *
+   * @param String region
+   *   Region name. Either "top" or "bottom".
+   *
+   * @return Number
+   *   The total displacement of given region in pixels.
+   */
+  _getDisplacement: function (region) {
+    var displacement = 0;
+    var lastDisplaced = $('[data-offset-' + region + ']');
+    if (lastDisplaced.length) {
+      displacement = parseInt(lastDisplaced.attr('data-offset-' + region), 10);
+    }
+    return displacement;
+  }
+
+});
+
+/**
+ * Registers theme templates with Drupal.theme().
+ */
+$.extend(Drupal.theme, {
+  /**
+   * Theme function for the preview container element.
+   *
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewContainer: function () {
+    return '<div id="responsive-preview" class="responsive-preview"><div class="modal-background"></div></div>';
+  },
+
+  /**
+   * Theme function for the close button for the preview container.
+   *
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewFrameContainer: function () {
+    return '<div id="responsive-preview-frame-container" class="frame-container">'
+      + '<label id="responsive-preview-frame-label" class="device-label"></label>'
+      + '<button id="responsive-preview-close" role="button" class="icon icon-close control close" aria-pressed="false"><span class="element-invisible"></span></button>'
+      + '<button id="responsive-preview-orientation" role="button" class="icon icon-orientation control orientation" aria-pressed="false"><span class="element-invisible"></span></button>'
+      + '</div>';
+  },
+
+  /**
+   * Theme function for a responsive preview iframe element.
+   *
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewFrame: function (url) {
+    return '<iframe id="responsive-preview-frame" frameborder="0" scrolling="auto" allowtransparency="true"></iframe>';
+  }
+});
+
+}(jQuery, Backbone, Drupal, drupalSettings, window, document));
diff --git a/core/modules/responsive_preview/responsive_preview.info b/core/modules/responsive_preview/responsive_preview.info
new file mode 100644
index 0000000..e10cfe1
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.info
@@ -0,0 +1,7 @@
+name = Responsive Preview
+description = Provides a component that previews a page in various device dimensions.
+package = Core
+version = VERSION
+core = 8.x
+
+dependencies[] = testswarm
diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module
new file mode 100644
index 0000000..3ee5395
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.module
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * @file
+ * Provides a component that previews the a page in various device dimensions.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function responsive_preview_help($path, $arg) {
+
+  switch ($path) {
+    case 'admin/help#responsive_preview':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Responsive Preview module provides a quick way to preview a page on your site within the dimensions of many popular device and screen sizes.') . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<p>' . t('To launch a preview, first click the toolbar tab with the small device icon. The tab has the title "@title". A list of devices will appear. Selecting a device name will launch a preview of the current page within the dimensions of that device.', array('@title' => t('Preview page layout'))) . '</p>';
+      $output .= '<p>' . t('To close the preview, click the close button signified visually by an x.') . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Returns a list of devices and their properties from configuration.
+ */
+function responsive_preview_get_devices_list() {
+  $devices = config('responsive_preview.devices')->get();
+
+  $links = array();
+  foreach($devices as $name => $info) {
+    $links[$name] = array(
+      '#theme' => 'html_tag',
+      '#tag' => 'button',
+      '#value' => $info['label'],
+      '#attributes' => array(
+        'class' => array('device', 'icon', 'icon-active'),
+        'data-responsive-preview-width' => ($info['dimensions']['width']) ? $info['dimensions']['width'] : '',
+        'data-responsive-preview-height' => ($info['dimensions']['height']) ? $info['dimensions']['height'] : '',
+        'data-responsive-preview-dppx' => ($info['dimensions']['dppx']) ? $info['dimensions']['dppx'] : '1',
+      ),
+    );
+  }
+
+  return $links;
+}
+
+/**
+ * Prevents the preview tab from rendering on administration pages.
+ */
+function responsive_preview_access() {
+  return !path_is_admin(current_path());
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function responsive_preview_toolbar() {
+  $items['responsive_preview'] = array(
+    '#type' => 'toolbar_item',
+    'tab' => array(
+      'trigger' => array(
+        '#theme' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => t('Layout preview'),
+        '#value_prefix' => '<span class="element-invisible">',
+        '#value_suffix' => '</span>',
+        '#attributes' => array(
+          'title' => t('Preview page layout'),
+          'class' => array('icon', 'icon-responsive-preview', 'trigger'),
+        ),
+      ),
+      'device_options' => array(
+        '#theme' => 'item_list',
+        '#items' => responsive_preview_get_devices_list(),
+        '#attributes' => array(
+          'class' => array('options'),
+        ),
+      ),
+    ),
+    '#wrapper_attributes' => array(
+      'id' => 'responsive-preview-toolbar-tab',
+      'class' => array('toolbar-tab-responsive-preview'),
+    ),
+    '#attached' => array(
+      'library' => array(
+        array('responsive_preview', 'responsive-preview'),
+      ),
+    ),
+    '#weight' => 200,
+    '#access' => responsive_preview_access(),
+  );
+
+  return $items;
+}
+
+/**
+ * Implements hook_library().
+ */
+function responsive_preview_library_info() {
+  $path = drupal_get_path('module', 'responsive_preview');
+  $options = array(
+    'scope' => 'footer',
+    'attributes' => array('defer' => TRUE),
+  );
+
+  $libraries['responsive-preview'] = array(
+    'title' => 'Preview layouts',
+    'version' => VERSION,
+    'css' => array(
+      $path . '/css/responsive-preview.base.css',
+      $path . '/css/responsive-preview.theme.css',
+      $path . '/css/responsive-preview.icons.css',
+    ),
+    'js' => array(
+      $path . '/js/responsive-preview.js' => $options,
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'backbone'),
+      array('system', 'jquery.ui.position'),
+    ),
+  );
+
+  return $libraries;
+}
+
+/**
+ * Implements hook_testswarm_tests().
+ */
+function responsive_preview_testswarm_tests() {
+
+  $path = drupal_get_path('module', 'responsive_preview');
+
+  return array(
+    'responsivePreview' => array(
+      'module' => 'responsive_preview',
+      'description' => 'Test the responsive preview module.',
+      'js' => array(
+        $path . '/tests/testswarm/responsive_preview.tests.js' => array(),
+        array(
+          'data' => array(
+            'responsive_preview' => array(
+              'devices' => config('responsive_preview.devices')->get()
+            ),
+          ),
+          'type' => 'setting',
+        ),
+      ),
+      'dependencies' => array(
+        array('system', 'jquery'),
+        array('system', 'drupalSettings'),
+        array('testswarm', 'jquery.simulate'),
+      ),
+      'path' => '',
+      'permissions' => array()
+    ),
+    'responsivePreviewAdmin' => array(
+      'module' => 'responsive_preview',
+      'description' => 'Test the responsive preview module admin.',
+      'js' => array(
+        $path . '/tests/testswarm/responsive_preview.admin.tests.js' => array(),
+      ),
+      'dependencies' => array(
+        array('system', 'jquery'),
+      ),
+      'path' => 'admin',
+      'permissions' => array()
+    ),
+  );
+}
diff --git a/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js b/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js
new file mode 100644
index 0000000..b0466e9
--- /dev/null
+++ b/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js
@@ -0,0 +1,27 @@
+/*jshint strict:true, browser:true, curly:true, eqeqeq:true, expr:true, forin:true, latedef:true, newcap:true, noarg:true, trailing: true, undef:true, unused:true */
+/*global Drupal: true, jQuery: true, QUnit:true*/
+(function ($, Drupal, drupalSettings, window, document, undefined) {
+  "use strict";
+  Drupal.tests.responsivePreviewAdmin = {
+    getInfo: function() {
+      return {
+        name: 'Responsive Preview',
+        description: 'Tests for the responsive preview admin.',
+        group: 'Core'
+      };
+    },
+    setup: function () {},
+    teardown: function () {},
+    tests: {
+      toolbarTab: function ($, Drupal, window, document, undefined) {
+        return function() {
+          QUnit.expect(1);
+
+          // The toolbar tab should not be present on an admin path.
+          var $tab = $('.toolbar .toolbar-tab-responsive-preview');
+          QUnit.equal($tab.length, 0, Drupal.t('The tab is not present.'));
+        };
+      }
+    }
+  };
+})(jQuery, Drupal, drupalSettings, this, this.document);
diff --git a/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js b/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js
new file mode 100644
index 0000000..f3f66cc
--- /dev/null
+++ b/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js
@@ -0,0 +1,69 @@
+/*jshint strict:true, browser:true, curly:true, eqeqeq:true, expr:true, forin:true, latedef:true, newcap:true, noarg:true, trailing: true, undef:true, unused:true */
+/*global Drupal: true, jQuery: true, QUnit:true*/
+(function ($, Drupal, drupalSettings, window, document, undefined) {
+  "use strict";
+  Drupal.tests.responsivePreview = {
+    getInfo: function() {
+      return {
+        name: 'Responsive Preview',
+        description: 'Tests for the responsive preview feature.',
+        group: 'Core'
+      };
+    },
+    setup: function () {},
+    teardown: function () {
+      // Close the preview container.
+      $('#responsive-preview-close').trigger('click');
+    },
+    tests: {
+      toolbarTab: function ($, Drupal, window, document, undefined) {
+        return function() {
+          QUnit.expect(3);
+
+          // Find the toolbar tab.
+          var $tab = $('.toolbar .toolbar-tab-responsive-preview');
+          QUnit.equal($tab.length, 1, Drupal.t('The tab is present.'));
+
+          // Verify the tab dropdown click functionality.
+          $tab.find('> .trigger').trigger('click');
+          QUnit.ok($tab.hasClass('open'), Drupal.t('The tab dropdown list opens.'));
+
+          // Verify the number of devices in the list.
+          var devices = drupalSettings.responsive_preview.devices;
+          var count = 0;
+          for (var device in devices) {
+            if (devices.hasOwnProperty(device)) {
+              count++;
+            }
+          }
+          var $devices = $tab.find('.options .device');
+          QUnit.equal($devices.length, count, Drupal.t('The correct number of devices are listed.'));
+        };
+      },
+      previewLaunch: function ($, Drupal, window, document, undefined) {
+        return function () {
+          QUnit.expect(3);
+
+          // Find the toolbar tab.
+          var $tab = $('.toolbar .toolbar-tab-responsive-preview');
+          // Verify that the responsive preview container is not been built yet.
+          var $container = $('#responsive-preview');
+          QUnit.equal($container.length, 0, Drupal.t('The preview container does not exist yet.'));
+
+          // Verify that clicking a device link activates the preview container.
+          $tab.find('.options .device').first().trigger('click');
+          QUnit.stop();
+          window.setTimeout(function () {
+            $container = $('#responsive-preview');
+            // Verify that the preview container exists.
+            QUnit.equal($container.length, 1, Drupal.t('The preview container exists.'));
+
+            // Verify that preview container is active.
+            QUnit.ok($container.hasClass('active'), Drupal.t('The preview container is active.'));
+            QUnit.start();
+          }, 500);
+        };
+      }
+    }
+  };
+})(jQuery, Drupal, drupalSettings, this, this.document);
-- 
1.7.10.4

