From 924e47c2a145d266a73fe7727488e35ee039d267 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Wed, 24 Jul 2013 12:26:53 -0400
Subject: [PATCH] Issue #1741498: Add a responsive preview toolbar tab 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      |   10 +-
 .../config/responsive_preview.device.ipad.yml      |   10 +
 .../config/responsive_preview.device.iphone4.yml   |   10 +
 .../config/responsive_preview.device.iphone5.yml   |   10 +
 .../config/responsive_preview.device.large.yml     |   10 +
 .../config/responsive_preview.device.medium.yml    |   10 +
 .../config/responsive_preview.device.nexus4.yml    |   10 +
 .../config/responsive_preview.device.nexus7.yml    |   10 +
 .../config/responsive_preview.device.small.yml     |   10 +
 .../config/schema/responsive_preview.schema.yml    |   40 +
 .../css/responsive-preview.icons.css               |  128 +++
 .../css/responsive-preview.module.css              |  117 +++
 .../css/responsive-preview.theme.css               |  232 +++++
 .../images/responsive-preview-icons.png            |   13 +
 .../responsive_preview/js/responsive-preview.js    |  969 ++++++++++++++++++++
 .../responsive_preview/DeviceAccessController.php  |   35 +
 .../responsive_preview/DeviceFormController.php    |  126 +++
 .../Drupal/responsive_preview/DeviceInterface.php  |   17 +
 .../responsive_preview/DeviceListController.php    |  138 +++
 .../responsive_preview/Form/DeviceDelete.php       |   49 +
 .../Plugin/Block/ResponsivePreviewControlBlock.php |   46 +
 .../Plugin/Core/Entity/Device.php                  |   99 ++
 .../Menu/LocalAction/AddDeviceLocalAction.php      |   24 +
 .../responsive_preview/Tests/DeviceCRUDTest.php    |   89 ++
 .../responsive_preview/responsive_preview.info.yml |    7 +
 .../responsive_preview/responsive_preview.module   |  190 ++++
 .../responsive_preview.routing.yml                 |   28 +
 27 files changed, 2436 insertions(+), 1 deletion(-)
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.ipad.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.large.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.medium.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml
 create mode 100644 core/modules/responsive_preview/config/responsive_preview.device.small.yml
 create mode 100644 core/modules/responsive_preview/config/schema/responsive_preview.schema.yml
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.icons.css
 create mode 100644 core/modules/responsive_preview/css/responsive-preview.module.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/lib/Drupal/responsive_preview/DeviceAccessController.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Menu/LocalAction/AddDeviceLocalAction.php
 create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php
 create mode 100644 core/modules/responsive_preview/responsive_preview.info.yml
 create mode 100644 core/modules/responsive_preview/responsive_preview.module
 create mode 100644 core/modules/responsive_preview/responsive_preview.routing.yml

diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index f4a37f5..6659544 100644
--- a/core/modules/contextual/contextual.toolbar.js
+++ b/core/modules/contextual/contextual.toolbar.js
@@ -32,13 +32,21 @@ function initContextualToolbar (context) {
   new contextualToolbar.VisualView(viewOptions);
   new contextualToolbar.AuralView(viewOptions);
 
-  // Update the model based on overlay events.
   $(document).on({
+    // Update the model based on overlay events.
     'drupalOverlayOpen.contextualToolbar': function () {
       model.set('overlayIsOpen', true);
     },
     'drupalOverlayClose.contextualToolbar': function () {
       model.set('overlayIsOpen', false);
+      model.set('isVisible', true);
+    },
+    // Update the model based on Responsive Preview events.
+    'drupalResponsivePreviewStarted.contextualToolbar': function () {
+      model.set('isVisible', false);
+    },
+    'drupalResponsivePreviewStopped.contextualToolbar': function () {
+      model.set('isVisible', true);
     }
   });
 
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml b/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml
new file mode 100644
index 0000000..7a1b498
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.ipad.yml
@@ -0,0 +1,10 @@
+id: ipad
+label: iPad
+dimensions:
+  width: 1536
+  height: 2048
+  dppx: 2
+orientation: portrait
+weight: 5
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml b/core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml
new file mode 100644
index 0000000..07ca7b7
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.iphone4.yml
@@ -0,0 +1,10 @@
+id: iphone4
+label: iPhone 4
+dimensions:
+  width: 640
+  height: 960
+  dppx: 2
+orientation: portrait
+weight: 4
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml b/core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml
new file mode 100644
index 0000000..d44c3a1
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.iphone5.yml
@@ -0,0 +1,10 @@
+id: iphone5
+label: iPhone 5
+dimensions:
+  width: 640
+  height: 1136
+  dppx: 2
+orientation: portrait
+weight: 3
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.large.yml b/core/modules/responsive_preview/config/responsive_preview.device.large.yml
new file mode 100644
index 0000000..97bab7f
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.large.yml
@@ -0,0 +1,10 @@
+id: large
+label: Typical desktop
+dimensions:
+  width: 1366
+  height: 768
+  dppx: 1
+orientation: landscape
+weight: 2
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.medium.yml b/core/modules/responsive_preview/config/responsive_preview.device.medium.yml
new file mode 100644
index 0000000..d4d7c00
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.medium.yml
@@ -0,0 +1,10 @@
+id: medium
+label: Tablet
+dimensions:
+  width: 800
+  height: 1280
+  dppx: 1.325
+orientation: portrait
+weight: 1
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml b/core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml
new file mode 100644
index 0000000..824742b
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.nexus4.yml
@@ -0,0 +1,10 @@
+id: nexus4
+label: Nexus 4
+dimensions:
+  width: 768
+  height: 1280
+  dppx: 2
+orientation: portrait
+weight: 6
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml b/core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml
new file mode 100644
index 0000000..d6138fa
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.nexus7.yml
@@ -0,0 +1,10 @@
+id: nexus7
+label: Nexus 7
+dimensions:
+  width: 800
+  height: 1280
+  dppx: 1.325
+orientation: portrait
+weight: 7
+status: 1
+langcode: en
diff --git a/core/modules/responsive_preview/config/responsive_preview.device.small.yml b/core/modules/responsive_preview/config/responsive_preview.device.small.yml
new file mode 100644
index 0000000..bf44935
--- /dev/null
+++ b/core/modules/responsive_preview/config/responsive_preview.device.small.yml
@@ -0,0 +1,10 @@
+id: small
+label: Smart phone
+dimensions:
+  width: 768
+  height: 1280
+  dppx: 2
+orientation: portrait
+weight: 0
+status: 0
+langcode: en
diff --git a/core/modules/responsive_preview/config/schema/responsive_preview.schema.yml b/core/modules/responsive_preview/config/schema/responsive_preview.schema.yml
new file mode 100644
index 0000000..2b1a23d
--- /dev/null
+++ b/core/modules/responsive_preview/config/schema/responsive_preview.schema.yml
@@ -0,0 +1,40 @@
+# Schema for the configuration files of the Responsive preview module.
+
+responsive_preview.device.*:
+  type: mapping
+  label: 'Responsive preview device'
+  mapping:
+    id:
+      type: string
+      label: 'Device ID'
+    uuid:
+      type: string
+      label: 'UUID'
+    label:
+      type: label
+      label: 'Device name'
+    weight:
+      type: integer
+      label: 'Device weight'
+    status:
+      type: integer
+      label: 'Show in preview list'
+    orientation:
+      type: string
+      label: 'Default orientation'
+    dimensions:
+      type: mapping
+      label: 'Dimensions'
+      mapping:
+        width:
+          type: integer
+          label: 'Width'
+        height:
+          type: integer
+          label: 'Height'
+        dppx:
+          type: float
+          label: 'Dots per pixel (dppx)'
+    langcode:
+      type: string
+      label: 'Default language'
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..1dd177a
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.icons.css
@@ -0,0 +1,128 @@
+/**
+ * @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 .toolbar-bar .toolbar-tab-responsive-preview .icon:before {
+  width: 13px;
+}
+.toolbar-tab-responsive-preview button.icon,
+.responsive-preview button.icon {
+  background-color: transparent;
+  border: 0;
+  font-size: 1em;
+}
+
+/* Toolbar icon. */
+.toolbar .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 .toolbar-bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: 1em; /* LTR */
+  height: 22px;
+  top: 0.6667em;
+}
+[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab-responsive-preview .icon-responsive-preview:before {
+  left: auto;
+  right: 6px;
+}
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.icon-active {
+  padding: 0.5em 1.3333em;
+  text-indent: 0;
+  -moz-transition: padding 0.25s;
+  -webkit-transition: padding 0.25s;
+  transition: padding 0.25s;
+}
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.icon-active.active {
+  padding-left: 2.25em; /* LTR */
+}
+[dir="rtl"] .toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.icon-active.active {
+  padding-left: 1.333em;
+  padding-right: 2.25em;
+}
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.icon-active:before {
+  background-position: -999px -999px;
+  height: 14px;
+  left: 0.667em; /* LTR */
+  top: 0.5em;
+  width: 13px;
+}
+[dir="rtl"] .toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.icon-active:before {
+  left: auto;
+  right: 0.667em;
+}
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.icon-active.active:before {
+  background-position: center -116px;
+}
+
+
+/**
+ * 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 */
+}
+[dir="rtl"] .responsive-preview .icon-close:before {
+  left: 9px;
+  right: auto;
+}
+.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 -92px; /* LTR */
+  left: 9px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation:before {
+  background-position: left -155px;
+  left: auto;
+  right: 9px;
+}
+.responsive-preview .icon-orientation:hover:before {
+  background-position: left -104px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation:hover:before {
+  background-position: left -167px;
+}
+.responsive-preview .icon-orientation.rotated:before {
+  background-position: left -68px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation.rotated:before {
+  background-position: left -131px;
+}
+.responsive-preview .icon-orientation.rotated:hover:before {
+  background-position: left -80px; /* LTR */
+}
+[dir="rtl"] .responsive-preview .icon-orientation.rotated:hover:before {
+  background-position: left -143px;
+}
diff --git a/core/modules/responsive_preview/css/responsive-preview.module.css b/core/modules/responsive_preview/css/responsive-preview.module.css
new file mode 100644
index 0000000..976c41d
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.module.css
@@ -0,0 +1,117 @@
+/**
+ * @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. */
+.toolbar .toolbar-bar .toolbar-tab-responsive-preview.toolbar-tab {
+  display: block;
+  float: right; /* LTR */
+  position: relative;
+}
+[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab-responsive-preview.toolbar-tab {
+  float: left;
+}
+.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;
+}
+.toolbar-tab-responsive-preview.toolbar-tab .options li {
+  float: none;
+  position: relative;
+}
+
+/**
+ * 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: 500;
+}
+[dir="rtl"] .responsive-preview {
+  left: auto;
+  right: -200%;
+}
+.responsive-preview.active {
+  left: 0; /* LTR */
+  position: fixed;
+}
+[dir="rtl"] .responsive-preview.active {
+  left: auto;
+  right: 0;
+}
+.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.theme.css b/core/modules/responsive_preview/css/responsive-preview.theme.css
new file mode 100644
index 0000000..e2e0137
--- /dev/null
+++ b/core/modules/responsive_preview/css/responsive-preview.theme.css
@@ -0,0 +1,232 @@
+/**
+ * @file
+ * Styling for responsive preview.
+ */
+
+/**
+ * Toolbar tab.
+ */
+.toolbar-tab-responsive-preview .options {
+  background-color: white;
+}
+/* Device preview options. */
+.toolbar-tab-responsive-preview .options {
+  box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75);
+}
+/* [dir] is needed to override Bartik's .item-list li padding */
+[dir] .toolbar-tab-responsive-preview .options li {
+  margin: 0;
+  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.toolbar-tab .options .device {
+  color: #777;
+  text-align: left;
+  width: 100%;
+}
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device:hover,
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device.active {
+  color: black;
+}
+.toolbar .toolbar-tab-responsive-preview.toolbar-tab .options .device[disabled] {
+  color: #ccc;
+  cursor: default;
+}
+/* Configuration link. */
+.toolbar-tab-responsive-preview.toolbar-tab .configure {
+  border-top: 1px solid #000;
+  color: #000;
+  margin-top: 0.5em;
+  padding-bottom: 0.5em;
+  padding-top: 0.5em;
+}
+
+/* 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
+}
+[dir="rtl"] .toolbar-tab-responsive-preview .trigger:after {
+  left: 1em;
+  right: auto;
+}
+.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;
+}
+[dir="rtl"] .toolbar-tab-responsive-preview.open:before {
+  left: 0;
+  right: auto;
+}
+.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;
+}
+[dir="rtl"] .toolbar-tab-responsive-preview.open .trigger:after {
+  left: 0.7em;
+  right: auto;
+}
+.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;
+}
+.responsive-preview .frame-container {
+  opacity: 0;
+  -moz-transition: all 450ms;
+  -webkit-transition: all 450ms;
+  transition: all 450ms;
+}
+.responsive-preview.active .frame-container {
+  opacity: 1;
+}
+.responsive-preview .modal-background {
+  background-color: black;
+  background-color: rgba(0,0,0,0.92);
+  background-image: -webkit-linear-gradient(left, rgb(20,20,20),rgb(50,50,50) 25%, rgb(100,100,100) 40%, rgb(100,100,100) 60%, rgb(50,50,50) 75%, rgb(20,20,20));
+  background-image: linear-gradient(left, rgb(20,20,20),rgb(50,50,50) 25%, rgb(100,100,100) 40%, rgb(100,100,100) 60%, rgb(50,50,50) 75%, rgb(20,20,20));
+}
+
+/**
+ * Responsive preview control placement.
+ */
+.responsive-preview .control {
+  cursor: pointer;
+  height: 40px;
+  position: absolute;
+  top: 0;
+  width: 40px;
+}
+.responsive-preview .control.close {
+  right: 0; /* LTR */
+}
+[dir="rtl"] .responsive-preview .control.close {
+  left: 0;
+  right: auto;
+}
+.responsive-preview .control.orientation {
+  left: 0; /* LTR */
+}
+[dir="rtl"] .responsive-preview .control.orientation {
+  left: auto;
+  right: 0;
+}
+.responsive-preview .device-label {
+  color: #bbbbbb;
+  font-family: sans-serif;
+  font-size: 0.85em;
+  font-weight: normal;
+  left: 30px;
+  letter-spacing: 0.25ex;
+  line-height: 2.6667;
+  margin: 0;
+  overflow: hidden;
+  position: absolute;
+  right: 40px;
+  text-overflow: ellipsis;
+  bottom: 3px;
+  white-space: nowrap;
+  width: auto;
+}
+
+/**
+ * Responsive preview frame.
+ */
+.responsive-preview .frame-container {
+  background-color: #212121;
+  border-radius: 20px;
+  box-shadow:
+    0 0 0px 1px #777,
+    1px 1px 60px 0px #000;
+  -webkit-transition: all 150ms ease-out;
+  -moz-transition: all 150ms ease-out;
+  -o-transition: all 150ms ease-out;
+  transition: all 150ms ease-out;
+  margin-top: 2em;
+}
+.responsive-preview .frame-container iframe {
+  box-shadow: 0 0 0 1px #808080;
+  -webkit-transition: all 150ms ease-out;
+  -moz-transition: all 150ms ease-out;
+  -o-transition: all 150ms ease-out;
+  transition: all 150ms ease-out;
+}
+
+/**
+ * Control block styling.
+ */
+#block-responsive-preview-controls .content .device {
+  background: none;
+  border: none;
+  color: inherit;
+  cursor: pointer;
+  font: inherit;
+  line-height: 1;
+  margin: 0;
+  padding: 0.25em 0;
+}
+#block-responsive-preview-controls .content .device[disabled] {
+  color: #ccc;
+  cursor: default;
+}
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..b30de7e
--- /dev/null
+++ b/core/modules/responsive_preview/images/responsive-preview-icons.png
@@ -0,0 +1,13 @@
+PNG
+
+   IHDR         R+y  nIDATh͘[J+A{	,a%d	YB|(DDD1,!K5h.驮=~B&Mj9;;YA)ԕ999Q\{||(DGGG@{xx(D@(D{{{@(D Pbwvv (v{{Pb (vssPb766 (v}}Pb (vuu}`TXgw
+nGVPC# SwM'ԕ f)v>JQ;4žJQ+4ž JQ34>==JQ#4><< JQl6{@i [@i k@i 2hns0[gws~~>*u ,=>1O	 ȁ (TFcR^k֩S>!3JiLVF{p"uJ{ξ>P5ϴhL+b-:u8(>M28؊.Q`2%3DT1&Ĵ0vC)11:X=TjA1&hTЯJ؉Uso/s1bU.'Js,[bZ!Ɣ:0#;{cmFW	˽FW܋]+P̕Ju%m7M]ɇԮ0O$օX*|[AR%l,{+!x(JQHr*p%RSJ1H='qBVܰU{BYCvMOd1P$Ӊukc
+U,N)VX$bAr
+IXr
+QPn g1Od屍=k/Un
+n)옼=kcP.nS
+ql^n(vWThNXXi,	ݤ+
+n*Ie;Z
+A,S
+15Qpo(Q@RU
+MR@J.Db%!hJ.$H#,cǣrǣJSئ]H.@\2QѨ),	pxQ4㑘{oΏ*    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..e4b2717
--- /dev/null
+++ b/core/modules/responsive_preview/js/responsive-preview.js
@@ -0,0 +1,969 @@
+/**
+ * @file
+ * Provides a component that previews the page in various device dimensions.
+ */
+
+(function ($, Backbone, Drupal, drupalSettings, undefined) {
+
+"use strict";
+
+var options = $.extend({
+  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')
+  }
+}, drupalSettings.responsivePreview || {});
+
+var currentPath;
+
+/**
+ * Attaches behaviors to the toolbar tab and preview containers.
+ */
+Drupal.behaviors.responsivePreview = {
+  attach: function (context) {
+    // jQuery.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');
+    // Store the current path. The drupalSettings.currentPath changes whenever
+    // an AJAX request is sent, so we save it on the first process of attach.
+    currentPath = currentPath || drupalSettings.currentPath;
+
+    if ($body.length) {
+      // 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 = Drupal.responsivePreview.models.envModel = new Drupal.responsivePreview.EnvironmentModel({
+        dir: document.getElementsByTagName('html')[0].getAttribute('dir')
+      });
+      var tabModel = Drupal.responsivePreview.models.tabModel = new Drupal.responsivePreview.TabStateModel();
+      var previewModel = Drupal.responsivePreview.models.previewModel = new Drupal.responsivePreview.PreviewStateModel();
+
+      // Manages the PreviewView.
+      Drupal.responsivePreview.views.appView = new Drupal.responsivePreview.AppView({
+        // The previewView model.
+        model: previewModel,
+        envModel: envModel,
+        // Gutter size around preview frame.
+        gutter: options.gutter,
+        // Preview device frame width.
+        bleed: options.bleed,
+        strings: options.strings
+      });
+
+      // The toolbar tab view.
+      var $tab = $(context).find('#responsive-preview-toolbar-tab');
+      if ($tab.length > 0) {
+        Drupal.responsivePreview.views.tabView = new Drupal.responsivePreview.TabView({
+          el: $tab.get(),
+          model: previewModel,
+          tabModel: tabModel,
+          envModel: envModel,
+          // Gutter size around preview frame.
+          gutter: options.gutter,
+          // Preview device frame width.
+          bleed: options.bleed
+        });
+      }
+      // The control block view.
+      var $block = $(context).find('#block-responsive-preview-controls');
+      if ($block.length > 0) {
+        Drupal.responsivePreview.views.blockView = new Drupal.responsivePreview.BlockView({
+          el: $block.get(),
+          model: previewModel,
+          envModel: envModel,
+          // Gutter size around preview frame.
+          gutter: options.gutter,
+          // Preview device frame width.
+          bleed: options.bleed
+        });
+      }
+
+      // Keyboard controls view.
+      Drupal.responsivePreview.views.keyboardView = new Drupal.responsivePreview.KeyboardView({
+        el: $block.get(),
+        model: previewModel
+      });
+
+      /**
+       * Sets the viewport width and height dimensions on the envModel.
+       */
+      var setViewportDimensions = function() {
+        envModel.set({
+          'viewportWidth': document.documentElement.clientWidth,
+          'viewportHeight': document.documentElement.clientHeight
+        });
+      };
+
+      $(window)
+        // Update the viewport width whenever it is resized, but max 4 times/s.
+        .on('resize.responsivepreview', Drupal.debounce(setViewportDimensions, 250));
+
+      $(document)
+        // Respond to viewport offsetting elements like the Toolbar.
+        .on('drupalViewportOffsetChange.responsivepreview', function (event, offsets) {
+          envModel.set('offsets', offsets);
+        })
+        .on('keyup.responsivepreview', function (event) {
+          // Close the preview if the Esc key is pressed.
+          if (event.keyCode === 27) {
+            previewModel.set('isActive', false);
+          }
+        })
+        // Close the preview if the overlay is opened.
+        .on('drupalOverlayOpen.responsivepreview', function () {
+          previewModel.set('isActive', false);
+        });
+
+      // 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.
+      setViewportDimensions();
+    }
+    // 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';
+        // Call Drupal.displace in the next process frame to relayout the page
+        // in the iframe. This will ensure that no gaps in the presentation
+        // exist from elements that are hidden, such as the toolbar.
+        var win = window;
+        window.setTimeout(function () {
+          win.Drupal.displace();
+        }, 0);
+      }
+    }
+  },
+  detach: function (context, settings, trigger) {
+    /**
+     * Loops through object properties; applies a callback function.
+     */
+    function looper (obj, iterator) {
+      for (var prop in obj) {
+        if (obj.hasOwnProperty(prop)) {
+          iterator.call(null, prop, obj[prop]);
+        }
+      }
+    }
+
+    var app = Drupal.responsivePreview.views.appView || null;
+    // Detach only if the app view is unloading.
+    if (app && context === app && trigger === 'unload') {
+      // Remove listeners on the window and document.
+      $(window).add(document).off('.responsivepreview');
+      // Remove and delete the view references.
+      looper(Drupal.responsivePreview.views, function (label, view) {
+        view.remove();
+        Drupal.responsivePreview.views[label] = undefined;
+      });
+      // Reset models, remove listeners and delete the model references.
+      looper(Drupal.responsivePreview.models, function (label, model) {
+        model.set(model.defaults);
+        model.off();
+        Drupal.responsivePreview.models[label] = undefined;
+      });
+    }
+  }
+};
+
+Drupal.responsivePreview = Drupal.responsivePreview || {
+
+  // Storage for view instances.
+  views: {},
+
+  // Storage for model instances.
+  models: {},
+
+  /**
+   * Backbone Model for the environment in which the Responsive Preview operates.
+   */
+  EnvironmentModel: Backbone.Model.extend({
+    defaults: {
+      // The viewport width, within which the preview will have to fit.
+      viewportWidth: null,
+      // The viewport height, within which the preview will have to fit.
+      viewportHeight: null,
+      // Text direction of the document, affects some positioning.
+      dir: 'ltr',
+      // Viewport offset values.
+      offsets: {
+        top: 0,
+        right: 0,
+        bottom: 0,
+        left: 0
+      }
+    }
+  }),
+
+  /**
+   * Backbone Model for the Responsive Preview toolbar tab state.
+   */
+  TabStateModel: Backbone.Model.extend({
+    defaults: {
+      // The state of toolbar list of available device previews.
+      isDeviceListOpen: false
+    }
+  }),
+
+  /**
+   * Backbone Model for the Responsive Preview preview state.
+   */
+  PreviewStateModel: Backbone.Model.extend({
+    defaults: {
+      // The state of the preview.
+      isActive: false,
+      // Indicates whether the preview iframe has been built.
+      isBuilt: false,
+      // Indicates whether the device is portrait (false) or landscape (true).
+      isRotated: 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
+      }
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.on('change:isActive', this.reset, this);
+    },
+
+    /**
+     * Puts the model back into a ready state where no device is active.
+     *
+     * @param Backbone.Model model
+     *   This model.
+     * @param Boolean isActive
+     *   Whether the responsive preview is currently active.
+     */
+    reset: function (model, isActive) {
+      // Reset the model when it is deactivated.
+      if (!isActive) {
+        // Process this model change after any views have had the chance to
+        // react to the change of isActive.
+        var that = this;
+        window.setTimeout(function () {
+          that.set({
+            isRotated: false,
+            activeDevice: null,
+            dimensions: {
+              width: null,
+              height: null,
+              dppx: null
+            }
+          }, {silent: true});
+        }, 0);
+      }
+    }
+  }),
+
+  /**
+   * Manages the PreviewView.
+   */
+  AppView: Backbone.View.extend({
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.envModel = this.options.envModel;
+      // Listen to changes on the previewModel.
+      this.model.on('change:isActive', this.render, this);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function (previewModel, isActive, options) {
+      // The preview container view.
+      if (isActive && !this.previewView) {
+        // Holds the Backbone View of the preview. This view is created and destroyed
+        // when the preview is enabled or disabled respectively.
+        this.previewView = new Drupal.responsivePreview.PreviewView({
+          el: Drupal.theme('responsivePreviewContainer'),
+          // The previewView model.
+          model: this.model,
+          envModel: this.envModel,
+          // Gutter size around preview frame.
+          gutter: this.options.gutter,
+          // Preview device frame width.
+          bleed: this.options.bleed,
+          strings: this.options.strings
+        });
+      }
+      else if (!isActive && this.previewView) {
+        this.previewView.remove();
+        delete this.previewView;
+      }
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    remove: function () {
+      // Remove the previewView if it exists.
+      if (this.previewView) {
+        this.previewView.remove();
+      }
+      // Call the parent remove method on this view.
+      Backbone.View.prototype.remove.call(this);
+    }
+  }),
+
+  /**
+   * Handles responsive preview toolbar tab interactions.
+   */
+  TabView: Backbone.View.extend({
+
+    events: {
+      'click .trigger': 'toggleDeviceList',
+      'mouseleave': 'toggleDeviceList'
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.gutter = this.options.gutter;
+      this.bleed = this.options.bleed;
+      this.tabModel = this.options.tabModel;
+      this.envModel = this.options.envModel;
+
+      // The selectDevice function is declared outside of the view because it is
+      // shared among views. It must be bound to this for the correct context
+      // to obtain.
+      this.$el.on('click.responsivepreview', '.device', $.proxy(selectDevice, this));
+
+      this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this);
+
+      this.tabModel.on('change:isDeviceListOpen', this.render, this);
+
+      this.envModel.on('change:viewportWidth', updateDeviceList, this);
+      this.envModel.on('change:viewportWidth', this.correctDeviceListEdgeCollision, this);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      var name = this.model.get('activeDevice');
+      var isActive = this.model.get('isActive');
+      var isDeviceListOpen = this.tabModel.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.
+      this.$el
+        .find('> button')
+        .toggleClass('active', isActive)
+        .attr('aria-pressed', isActive);
+
+      // Clean the active class from the device list.
+      this.$el
+        .find('.device.active')
+        .removeClass('active');
+
+      this.$el
+        .find('[data-responsive-preview-name="' + name + '"]')
+        .toggleClass('active', 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 jQuery.Event event
+     */
+    toggleDeviceList: function (event) {
+      // Force the options list closed on mouseleave.
+      if (event.type === 'mouseleave') {
+        this.tabModel.set('isDeviceListOpen', false);
+      }
+      else {
+        this.tabModel.set('isDeviceListOpen', !this.tabModel.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'
+        });
+    }
+  }),
+
+  /**
+   * Handles responsive preview control block interactions.
+   */
+  BlockView: Backbone.View.extend({
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.gutter = this.options.gutter;
+      this.bleed = this.options.bleed;
+      this.envModel = this.options.envModel;
+
+      // The selectDevice function is declared outside of the view because it is
+      // shared among views. It must be bound to this for the correct context
+      // to obtain.
+      this.$el.on('click.responsivepreview', '.device', $.proxy(selectDevice, this));
+
+      this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this);
+
+      this.envModel.on('change:viewportWidth', updateDeviceList, this);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      var name = this.model.get('activeDevice');
+      var isActive = this.model.get('isActive');
+      this.$el
+        // Render the visibility of the toolbar block.
+        .toggle(this.model.get('fittingDeviceCount') > 0)
+        .find('.device.active')
+        .removeClass('active');
+
+      this.$el
+        .find('[data-responsive-preview-name="' + name + '"]')
+        .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);
+      return this;
+    }
+  }),
+
+  /**
+   * Handles keyboard input.
+   */
+  KeyboardView: Backbone.View.extend({
+
+    /*
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      $(document).on('keyup.responsivepreview', _.bind(this.onKeypress, this));
+    },
+
+    /**
+     * Responds to esc key press events.
+     *
+     * @param jQuery.Event event
+     */
+    onKeypress: function (event) {
+      if (event.keyCode === 27) {
+        this.model.set('isActive', false);
+      }
+    },
+
+    /**
+     * Removes a listener on the document; calls the standard Backbone remove.
+     */
+    remove: function () {
+      // Unbind the keyup listener.
+      $(document).off('keyup.responsivepreview');
+      // Call the standard remove method on this.
+      Backbone.View.prototype.remove.call(this);
+    }
+  }),
+
+  /**
+   * Handles the responsive preview element interactions.
+   */
+  PreviewView: Backbone.View.extend({
+
+    events: {
+      'click #responsive-preview-close': 'onClose',
+      'click #responsive-preview-orientation': 'onRotate'
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.gutter = this.options.gutter;
+      this.bleed = this.options.bleed;
+      this.strings = this.options.strings;
+      this.envModel = this.options.envModel;
+
+      this.model.on('change:isRotated change:dimensions change:activeDevice', this.render, this);
+
+      // Recalculate the size of the preview container when the window resizes.
+      this.envModel.on('change:viewportWidth change:viewportHeight change:offsets', this.render, this);
+
+      // Build the preview.
+      this._build();
+
+      // Call an initial render.
+      this.render();
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      // Refresh the preview.
+      this._refresh();
+      Drupal.displace();
+
+      // 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', that.model.get('isActive'));
+      }, 0);
+
+      return this;
+    },
+
+    /**
+     * Closes the preview.
+     *
+     * @param jQuery.Event event
+     */
+    onClose: function (event) {
+      this.model.set('isActive', false);
+    },
+
+    /**
+     * Responds to rotation button presses.
+     *
+     * @param jQuery.Event event
+     */
+    onRotate: function (event) {
+      this.model.set('isRotated', !this.model.get('isRotated'));
+    },
+
+    /**
+     * Builds the preview iframe.
+     */
+    _build: function () {
+      var offsets = this.envModel.get('offsets');
+      var $frameContainer = $(Drupal.theme('responsivePreviewFrameContainer', this.strings))
+        // 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.
+      $(Drupal.theme('responsivePreviewFrame'))
+        .attr({
+          'data-loading': true,
+          src: drupalSettings.basePath + Drupal.encodePath(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);
+      // Insert the container into the DOM.
+      this.$el
+        .css({
+          'top': offsets.top,
+          'right': offsets.right,
+          'left': offsets.left
+        })
+        // 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 = $('[data-responsive-preview-name="' + this.model.get('activeDevice') + '"]').eq(0);
+      var $container = this.$el.find('#responsive-preview-frame-container');
+      var $frame = $container.find('> iframe');
+      var offsets = this.envModel.get('offsets');
+
+      // Get the static state.
+      var edge = (this.envModel.get('dir') === 'rtl') ? 'right' : 'left';
+      var minGutter = this.gutter;
+
+      // Get current (dynamic) state.
+      var dimensions = this.model.get('dimensions');
+      var viewportWidth = this.envModel.get('viewportWidth') - (offsets.left + offsets.right);
+      var viewportHeight = this.envModel.get('viewportHeight') - (offsets.top + offsets.bottom);
+
+      // Calculate preview width & height. If the preview is rotated, swap width
+      // and height.
+      var displayWidth = dimensions[(isRotated) ? 'height' : 'width'];
+      var displayHeight = dimensions[(isRotated) ? 'width' : 'height'];
+      var width = displayWidth / dimensions.dppx;
+      var height = displayHeight / dimensions.dppx;
+
+      // Get the container padding and border width for both dimensions.
+      var bleed = this.bleed;
+      var widthSpread = width + (bleed * 2);
+      var heightSpread = height + (bleed * 2);
+
+      // Calculate how much space is required to the right and left of the
+      // preview container in order to center it.
+      var gutterPercent = (1 - (widthSpread / viewportWidth)) / 2;
+      var gutter = gutterPercent * viewportWidth;
+      gutter = (gutter < minGutter) ? minGutter : gutter;
+
+      // The device dimension size plus gutters must fit within the viewport
+      // area for that dimension. The spread is how much room the preview
+      // needs for that dimension.
+      width = Math.ceil((viewportWidth - (gutter * 2) < widthSpread) ? viewportWidth - (gutter * 2) - (bleed * 2) : width);
+      // Use one gutter unit instead of two so that the preview element will
+      // appear closer to the top/bottom screen elements than it does to the
+      // left/right screen elements.
+      height = Math.ceil((viewportHeight - this.gutter < heightSpread) ? viewportHeight - this.gutter - (bleed * 2) : height);
+
+      // Updated the state of the rotated icon.
+      this.$el.find('.control.orientation').toggleClass('rotated', isRotated);
+
+      // Resize & reposition the iframe.
+      this.$el.css({
+        'top': offsets.top,
+        'right': offsets.right,
+        'left': offsets.left
+      });
+      var position = {};
+      position[edge] = (gutter > minGutter) ? gutter : minGutter; // 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, @displayWidth@width by @displayHeight@height, @dpi, @orientation', {
+        '@label': $deviceLink.text(),
+        '@displayWidth': displayWidth + 'px',
+        // If the width of the preview element is not equivalent to the
+        // configured display width, display the actual width of the preview
+        // in parentheses.
+        '@width': (displayWidth !== Math.floor(width * dimensions.dppx)) ? ' (' + (Math.floor(width * dimensions.dppx)) + 'px)' : '',
+        '@displayHeight': displayHeight + 'px',
+        // If the height of the preview element is not equivalent to the
+        // configured display height, display the actual height of the preview
+        // in parentheses.
+        '@height': (displayHeight !== Math.floor(height * dimensions.dppx)) ? ' (' + (Math.floor(height * dimensions.dppx)) + 'px)' : '',
+        '@dpi': dimensions.dppx + 'ppx',
+        '@orientation': (isRotated) ? this.strings.landscape : this.strings.portrait
+      }));
+
+      // Update the positioning of the modal background.
+      this.$el.find('.modal-background').css(offsets);
+    },
+
+    /**
+     * 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 isRotated = this.model.get('isRotated');
+      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, 1) range).
+      var dimensions = this.model.get('dimensions');
+      // If the preview is rotated, width and height are swapped.
+      visualViewPortWidth = dimensions[(isRotated) ? 'height' : 'width'] / dimensions.dppx;
+      var scale = initialScale * (100 / layoutViewportWidth) * (visualViewPortWidth / 100);
+      scale = Math.min(scale, 1);
+      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;
+    }
+  })
+};
+
+/**
+ * Functions that are common to both the TabView and BlockView.
+ */
+
+/**
+ * Model change handler; hides devices that don't fit the current viewport.
+ */
+function updateDeviceList () {
+  var gutter = this.gutter;
+  var bleed = this.bleed;
+  var viewportWidth = this.envModel.get('viewportWidth');
+  var $devices = this.$el.find('.device');
+  var fittingDeviceCount = $devices.length;
+
+  // 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);
+    if (!fits) {
+      fittingDeviceCount--;
+    }
+    // Set the button to disabled if the device doesn't fit in the current
+    // viewport.
+    // Toggle between the prop() and removeProp() methods.
+    $this.prop('disabled', !fits)
+      .attr('aria-disabled', !fits);
+  });
+  // Set the number of devices that fit the current viewport.
+  this.model.set('fittingDeviceCount', fittingDeviceCount);
+}
+
+/**
+ * Updates the model to reflect the properties of the chosen device.
+ *
+ * @param jQuery.Event event
+ */
+function selectDevice (event) {
+  var $link = $(event.target);
+  var name = $link.data('responsive-preview-name');
+  // If the clicked link is already active, then shut down the preview.
+  if (this.model.get('activeDevice') === name) {
+    this.model.set('isActive', false);
+    return;
+  }
+  // Update the device dimensions.
+  this.model.set({
+    'activeDevice': name,
+    '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();
+}
+
+/**
+ * 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.
+   *
+   * @param Object strings
+   *   A hash of strings to use in the template.
+   * @return
+   *   The corresponding HTML.
+   */
+  responsivePreviewFrameContainer: function (strings) {
+    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" title="' + strings.close + '" role="button" class="icon icon-close control close" aria-pressed="false"><span class="visually-hidden">' + strings.close + '</span></button>' +
+      '<button id="responsive-preview-orientation" title="' + strings.orientation + '" role="button" class="icon icon-orientation control orientation" aria-pressed="false"><span class="visually-hidden">' + strings.orientation + '</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));
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceAccessController.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceAccessController.php
new file mode 100644
index 0000000..eacbc49
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceAccessController.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\CommentAccessController
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Access controller for the Device entity.
+ *
+ * @see \Drupal\responsive_preview\Plugin\Core\Entity\Device.
+ */
+class DeviceAccessController extends EntityAccessController {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    switch ($operation) {
+      case 'update':
+        return user_access('administer site configuration', $account);
+        break;
+      case 'delete':
+        return user_access('administer site configuration', $account);
+        break;
+    }
+  }
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php
new file mode 100644
index 0000000..58baf38
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\DeviceFormController.
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Form controller for the device entity edit forms.
+ */
+class DeviceFormController extends EntityFormController {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, array &$form_state, EntityInterface $responsive_preview_device = NULL) {
+    switch ($form_state['controller']->getOperation()) {
+      case 'add':
+        drupal_set_title(t('Add device'));
+        break;
+      case 'edit':
+        drupal_set_title(t('Edit device'));
+        break;
+      default:
+        break;
+    }
+    $entity = $this->entity;
+    $form['label'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Device name'),
+      '#default_value' => $entity->label(),
+      '#size' => 30,
+      '#required' => TRUE,
+      '#maxlength' => 64,
+    );
+    $form['id'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $entity->id(),
+      '#required' => TRUE,
+      '#disabled' => !$entity->isNew(),
+      '#size' => 30,
+      '#maxlength' => 64,
+      '#machine_name' => array(
+        'exists' => 'responsive_preview_device_load',
+      ),
+    );
+    $dimensions = $entity->get('dimensions');
+    $form['dimensions'] = array(
+      '#type' => 'container',
+      '#tree' => TRUE,
+    );
+    $form['dimensions']['width'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Width'),
+      '#default_value' => $dimensions['width'],
+      '#field_suffix' => 'px',
+      '#size' => 6,
+      '#required' => TRUE,
+    );
+    $form['dimensions']['height'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Height'),
+      '#default_value' => $dimensions['height'],
+      '#field_suffix' => 'px',
+      '#size' => 6,
+      '#required' => TRUE,
+    );
+    $form['dimensions']['dppx'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Dots per pixel (dppx)'),
+      '#description' => t('Size of a single dot in graphical representation. Classic desktop displays have 1dppx, typical modern smartphones and laptops have 2dppx or higher. For example Google Nexus 4 and iPhone 5 has 2dppx, while Google Nexus 7 has 1.325dppx and Samsung Galaxy S4 has 3dppx.'),
+      '#default_value' => $dimensions['dppx'],
+      '#size' => 4,
+      '#required' => TRUE,
+    );
+    $form['orientation'] = array(
+      '#type' => 'select',
+      '#title' => t('Default orientation'),
+      '#default_value' => $entity->get('orientation'),
+      '#options' => array('portrait' => t('Portrait'), 'landscape' => t('Landscape')),
+    );
+    $form['status'] = array(
+      '#type' => 'value',
+      '#value' => $entity->get('status'),
+    );
+    $form['weight'] = array(
+      '#type' => 'value',
+      '#value' => $entity->get('weight'),
+    );
+
+    return parent::form($form, $form_state, $entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, array &$form_state) {
+    $entity = $this->entity;
+
+    // Prevent leading and trailing spaces in device names.
+    $entity->set('label', trim($entity->label()));
+    $uri = $entity->uri();
+    if ($entity->save() == SAVED_UPDATED) {
+      drupal_set_message(t('Device %label has been updated.', array('%label' => $entity->label())));
+      watchdog('responsive_preview', 'Device %label has been updated.', array('%label' => $entity->label()), WATCHDOG_NOTICE, l(t('Edit'), $uri['path']));
+    }
+    else {
+      drupal_set_message(t('Device %label has been added.', array('%label' => $entity->label())));
+      watchdog('responsive_preview', 'Device %label has been added.', array('%label' => $entity->label()), WATCHDOG_NOTICE, l(t('Edit'), $uri['path']));
+    }
+    $form_state['redirect'] = 'admin/config/content/responsive-preview';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(array $form, array &$form_state) {
+    $form_state['redirect'] = 'admin/config/content/responsive-preview/manage/' . $this->entity->id() . '/delete';
+  }
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php
new file mode 100644
index 0000000..7b01572
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\DeviceInterface.
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a device entity.
+ */
+interface DeviceInterface extends ConfigEntityInterface {
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php
new file mode 100644
index 0000000..5da4bac
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceListController.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\DeviceListController.
+ */
+
+namespace Drupal\responsive_preview;
+
+use Drupal\Core\Config\Entity\ConfigEntityListController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormInterface;
+
+/**
+ * Provides a listing of responsive preview devices.
+ */
+class DeviceListController extends ConfigEntityListController implements FormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'responsive_preview_admin_devices_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $row = parent::buildHeader();
+    unset($row['operations']);
+    $row['label'] = t('Name');
+    $row['status'] = t('Show in list');
+    $row['dimensions'] = t('Dimensions');
+    unset($row['id']);
+    $row['weight'] = t('Weight');
+    $row['operations'] = t('Operations');
+    return $row;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row = parent::buildRow($entity);
+    $operations = $row['operations'];
+    unset($row['operations']);
+
+    // Override default values to markup elements.
+    $row['#attributes']['class'][] = 'draggable';
+    unset($row['id']);
+
+    $row['label'] = array(
+      '#markup' => check_plain($entity->label()),
+    );
+    $row['status'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Show %title in list', array('%title' => $entity->label())),
+      '#title_display' => 'invisible',
+      '#default_value' => $entity->get('status'),
+    );
+    $dimensions = $entity->get('dimensions');
+    $row['dimensions'] = array(
+      '#markup' => check_plain($dimensions['width'] . 'x' . $dimensions['height'] . ' (' . $dimensions['dppx'] . ' dppx)'),
+    );
+    $row['#weight'] = $entity->get('weight');
+    // Add weight column.
+    $row['weight'] = array(
+      '#type' => 'weight',
+      '#title' => t('Weight for @title', array('@title' => $entity->label())),
+      '#title_display' => 'invisible',
+      '#default_value' => $entity->get('weight'),
+      '#attributes' => array('class' => array('weight')),
+    );
+    $row['operations'] = $operations;
+    return $row;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return drupal_get_form($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $form['entities'] = array(
+      '#type' => 'table',
+      '#header' => $this->buildHeader(),
+      '#empty' => t('There is no @label yet.', array('@label' => $this->entityInfo['label'])),
+      '#tabledrag' => array(
+        array('order', 'sibling', 'weight'),
+      ),
+    );
+
+    foreach ($this->load() as $entity) {
+      $form['entities'][$entity->id()] = $this->buildRow($entity);
+    }
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+      '#button_type' => 'primary',
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+    // No validation.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $values = $form_state['values']['entities'];
+
+    $entities = entity_load_multiple($this->entityType, array_keys($values));
+    foreach ($values as $id => $value) {
+      if (isset($entities[$id]) && (($value['weight'] != $entities[$id]->get('weight')) || ($value['status'] != $entities[$id]->get('status')))) {
+        // Update changed weight.
+        $entities[$id]->set('weight', $value['weight']);
+        $entities[$id]->set('status', $value['status']);
+        $entities[$id]->save();
+      }
+    }
+
+    drupal_set_message(t('The device settings have been updated.'));
+  }
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php
new file mode 100644
index 0000000..41cce61
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Form\DeviceDelete.
+ */
+
+namespace Drupal\responsive_preview\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\responsive_preview\DeviceInterface;
+
+/**
+ * Provides a deletion confirmation form for a device entity.
+ */
+class DeviceDelete extends EntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to delete the device %name?', array('%name' => $this->entity->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelPath() {
+    return 'admin/config/content/responsive-preview';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submit(array $form, array &$form_state) {
+    $this->entity->delete();
+    watchdog('responsive_preview', 'Device %name has been deleted.', array('%name' => $this->entity->label()));
+    drupal_set_message(t('Device %name has been deleted.', array('%name' => $this->entity->label())));
+    $form_state['redirect'] = 'admin/config/content/responsive-preview';
+  }
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php
new file mode 100644
index 0000000..4b46cb1
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Block/ResponsivePreviewControlBlock.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Plugin\Block\ResponsivePreviewControlBlock.
+ */
+
+namespace Drupal\responsive_preview\Plugin\Block;
+
+use Drupal\block\BlockBase;
+use Drupal\Component\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Provides a 'Responsive preview controls' block.
+ *
+ * @Plugin(
+ *   id = "responsive-preview-controls",
+ *   admin_label = @Translation("Responsive preview controls"),
+ *   module = "responsive_preview"
+ * )
+ */
+class ResponsivePreviewControlBlock extends BlockBase {
+
+  /**
+   * Implements \Drupal\block\BlockBase::blockBuild().
+   */
+  public function build() {
+    $block = array(
+      'device_options' => array(
+        '#theme' => 'item_list',
+        '#items' => responsive_preview_get_devices_list(),
+        '#attributes' => array(
+          'class' => array('options'),
+        ),
+        '#attached' => array(
+          'library' => array(
+            array('responsive_preview', 'responsive-preview'),
+          ),
+        ),
+      ),
+    );
+
+    return $block;
+  }
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php
new file mode 100644
index 0000000..7e20a51
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Core/Entity/Device.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Plugin\Core\Entity\Device.
+ */
+
+namespace Drupal\responsive_preview\Plugin\Core\Entity;
+
+use Drupal\Core\Entity\Annotation\EntityType;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+
+/**
+ * Defines the responsive preview device entity class.
+ *
+ * @EntityType(
+ *   id = "responsive_preview_device",
+ *   label = @Translation("Responsive preview device"),
+ *   module = "responsive_preview",
+ *   controllers = {
+ *     "access" = "Drupal\responsive_preview\DeviceAccessController",
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
+ *     "list" = "Drupal\responsive_preview\DeviceListController",
+ *     "form" = {
+ *       "edit" = "Drupal\responsive_preview\DeviceFormController",
+ *       "add" = "Drupal\responsive_preview\DeviceFormController",
+ *       "delete" = "Drupal\responsive_preview\Form\DeviceDelete"
+ *     }
+ *   },
+ *   config_prefix = "responsive_preview.device",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "label" = "label"
+ *   }
+ * )
+ */
+class Device extends ConfigEntityBase {
+
+  /**
+   * The machine name of this device.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The UUID of this device.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The human-readable label of this device.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * The weight of this device in listings.
+   *
+   * @var int
+   */
+  public $weight;
+
+  /**
+   * Default orientation.
+   *
+   * @var string
+   *   Either 'landscape' or 'portrait'.
+   */
+  public $orientation;
+
+  /**
+   * Dimension information.
+   *
+   * @var array
+   *   Associative array with keys 'weight' (int), 'height' (int)
+   *   and 'dppx' (int).
+   */
+  public $dimensions;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uri() {
+    return array(
+      'path' => 'admin/config/content/responsive-preview/manage/' . $this->id(),
+      'options' => array(
+        'entity_type' => $this->entityType,
+        'entity' => $this,
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Menu/LocalAction/AddDeviceLocalAction.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Menu/LocalAction/AddDeviceLocalAction.php
new file mode 100644
index 0000000..d40e4b3
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/Menu/LocalAction/AddDeviceLocalAction.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\responsive_preview\Plugin\Menu\LocalAction\AddDeviceLocalAction.
+ */
+
+namespace Drupal\responsive_preview\Plugin\Menu\LocalAction;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Menu\LocalActionBase;
+use Drupal\Core\Annotation\Menu\LocalAction;
+
+/**
+ * @LocalAction(
+ *   id = "device_add_local_action",
+ *   route_name = "responsive_preview_device_add",
+ *   title = @Translation("Add device"),
+ *   appears_on = {"responsive_preview_device_list"}
+ * )
+ */
+class AddDeviceLocalAction extends LocalActionBase {
+
+}
diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php
new file mode 100644
index 0000000..ce89a12
--- /dev/null
+++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\responsive_preview\Tests\DeviceCRUDTest.
+ */
+
+namespace Drupal\responsive_preview\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the device listing.
+ */
+class DeviceCRUDTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('toolbar', 'responsive_preview');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Responsive preview',
+      'description' => 'Tests device management functionality.',
+      'group' => 'Responsive preview',
+    );
+  }
+
+  /**
+   * Tests configuring devices.
+   */
+  function testDeviceConfiguration() {
+    // Create and login administrative user.
+    $admin_user = $this->drupalCreateUser(array(
+      'administer site configuration',
+      'access toolbar',
+    ));
+    $this->drupalLogin($admin_user);
+
+    $this->drupalGet('admin/config/content/responsive-preview');
+
+    // Some default devices exist.
+    $this->assertLinkByHref('admin/config/content/responsive-preview/manage/large/delete');
+    $this->assertLinkByHref('admin/config/content/responsive-preview/manage/ipad/delete');
+
+    // Some devices are shown by default.
+    $this->drupalGet('');
+    $this->checkDevices(array('iphone5', 'ipad', 'nexus4', 'nexus7'));
+
+    // Delete one of the predefined devices.
+    $this->drupalPost('admin/config/content/responsive-preview/manage/iphone5/delete', array(), t('Delete'));
+    $this->assertRaw(t('Device %name has been deleted.', array('%name' => 'iPhone 5')));
+
+    // Make generic tablet appear in the list.
+    $this->drupalPost('admin/config/content/responsive-preview', array('entities[medium][status]' => 1), t('Save'));
+    $this->assertRaw(t('The device settings have been updated.'));
+
+    // Add a new device as well.
+    $edit = array(
+      'label' => 'Smartwatch',
+      'id' => 'smartwatch',
+      'dimensions[width]' => '200',
+      'dimensions[height]' => '350',
+      'dimensions[dppx]' => '3',
+    );
+    $this->drupalPost('admin/config/content/responsive-preview/add', $edit, t('Save'));
+    $this->assertRaw(t('Device %name has been added.', array('%name' => 'Smartwatch')));
+
+    // Check updated device list. New devices are shown in the list by default.
+    $this->drupalGet('');
+    $this->checkDevices(array('smartwatch', 'ipad', 'medium', 'nexus4', 'nexus7'));
+  }
+
+  /**
+   * Tests exposed devices in the responsive preview list.
+   */
+  private function checkDevices(array $devices) {
+    foreach ($devices as $name) {
+      $device_button = $this->xpath('//button[@data-responsive-preview-name=:name]', array(
+        ':name' => $name
+      ));
+      $this->assertTrue(!empty($device_button), format_string('%name device shown by default', array('%name' => $name)));
+    }
+  }
+
+}
diff --git a/core/modules/responsive_preview/responsive_preview.info.yml b/core/modules/responsive_preview/responsive_preview.info.yml
new file mode 100644
index 0000000..5f9292e
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.info.yml
@@ -0,0 +1,7 @@
+name: 'Responsive Preview'
+type: module
+description: 'Provides a component that previews a page in various device dimensions.'
+package: Core
+version: VERSION
+core: 8.x
+configure: admin/config/content/responsive-preview
diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module
new file mode 100644
index 0000000..227b4ed
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.module
@@ -0,0 +1,190 @@
+<?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;
+    case 'admin/config/content/responsive-preview':
+      $output = '<p>' . t('Configure the set and order of available devices on this page for responsive site preview. The list of devices is shown in a dropdown accessible from the toolbar tab with a small device icon.') . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function responsive_preview_menu() {
+  $items['admin/config/content/responsive-preview'] = array(
+    'title' => 'Responsive preview',
+    'description' => 'Configure device listings for content preview.',
+    'route_name' => 'responsive_preview_device_list',
+  );
+  $items['admin/config/content/responsive-preview/add'] = array(
+    'route_name' => 'responsive_preview_device_add',
+    'type' => MENU_SIBLING_LOCAL_TASK,
+  );
+  $items['admin/config/content/responsive-preview/manage/%responsive_preview_device'] = array(
+    'title' => 'Edit device',
+    'route_name' => 'responsive_preview_device_edit',
+  );
+  $items['admin/config/content/responsive-preview/manage/%responsive_preview_device/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/config/content/responsive-preview/manage/%responsive_preview_device/delete'] = array(
+    'title' => 'Delete',
+    'route_name' => 'responsive_preview_device_delete',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE,
+    'weight' => 10,
+  );
+  return $items;
+}
+/**
+ * Returns a list of devices and their properties from configuration.
+ */
+function responsive_preview_get_devices_list() {
+  $devices = entity_load_multiple('responsive_preview_device');
+  uasort($devices, array('Drupal\responsive_preview\Plugin\Core\Entity\Device', 'sort'));
+
+  $links = array();
+  foreach ($devices as $device) {
+    if ($device->status) {
+      $dimensions = $device->get('dimensions');
+      $links[$device->id()] = array(
+        '#type' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => $device->label(),
+        '#attributes' => array(
+          'class' => array('device', 'icon', 'icon-active'),
+          'data-responsive-preview-name' => $device->id(),
+          'data-responsive-preview-width' => (!empty($dimensions['width'])) ? $dimensions['width'] : '',
+          'data-responsive-preview-height' => (!empty($dimensions['height'])) ? $dimensions['height'] : '',
+          'data-responsive-preview-dppx' => (!empty($dimensions['dppx'])) ? $dimensions['dppx'] : '1',
+        ),
+      );
+    }
+  }
+  // Add a configuration link.
+  $links['configure_link'] = array(
+    '#type' => 'link',
+    '#title' => t('Configure devices'),
+    '#href' => url('admin/config/content/responsive-preview'),
+    '#options' => array(
+      'attributes' => array(
+        'class' => array('configure'),
+      ),
+    ),
+  );
+
+  return $links;
+}
+
+/**
+ * Fetches a responsive preview device by ID.
+ *
+ * @param string $id
+ *   A string representing the device ID (machine name).
+ *
+ * @return
+ *   A fully-loaded device object if a device with the given ID exists,
+ *   or FALSE otherwise.
+ */
+function responsive_preview_device_load($id) {
+  return entity_load('responsive_preview_device', $id);
+}
+
+/**
+ * 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(
+        '#type' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => t('Layout preview'),
+        '#value_prefix' => '<span class="visually-hidden">',
+        '#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.module.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;
+}
diff --git a/core/modules/responsive_preview/responsive_preview.routing.yml b/core/modules/responsive_preview/responsive_preview.routing.yml
new file mode 100644
index 0000000..dcf7e63
--- /dev/null
+++ b/core/modules/responsive_preview/responsive_preview.routing.yml
@@ -0,0 +1,28 @@
+responsive_preview_device_list:
+  pattern: '/admin/config/content/responsive-preview'
+  defaults:
+    _content: '\Drupal\Core\Entity\Controller\EntityListController::listing'
+    entity_type: 'responsive_preview_device'
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_add:
+  pattern: '/admin/config/content/responsive-preview/add'
+  defaults:
+    _entity_form: responsive_preview_device.add
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_edit:
+  pattern: '/admin/config/content/responsive-preview/manage/{responsive_preview_device}'
+  defaults:
+    _entity_form: responsive_preview_device.edit
+  requirements:
+    _permission: 'administer site configuration'
+
+responsive_preview_device_delete:
+  pattern: '/admin/config/content/responsive-preview/manage/{responsive_preview_device}/delete'
+  defaults:
+    _entity_form: responsive_preview_device.delete
+  requirements:
+    _permission: 'administer site configuration'
-- 
1.7.10.4

