From e5c7c0ffcdcbaf4802d7a43a9d00cc419b3b908b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Wed, 27 Feb 2013 01:08:27 -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            |   38 ++
 .../css/responsive-preview.base.css                |  111 ++++
 .../css/responsive-preview.icons-rtl.css           |   21 +
 .../css/responsive-preview.icons.css               |   76 +++
 .../css/responsive-preview.theme-rtl.css           |   30 +
 .../css/responsive-preview.theme.css               |  161 +++++
 .../images/responsive-preview-icons.png            |    7 +
 .../responsive_preview/js/responsive-preview.js    |  646 ++++++++++++++++++++
 .../responsive_preview/responsive_preview.info     |    5 +
 .../responsive_preview/responsive_preview.module   |  127 ++++
 12 files changed, 1280 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

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..7a314cd
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css
@@ -0,0 +1,38 @@
+/**
+ * @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 .responsive-preview-toolbar-tab.tab {
+  float: right;
+}
+/* At wide widths, float the tab to the left. */
+@media only screen and (min-width: 36em) {
+  .js .toolbar .bar .responsive-preview-toolbar-tab.tab {
+    float: left;
+  }
+}
+.responsive-preview-toolbar-tab .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..b98ef78
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.base.css
@@ -0,0 +1,111 @@
+/**
+ * @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.
+ */
+.responsive-preview-toolbar-tab {
+  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 .responsive-preview-toolbar-tab.tab {
+  display: block;
+  float: left; /* LTR */
+  position: relative;
+}
+/* At wide widths, float the tab to the right. */
+@media only screen and (min-width: 36em) {
+  .js .toolbar .bar .responsive-preview-toolbar-tab.tab {
+    float: right; /* LTR */
+  }
+}
+.responsive-preview-toolbar-tab .trigger {
+  display: block;
+}
+/* Device preview options. */
+.responsive-preview-toolbar-tab .item-list {
+  display: none;
+  position: absolute;
+  white-space: nowrap;
+  z-index: 1;
+}
+.responsive-preview-toolbar-tab.open .item-list {
+  display: block;
+}
+.js .responsive-preview-toolbar-tab.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..8f94e7b
--- /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 .responsive-preview-toolbar-tab .icon-responsive-preview:before {
+  left: auto; /* LTR */
+  right: 1em;
+}
+
+/**
+ * Responsive preview controls icons.
+ */
+.responsive-preview .icon-close:before {
+  left: 8px;
+  right: auto;
+}
+.responsive-preview .icon-orientation:before {
+  left: auto;
+  right: 8px;
+}
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..4cd26a3
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons.css
@@ -0,0 +1,76 @@
+/**
+ * @file
+ * Responsive preview icon styling.
+ */
+.responsive-preview-toolbar-tab .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 .responsive-preview-toolbar-tab .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;
+}
+.responsive-preview-toolbar-tab .icon.icon-responsive-preview:before {
+  background-position: center top;
+}
+.responsive-preview-toolbar-tab.open .icon-responsive-preview:before,
+.responsive-preview-toolbar-tab .icon-responsive-preview.active:before,
+.responsive-preview-toolbar-tab .icon-responsive-preview:hover:before {
+  background-position: center -22px;
+}
+.toolbar .bar .responsive-preview-toolbar-tab .icon-responsive-preview:before {
+  left: 1em; /* LTR */
+  height: 22px;
+  top: 0.6667em;
+}
+
+/**
+ * Responsive preview controls icons.
+ */
+.responsive-preview .control.icon:before {
+  height: 12px;
+  width: 12px;
+  top: 10px;
+}
+.responsive-preview .icon-close:before {
+  background-position: left -44px;
+  right: 8px; /* 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: 8px; /* 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..6cfa4f8
--- /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. */
+.responsive-preview-toolbar-tab .trigger:after {
+  left: 1em;
+  right: auto;
+}
+.responsive-preview-toolbar-tab.open:before {
+  left: 0;
+  right: auto;
+}
+.responsive-preview-toolbar-tab.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..1c2adf8
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme.css
@@ -0,0 +1,161 @@
+/**
+ * @file
+ * Styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+.responsive-preview-toolbar-tab .options {
+  background-color: #0f0f0f;
+}
+/* Device preview options. */
+.responsive-preview-toolbar-tab .options {
+  box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75);
+}
+.responsive-preview-toolbar-tab .options li {
+  background-color: white;
+  padding: 0;
+}
+.responsive-preview-toolbar-tab .trigger {
+  cursor: pointer;
+  line-height: 1;
+  height: 3em;
+}
+.responsive-preview-toolbar-tab .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%);
+}
+.responsive-preview-toolbar-tab .trigger.active,
+.responsive-preview-toolbar-tab .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%);
+}
+.responsive-preview-toolbar-tab .trigger,
+.responsive-preview-toolbar-tab .options .device {
+  padding-bottom: 1em;
+  padding-top: 1em;
+}
+.responsive-preview-toolbar-tab .options .device {
+  background: none;
+  border: none;
+  cursor: pointer;
+  font-family: inherit;
+  font-size: 1em;
+  padding: 0.5em 1.3333em
+}
+.toolbar .responsive-preview-toolbar-tab.tab .options .device {
+  color: #777;
+}
+.toolbar .responsive-preview-toolbar-tab.tab .options .device:hover {
+  color: black;
+}
+
+/* Toolbar tab triangle toggle. */
+.responsive-preview-toolbar-tab .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
+}
+.responsive-preview-toolbar-tab.open:before {
+  background-color: white;
+  bottom: 0;
+  content: ' ';
+  display: block;
+  position: absolute;
+  right: 0; /* LTR */
+  top: 0;
+  width: 2em;
+  z-index: 1;
+}
+.responsive-preview-toolbar-tab.open .trigger:after {
+  border-bottom: 0.4545em solid;
+  border-top-color: transparent;
+  color: black;
+  right: 0.7em; /* LTR */
+  top: 1.25em;
+}
+.responsive-preview-toolbar-tab:hover .trigger:after,
+.responsive-preview-toolbar-tab .trigger.active:after,
+.responsive-preview-toolbar-tab:hover .trigger.active:after {
+  color: white;
+}
+.responsive-preview-toolbar-tab.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 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..167847d
--- /dev/null
+++ b/core/modules/responsive_preview/images/responsive-preview-icons.png
@@ -0,0 +1,7 @@
+PNG
+
+   IHDR      v   ~   tEXtSoftware Adobe ImageReadyqe<  @IDATxW͎0vH>+8wO 7ʩUO=T\	v©D@gA ;'qؖh-yƓ϶n{_nKr[V3z7MH 8sBnAʠF0"
+σTA}z~3O==LȽS@O/c>c_t:5Bb<m}R+u]G<Nb;#v&iJ6`}IE`jq_R[|&	kjjףNÔZyEEoD׉?ÁOi<'2	o^o~,o,-C`KH1E3b]5LG=7  e?{e8:`9ZL6|ทBseЋ \IL}5+S+{1tJߠ& /T"˵`ƒj˔|:(a7"zL&ole\
+' +!TqĨ[	I HqWl aZMGL$ {@;!ta8*eЈ&;f9qj';!. ^p/,u'*
+L%RJ?7NLIDL!⦧JQ{*X:}J\C :?!RXX R.y7.7N2*H	Q%5(AԠU:%F4s(נ{Ki#[b}lF@H}*.
+Cnw) h+p}P    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..91d0957
--- /dev/null
+++ b/core/modules/responsive_preview/js/responsive-preview.js
@@ -0,0 +1,646 @@
+/**
+ * @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('#toolbar-tab-responsive-preview'),
+        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: 25,
+    strings: {
+      close: Drupal.t('close'),
+      orientation: Drupal.t('Change orientation')
+    }
+  }
+};
+
+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,
+    // 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': 'updateDimensions'
+  },
+
+  /**
+   * 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', 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 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);
+    // 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 dimensions of the chosen device.
+   *
+   * @param Object event
+   *   A jQuery event object.
+   */
+  updateDimensions: function (event) {
+    var $link = $(event.target);
+    // Update the device dimensions.
+    this.model.set('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', 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 $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();
+  },
+
+  /**
+   * 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">'
+      + '<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..1930a4d
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.info
@@ -0,0 +1,5 @@
+name = Responsive Preview
+description = Provides a component that previews a page in various device dimensions.
+package = Core
+version = VERSION
+core = 8.x
diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module
new file mode 100644
index 0000000..6eb095a
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.module
@@ -0,0 +1,127 @@
+<?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'),
+        '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' => 'toolbar-tab-responsive-preview',
+      'class' => array('responsive-preview-toolbar-tab'),
+    ),
+    '#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;
+}
-- 
1.7.10.4

