From b449948e0eec6637378a746d9100db93d859434d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Thu, 1 Aug 2013 14:40:14 -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. Renée Beach --- 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 | 258 +++++ .../images/responsive-preview-icons.png | 13 + .../responsive_preview/js/responsive-preview.js | 1010 ++++++++++++++++++++ .../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, 2503 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..96b94a8 --- /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 .responsive-preview-icon:before, +.responsive-preview .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 .responsive-preview-icon:before { + width: 13px; +} +.toolbar-tab-responsive-preview button.responsive-preview-icon, +.responsive-preview button.responsive-preview-icon { + background-color: transparent; + border: 0; + font-size: 1em; +} + +/* Toolbar icon. */ +.toolbar .toolbar-bar .responsive-preview-icon.responsive-preview-icon-responsive-preview { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + width: 5em; +} +.toolbar-tab-responsive-preview .responsive-preview-icon.responsive-preview-icon-responsive-preview:before { + background-position: center top; +} +.toolbar-tab-responsive-preview.open .responsive-preview-icon-responsive-preview:before, +.toolbar-tab-responsive-preview .responsive-preview-icon-responsive-preview.active:before, +.toolbar-tab-responsive-preview .responsive-preview-icon-responsive-preview:hover:before { + background-position: center -22px; +} +.toolbar .toolbar-bar .toolbar-tab-responsive-preview .responsive-preview-icon-responsive-preview:before { + left: 1em; /* LTR */ + height: 22px; + top: 0.6667em; +} +[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab-responsive-preview .responsive-preview-icon-responsive-preview:before { + left: auto; + right: 6px; +} +.toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device.responsive-preview-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 .responsive-preview-options .responsive-preview-device.responsive-preview-icon-active.active { + padding-left: 2.25em; /* LTR */ +} +[dir="rtl"] .toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device.responsive-preview-icon-active.active { + padding-left: 1.333em; + padding-right: 2.25em; +} +.toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device.responsive-preview-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 .responsive-preview-options .responsive-preview-device.responsive-preview-icon-active:before { + left: auto; + right: 0.667em; +} +.toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device.responsive-preview-icon-active.active:before { + background-position: center -116px; +} + + +/** + * Responsive preview controls icons. + */ +.responsive-preview-control.responsive-preview-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..6dd4dcc --- /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 .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 .responsive-preview-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 .responsive-preview-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..00c147c --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.theme.css @@ -0,0 +1,258 @@ +/** + * @file + * Styling for responsive preview. + */ + +/** + * Toolbar tab. + */ +.toolbar-tab-responsive-preview .responsive-preview-options { + background-color: white; + padding-top: 0.5em; + padding-bottom: 0.5em; +} +/* Device preview options. */ +.toolbar-tab-responsive-preview .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 .responsive-preview-options li { + margin: 0; + padding: 0; +} +.toolbar-tab-responsive-preview .responsive-preview-trigger { + cursor: pointer; + line-height: 1; + height: 3em; +} +.toolbar-tab-responsive-preview .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 .responsive-preview-trigger.active, +.toolbar-tab-responsive-preview .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 .responsive-preview-trigger, +.toolbar-tab-responsive-preview .responsive-preview-options .responsive-preview-device { + padding-bottom: 1em; + padding-top: 1em; +} +.toolbar-tab-responsive-preview .responsive-preview-options .responsive-preview-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 .responsive-preview-options .responsive-preview-device { + color: #0074BD; + text-align: left; + width: 100%; +} +.toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device:hover, +.toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device.active { + color: black; +} +.toolbar .toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-options .responsive-preview-device[disabled] { + color: #ccc; + cursor: default; +} +/* Configuration link. */ +.toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-configure { + color: #777; + margin-top: 0.5em; + padding-bottom: 0.5em; + padding-top: 0.5em; +} +.toolbar-tab-responsive-preview.toolbar-tab .responsive-preview-configure:hover { + color: #000; +} + +/* Toolbar tab triangle toggle. */ +.toolbar-tab-responsive-preview .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 .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 .responsive-preview-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 .responsive-preview-trigger:after { + left: 0.7em; + right: auto; +} +.toolbar-tab-responsive-preview:hover .responsive-preview-trigger:after, +.toolbar-tab-responsive-preview .responsive-preview-trigger.active:after, +.toolbar-tab-responsive-preview:hover .responsive-preview-trigger.active:after { + color: white; +} +.toolbar-tab-responsive-preview.open:hover .responsive-preview-trigger:after { + color: black; +} + +/** + * Preview container. + */ +.responsive-preview { + opacity: 1; + -moz-transition: opacity 450ms; + -webkit-transition: opacity 450ms; + transition: opacity 450ms; +} +.responsive-preview .responsive-preview-frame-container { + opacity: 0; + -moz-transition: all 250ms; + -webkit-transition: all 250ms; + transition: all 250ms; +} +.responsive-preview.active .responsive-preview-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.responsive-preview-close { + right: 0; /* LTR */ +} +[dir="rtl"] .responsive-preview-control.responsive-preview-close { + left: 0; + right: auto; +} +.responsive-preview-control.responsive-preview-orientation { + left: 0; /* LTR */ +} +[dir="rtl"] .responsive-preview-control.responsive-preview-orientation { + left: auto; + right: 0; +} +.responsive-preview-device-label { + bottom: 0; + color: #bbbbbb; + font-family: sans-serif; + font-weight: normal; + left: 30px; + line-height: 2.6667; + margin: 0; + overflow: hidden; + position: absolute; + right: 30px; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} +.responsive-preview-device-details-trigger { + color: #666; + cursor: pointer; +} +.responsive-preview-device-details-trigger:hover { + color: #bbb; +} +.responsive-preview-device-label-details, +.responsive-preview-device-details-comma { + display: none; +} +.responsive-preview-expanded.responsive-preview-device-label { + cursor: pointer; +} +.responsive-preview-expanded .responsive-preview-device-label-details, +.responsive-preview-expanded .responsive-preview-device-details-comma { + display: inline; +} +.responsive-preview-expanded .responsive-preview-device-details-trigger { + display: none; +} + +/** + * 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 .responsive-preview-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 .responsive-preview-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+ynIDATh͘[J+A{ ,a%d YB|(DDD1,!K 5h.驮=~B&Mj9;;YA )ԕ999Q\{|| ( DGGG@{xx( D@( D{{{@ ( DPbwvv(v{{Pb(vssPb766(v}}Pb(vuu}`TXgw +nGVPC#SwM'ԕf)v>JQ;4žJQ+4žJQ34>==JQ#4><<JQl6{@i [@i k@i 2hns0[gws~~>*u ,=>1O ȁ(TFcR^k֩S>!3JiLVF{p"uJ{ξ>P5ϴhL+b-:u8(>M28؊.Q`2%3DT1&Ĵ0vC)11: X=TjA1&hTЯJ؉Uso/s1bU.'Js,[bZ!Ɣ:0#;{cmFW ˽FW܋]+P̕Ju%m7M]ɇԮ0O$օX*|[AR%l,{+!x(JQHr*p%RSJ1H='qBVܰU{BYCvMO d1P $Ӊuk c +U,N)VX$bAr +IXr +QPn g1Od屍=k/Un +n)옼=kcP.nS +ql^n(vWThNXXi, ݤ+ +n*Ie;Z +A,S +15Qpo(Q@RU +MR @J.Db %!hJ.$H#,cǣrǣJSئ]H.@\2QѨ ), pxQ4㑘{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..9c1c55f --- /dev/null +++ b/core/modules/responsive_preview/js/responsive-preview.js @@ -0,0 +1,1010 @@ +/** + * @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: { + details: Drupal.t('details'), + 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, + // Indicates of the device details are visible in the preview frame. + isDetailsExpanded: 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 && !Drupal.responsivePreview.views.previewView) { + // Holds the Backbone View of the preview. This view is created and destroyed + // when the preview is enabled or disabled respectively. + Drupal.responsivePreview.views.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 + }); + // Remove the inlined opacity style so that the CSS opacity transition + // will fade in the preview view. + window.setTimeout(function () { + Drupal.responsivePreview.views.previewView.el.style.opacity = null; + }, 0); + } + else if (!isActive && Drupal.responsivePreview.views.previewView) { + // The transitionEnd event is still heavily vendor-prefixed. + var transitionEnd = "transitionEnd.responsivepreview webkitTransitionEnd.responsivepreview transitionend.responsivepreview msTransitionEnd.responsivepreview oTransitionEnd.responsivepreview"; + // When the fade transition is complete, remove the view. + Drupal.responsivePreview.views.previewView.$el.on(transitionEnd, function (event) { + Drupal.responsivePreview.views.previewView.remove(); + delete Drupal.responsivePreview.views.previewView; + }); + // Fade out the preview. + Drupal.responsivePreview.views.previewView.el.style.opacity = 0; + } + } + }), + + /** + * Handles responsive preview toolbar tab interactions. + */ + TabView: Backbone.View.extend({ + + events: { + 'click .responsive-preview-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', '.responsive-preview-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('.responsive-preview-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', '.responsive-preview-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('.responsive-preview-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': 'shutdown', + 'click #responsive-preview-modal-background': 'shutdown', + 'click #responsive-preview-orientation': 'rotate', + 'click #responsive-preview-frame-label': 'revealDetails' + }, + + /** + * {@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:isDetailsExpanded 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 + */ + shutdown: function (event) { + this.model.set('isActive', false); + }, + + /** + * Removes a listener on the document; calls the standard Backbone remove. + */ + remove: function () { + // Unbind transition listeners. + this.$el.off('.responsivepreview'); + // Call the standard remove method on this. + Backbone.View.prototype.remove.call(this); + }, + + /** + * Responds to rotation button presses. + * + * @param jQuery.Event event + */ + rotate: function (event) { + this.model.set('isRotated', !this.model.get('isRotated')); + }, + + /** + * Responds to clicks on the device frame label. + * + * @param jQuery.Event event + */ + revealDetails: function (event) { + this.model.set('isDetailsExpanded', !this.model.get('isDetailsExpanded')); + }, + + /** + * 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('.responsive-preview-control.responsive-preview-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. + var $label = $container.find('.responsive-preview-device-label'); + // Expose the details if the user has expanded the label. + $label.toggleClass('responsive-preview-expanded', this.model.get('isDetailsExpanded')); + // Attach the device label. + $label + .find('.responsive-preview-device-label-text') + .text(Drupal.t('@label', { + '@label': $deviceLink.text() + })); + // The device details are appended to the device label node in a separate + // node so that their presentation can be varied independent of the label. + $label + .find('.responsive-preview-device-label-details') + .text(Drupal.t('@displayWidth@width by @displayHeight@height, @dpi, @orientation', { + '@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('.responsive-preview-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 . + * + * Websites that don't indicate via 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 tag's "content" attribute, if any. + * + * Parses something like this: + * + * 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('.responsive-preview-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 '
'; + }, + + /** + * 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 '
' + + '' + + '' + + '' + + '
'; + }, + + /** + * Theme function for a responsive preview iframe element. + * + * @return + * The corresponding HTML. + */ + responsivePreviewFrame: function () { + return ''; + } +}); + +}(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 @@ +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 @@ + 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 @@ + $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 @@ + 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 @@ + '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 @@ + '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..b87bf63 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.module @@ -0,0 +1,190 @@ +' . t('About') . ''; + $output .= '

' . 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.') . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '

' . 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'))) . '

'; + $output .= '

' . t('To close the preview, click the close button signified visually by an x.') . '

'; + return $output; + case 'admin/config/content/responsive-preview': + $output = '

' . 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.') . '

'; + 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('responsive-preview-device', 'responsive-preview-icon', 'responsive-preview-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('responsive-preview-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' => '', + '#value_suffix' => '', + '#attributes' => array( + 'title' => t('Preview page layout'), + 'class' => array('responsive-preview-icon', 'responsive-preview-icon-responsive-preview', 'responsive-preview-trigger'), + ), + ), + 'device_options' => array( + '#theme' => 'item_list', + '#items' => responsive_preview_get_devices_list(), + '#attributes' => array( + 'class' => array('responsive-preview-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