diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 011f95e..ff08f69 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -336,6 +336,9 @@ RDF module - Stéphane Corlosquet 'scor' http://drupal.org/user/52142 - Lin Clark 'linclark' http://drupal.org/user/396253 +Responsive preview module +- Jesse Renée Beach 'jessebeach' http://drupal.org/user/748566 + REST module - Klaus Purer 'klausi' http://drupal.org/user/262198 - Larry Garfield 'Crell' http://drupal.org/user/26398 diff --git a/core/modules/contextual/js/contextual.toolbar.js b/core/modules/contextual/js/contextual.toolbar.js index 53557e6..da938d7 100644 --- a/core/modules/contextual/js/contextual.toolbar.js +++ b/core/modules/contextual/js/contextual.toolbar.js @@ -31,13 +31,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..5e529b2 --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.module.css @@ -0,0 +1,131 @@ +/** + * @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; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 1; +} + +/** + * Preview iframe. + */ +.responsive-preview-frame-container { + position: absolute; + z-index: 100; +} +.responsive-preview-frame-container iframe { + display: block; + position: relative; +} + +/** + * Scroll container. + */ +#responsive-preview-scroll-track { + bottom: 0; + left: 0; + overflow: visible; + position: absolute; + right: 0; + top: 0; + z-index: 1; +} +#responsive-preview-scroll-pane { + min-height: 100%; + position: relative; + width: 100%; +} + +/** + * 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..c2e38d6 --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.theme.css @@ -0,0 +1,272 @@ +/** + * @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: -moz-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: -moz-linear-gradient(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: -moz-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 { + color: #909090; + cursor: pointer; + font-family: sans-serif; + font-size: 0.9286em; + font-weight: normal; + left: 30px; + line-height: 2.25; + margin: 0; + overflow: hidden; + position: absolute; + right: 30px; + text-align: center; + text-overflow: ellipsis; + top: 0; + white-space: nowrap; +} +.responsive-preview-device-label-text:after { + border-bottom-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + border-style: solid; + border-width: 0.8ex 0 0.8ex 0.9ex; + color: #909090; + content: ''; + display: inline-block; + line-height: 0; + margin-left: 0.75ex; + margin-right: 0.75ex; + width: 0; +} +[dir="rtl"] .responsive-preview-device-label-text:after { + border-left-color: transparent; + border-right-color: inherit; + border-width: 0.8ex 0.9ex 0.8ex 0; +} +.responsive-preview-device-label-text:hover:after { + color: inherit; +} +.responsive-preview-expanded .responsive-preview-device-label-text:after { + border-left-color: transparent; + border-right-color: inherit; + border-width: 0.8ex 0.9ex 0.8ex 0; +} +[dir="rtl"] .responsive-preview-expanded .responsive-preview-device-label-text:after { + border-left-color: inherit; + border-right-color: transparent; + border-width: 0.8ex 0 0.8ex 0.9ex; +} + +/** + * 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: left 150ms ease-out; + -moz-transition: left 150ms ease-out; + transition: left 150ms ease-out; +} +.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..0761aad --- /dev/null +++ b/core/modules/responsive_preview/js/responsive-preview.js @@ -0,0 +1,1073 @@ +/** + * @file + * Provides a component that previews the page in various device dimensions. + */ + +(function ($, Backbone, Drupal, drupalSettings, undefined) { + +"use strict"; + +var strings = { + close: Drupal.t('Close'), + orientation: Drupal.t('Change orientation'), + portrait: Drupal.t('Portrait'), + landscape: Drupal.t('Landscape') +}; + +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 +}, drupalSettings.responsivePreview); + +/** + * 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'); + + 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.documentElement.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: 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.addClass('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; + var handler; + + // Curry the 'this' object in order to pass it as an argument to the + // selectDevice function. + handler = selectDevice.bind(null, this); + this.$el.on('click.responsivepreview', '.responsive-preview-device', handler); + + this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this); + + this.tabModel.on('change:isDeviceListOpen', this.render, this); + + // Curry the 'this' object in order to pass it as an argument to the + // updateDeviceList function. + handler = updateDeviceList.bind(null, this); + this.envModel.on('change:viewportWidth', handler, null); + + 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; + var handler; + + // Curry the 'this' object in order to pass it as an argument to the + // selectDevice function. + handler = selectDevice.bind(null, this); + this.$el.on('click.responsivepreview', '.responsive-preview-device', handler); + + this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this); + + // Curry the 'this' object in order to pass it as an argument to the + // updateDeviceList function. + handler = updateDeviceList.bind(null, this); + this.envModel.on('change:viewportWidth', handler, null); + }, + + /** + * {@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-scroll-pane': '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')); + event.stopPropagation(); + }, + + /** + * Responds to clicks on the device frame label. + * + * @param jQuery.Event event + */ + revealDetails: function (event) { + this.model.set('isDetailsExpanded', !this.model.get('isDetailsExpanded')); + event.stopPropagation(); + }, + + /** + * 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. + var $frame = $(Drupal.theme('responsivePreviewFrame')) + // Load the current page URI into the preview iframe. + .on('load.responsivepreview', this._refresh.bind(this)) + // Add the frame to the preview container. + .appendTo($frameContainer); + // Wrap the frame container in a pair of divs that will allow for + // scrolling. + $frameContainer = $frameContainer.wrap(Drupal.theme('responsivePreviewScrollContainer')) + .closest('#responsive-preview-scroll-track'); + // Apply padding to the scroll pane. + $frameContainer.find('#responsive-preview-scroll-pane') + .css({ + 'padding-bottom': this.bleed, + 'padding-top': this.bleed + }); + // 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'); + // Load the path into the iframe. + $frame.get(0).contentWindow.location = Drupal.url(drupalSettings.currentPath); + // 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('#responsive-preview-frame'); + var $toolbarLining = $('.toolbar-lining'); + var $scrollPane = this.$el.find('#responsive-preview-scroll-pane'); + 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); + + // 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); + + // 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); + + // Updated the state of the rotated icon. + this.$el.find('.responsive-preview-control.responsive-preview-orientation').toggleClass('rotated', isRotated); + + // Reposition the preview root. + this.$el.css({ + top: offsets.top, + right: offsets.right, + left: offsets.left, + height: document.documentElement.clientHeight - (offsets.top + offsets.bottom) + }); + + // Position the frame. + var position = {}; + // Position depends on text direction. + position[edge] = (gutter > minGutter) ? gutter : minGutter; + $frame + .css({ + width: width, + height: height + }); + + // Position the frame container. + $container.css(position); + + // Resize the scroll pane. + var paneHeight = height + (this.bleed * 2) + $toolbarLining.height(); + // If the height of the pane that contains the preview frame is higher + // than the available viewport area, then make it scroll. + if (paneHeight > (document.documentElement.clientHeight - offsets.top - offsets.bottom)) { + $scrollPane + .css({ + height: paneHeight + }) + // Select the parent container that constrains the overflow. + .parent() + .css({ + overflow: 'scroll' + }); + } + // If the height of the viewport area is sufficient to display the preview + // frame, remove the scroll styling. + else { + $scrollPane.css({ + height: 'auto' + }) + // Select the parent container that constrains the overflow. + .parent() + .css({ + overflow: 'visible' + }); + } + + // Scale if not responsive. + this._scaleIfNotResponsive(); + + // Update the text in the device label. + var $label = $container.find('.responsive-preview-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, @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', + '@dpi': dimensions.dppx + 'ppx', + '@orientation': (isRotated) ? this.strings.landscape : this.strings.portrait + })); + + // Expose the details if the user has expanded the label. + var isDetailsExpanded = this.model.get('isDetailsExpanded'); + $label + .toggleClass('responsive-preview-expanded', isDetailsExpanded) + .find('.responsive-preview-device-label-details') + .toggleClass('visually-hidden', !isDetailsExpanded); + }, + + /** + * 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. + * + * @param Backbone.View view + * The View curried to this handler. This function is used in multiple Views, + * so we bind it as an argument to the handler function in order to avoid + * having to reference it through a 'this' object which will trigger 'Possible + * strict violation' warning messages in JSHint. + */ +function updateDeviceList (view) { + var gutter = view.gutter; + var bleed = view.bleed; + var viewportWidth = view.envModel.get('viewportWidth'); + var $devices = view.$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. + view.model.set('fittingDeviceCount', fittingDeviceCount); +} + +/** + * Updates the model to reflect the properties of the chosen device. + * + * @param Backbone.View view + * The View curried to this handler. This function is used in multiple Views, + * so we bind it as an argument to the handler function in order to avoid + * having to reference it through a 'this' object which will trigger 'Possible + * strict violation' warning messages in JSHint. + * @param jQuery.Event event + */ +function selectDevice (view, 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 (view.model.get('activeDevice') === name) { + view.model.set('isActive', false); + return; + } + // Update the device dimensions. + view.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. + view.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 the scrolling wrapper of the preview container. + * + * @return + * The corresponding HTML. + */ + responsivePreviewScrollContainer: function () { + 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..fbe8b7d --- /dev/null +++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceAccessController.php @@ -0,0 +1,40 @@ +hasPermission('administer site configuration'); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return $account->hasPermission('administer site configuration'); + } + +} 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..a33df45 --- /dev/null +++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/DeviceFormController.php @@ -0,0 +1,124 @@ +getOperation()) { + case 'add': + $form['#title'] = $this->t('Add device'); + break; + case 'edit': + $form['#title'] = $this->t('Edit device'); + break; + } + $entity = $this->entity; + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->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' => $this->t('Width'), + '#default_value' => $dimensions['width'], + '#field_suffix' => 'px', + '#size' => 6, + '#required' => TRUE, + ); + $form['dimensions']['height'] = array( + '#type' => 'textfield', + '#title' => $this->t('Height'), + '#default_value' => $dimensions['height'], + '#field_suffix' => 'px', + '#size' => 6, + '#required' => TRUE, + ); + $form['dimensions']['dppx'] = array( + '#type' => 'textfield', + '#title' => $this->t('Dots per pixel (dppx)'), + '#description' => $this->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' => $this->t('Default orientation'), + '#default_value' => $entity->get('orientation'), + '#options' => array('portrait' => $this->t('Portrait'), 'landscape' => $this->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($this->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($this->t('Edit'), $uri['path'])); + } + else { + drupal_set_message($this->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($this->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 @@ +t('Name'); + $row['status'] = $this->t('Show in list'); + $row['dimensions'] = $this->t('Dimensions'); + unset($row['id']); + $row['weight'] = $this->t('Weight'); + $row['operations'] = $this->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' => $this->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' => $this->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' => $this->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' => $this->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($this->t('The device settings have been updated.')); + } +} diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Entity/Device.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Entity/Device.php new file mode 100644 index 0000000..e22e56b --- /dev/null +++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Entity/Device.php @@ -0,0 +1,100 @@ + '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/Form/DeviceDelete.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php new file mode 100644 index 0000000..14a93a0 --- /dev/null +++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Form/DeviceDelete.php @@ -0,0 +1,51 @@ + $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelRoute() { + return array( + 'route_name' => 'responsive_preview.device.list', + ); + } + + /** + * {@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/Tests/DeviceCRUDTest.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php new file mode 100644 index 0000000..777a7de --- /dev/null +++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Tests/DeviceCRUDTest.php @@ -0,0 +1,89 @@ + '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->drupalPostForm('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->drupalPostForm('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->drupalPostForm('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.local_actions.yml b/core/modules/responsive_preview/responsive_preview.local_actions.yml new file mode 100644 index 0000000..2a7a2f9 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.local_actions.yml @@ -0,0 +1,5 @@ +responsive_preview.device.add_local_action: + route_name: responsive_preview.device.add + title: 'Add device' + appears_on: + - responsive_preview.device.list diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module new file mode 100644 index 0000000..216b194 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.module @@ -0,0 +1,191 @@ +' . 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('The preview does not purport to emulate the page rendering capabilities of an indicated device. It provides an approximate page preview rendered by your current browser, based on reported device sizes and their screen resolutions.') . '

'; + $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 or press the ESC key.') . '

'; + 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\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' => Drupal::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..1d3d338 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.routing.yml @@ -0,0 +1,28 @@ +responsive_preview.device.list: + path: '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: + path: 'admin/config/content/responsive-preview/add' + defaults: + _entity_form: responsive_preview_device.add + requirements: + _permission: 'administer site configuration' + +responsive_preview.device.edit: + path: '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: + path: 'admin/config/content/responsive-preview/manage/{responsive_preview_device}/delete' + defaults: + _entity_form: responsive_preview_device.delete + requirements: + _permission: 'administer site configuration'