From 4b35312fef820dede14c3c76c0107dbcf67ca8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Thu, 28 Mar 2013 12:21:38 -0400 Subject: [PATCH] Issue #1741498 by jessebeach, Wim Leers, jibran: Add a mobile preview bar to Drupal core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: J. Renée Beach --- core/modules/contextual/contextual.toolbar.js | 9 +- .../config/responsive_preview.devices.yml | 50 ++ .../css/responsive-preview.base-rtl.css | 32 + .../css/responsive-preview.base.css | 105 +++ .../css/responsive-preview.icons-rtl.css | 21 + .../css/responsive-preview.icons.css | 95 +++ .../css/responsive-preview.theme-rtl.css | 30 + .../css/responsive-preview.theme.css | 193 +++++ .../images/responsive-preview-icons.png | 10 + .../responsive_preview/js/responsive-preview.js | 752 ++++++++++++++++++++ .../block/block/ResponsivePreviewControlBlock.php | 47 ++ .../responsive_preview/responsive_preview.info.yml | 5 + .../responsive_preview/responsive_preview.module | 173 +++++ .../testswarm/responsive_preview.admin.tests.js | 27 + .../tests/testswarm/responsive_preview.tests.js | 69 ++ 15 files changed, 1617 insertions(+), 1 deletion(-) create mode 100644 core/modules/responsive_preview/config/responsive_preview.devices.yml create mode 100644 core/modules/responsive_preview/css/responsive-preview.base-rtl.css create mode 100644 core/modules/responsive_preview/css/responsive-preview.base.css create mode 100644 core/modules/responsive_preview/css/responsive-preview.icons-rtl.css create mode 100644 core/modules/responsive_preview/css/responsive-preview.icons.css create mode 100644 core/modules/responsive_preview/css/responsive-preview.theme-rtl.css create mode 100644 core/modules/responsive_preview/css/responsive-preview.theme.css create mode 100644 core/modules/responsive_preview/images/responsive-preview-icons.png create mode 100644 core/modules/responsive_preview/js/responsive-preview.js create mode 100644 core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/block/block/ResponsivePreviewControlBlock.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/tests/testswarm/responsive_preview.admin.tests.js create mode 100644 core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js index 45f9757..ada9f11 100644 --- a/core/modules/contextual/contextual.toolbar.js +++ b/core/modules/contextual/contextual.toolbar.js @@ -27,13 +27,20 @@ Drupal.behaviors.contextualToolbar = { model: model }); - // Update the model based on overlay events. $(document) + // Update the model based on Overlay events. .on('drupalOverlayOpen.contextualToolbar', function () { model.set('isVisible', false); }) .on('drupalOverlayClose.contextualToolbar', function () { model.set('isVisible', true); + }) + // Update the model based on Responsive Preview events. + .on('drupalResponsivePreviewStarted.contextualToolbar', function () { + model.set('isVisible', false); + }) + .on('drupalResponsivePreviewStopped.contextualToolbar', function () { + model.set('isVisible', true); }); // Update the model to show the edit tab if there's >=1 contextual link. diff --git a/core/modules/responsive_preview/config/responsive_preview.devices.yml b/core/modules/responsive_preview/config/responsive_preview.devices.yml new file mode 100644 index 0000000..8f4e088 --- /dev/null +++ b/core/modules/responsive_preview/config/responsive_preview.devices.yml @@ -0,0 +1,50 @@ +# References: +# - http://en.wikipedia.org/wiki/List_of_displays_by_pixel_density +# - http://www.w3.org/blog/CSS/2012/06/14/unprefix-webkit-device-pixel-ratio/ +# - http://pieroxy.net/blog/2012/10/18/media_features_of_the_most_common_devices.html +# +# The device listing and specifications will be updated periodically through +# minor releases of Drupal. + +iphone: + label: iPhone 5 + dimensions: + width: 640 + height: 1136 + dppx: 2 + orientation: portrait +iphone4: + label: iPhone 4 + dimensions: + width: 640 + height: 960 + dppx: 2 + orientation: portrait +ipad: + label: iPad + dimensions: + width: 1536 + height: 2048 + dppx: 2 + orientation: portrait +nexus4: + label: Nexus 4 + dimensions: + width: 768 + height: 1280 + dppx: 2 + orientation: portrait +nexus7: + label: Nexus 7 + dimensions: + width: 800 + height: 1280 + dppx: 1.325 + orientation: portrait +desktop: + label: Typical desktop + dimensions: + width: 1366 + height: 768 + dppx: 1 + orientation: landscape diff --git a/core/modules/responsive_preview/css/responsive-preview.base-rtl.css b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css new file mode 100644 index 0000000..790cc0a --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css @@ -0,0 +1,32 @@ +/** + * @file + * RTL base styling for responsive preview. + */ + +/** + * Toolbar tab. + */ + +/* At narrow screen widths, float the tab to the right so it falls in line with + * the rest of the toolbar tabs. */ +.js .toolbar .bar .toolbar-tab-responsive-preview.tab { + float: left; +} +.toolbar-tab-responsive-preview .responsive-preview-options { + left: 0.3em; + right: auto; +} + +/** + * Preview container. + * + * The container is kept offscreen after it is built and has been disabled. + */ +.responsive-preview { + left: auto; + right: -200%; +} +.responsive-preview.active { + left: auto; + right: 0; +} diff --git a/core/modules/responsive_preview/css/responsive-preview.base.css b/core/modules/responsive_preview/css/responsive-preview.base.css new file mode 100644 index 0000000..cd82f1b --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.base.css @@ -0,0 +1,105 @@ +/** + * @file + * Base styling for responsive preview. + */ + +/** + * Constrain the window height to the client height when the preview is active. + */ +.responsive-preview-active { + height: 100%; + overflow: hidden; +} + +/** + * Toolbar tab. + */ +.toolbar-tab-responsive-preview { + display: none; +} +/* At narrow screen widths, float the tab to the left so it falls in line with + * the rest of the toolbar tabs. */ +.js .toolbar .bar .toolbar-tab-responsive-preview.tab { + display: block; + float: right; /* LTR */ + position: relative; +} +.toolbar-tab-responsive-preview .trigger { + display: block; +} +/* Device preview options. */ +.toolbar-tab-responsive-preview .item-list { + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} +.toolbar-tab-responsive-preview.open .item-list { + display: block; +} +.js .toolbar-tab-responsive-preview.tab .options li { + float: none; +} + +/** + * Preview container. + * + * The container is kept offscreen after it is built and has been disabled. + */ +.responsive-preview { + bottom: 0; + height: 100%; + left: -200%; /* LTR */ + position: relative; + top: 0; + width: 100%; + z-index: 1050; +} +.responsive-preview.active { + left: 0; /* LTR */ + position: fixed; +} +.responsive-preview .control { + position: absolute; +} +.responsive-preview .modal-background { + bottom: 0; + height: 100%; + left: 0; + position: static; + right: 0; + top: 0; + width: 100%; + z-index: 1; +} +.responsive-preview.active .modal-background { + position: fixed; +} + +/** + * Preview iframe. + */ +.responsive-preview .frame-container { + position: absolute; + z-index: 100; +} +.responsive-preview .frame-container iframe { + position: relative; +} + +/** + * Override Toolbar styling in the preview iframe. + */ +body.toolbar-tray-open.responsive-preview-frame { + margin-left: 0 !important; + margin-right: 0 !important; +} +.responsive-preview-frame { + overflow-x: hidden !important; +} +.responsive-preview-frame #toolbar-administration { + display: none !important; +} +.responsive-preview-frame .contextual { + display: none !important; +} diff --git a/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css b/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css new file mode 100644 index 0000000..0c8d2b6 --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.icons-rtl.css @@ -0,0 +1,21 @@ +/** + * @file + * RTL icon styling for responsive preview. + */ + +.toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before { + left: auto; /* LTR */ + right: 1em; +} + +/** + * Responsive preview controls icons. + */ +.responsive-preview .icon-close:before { + left: 9px; + right: auto; +} +.responsive-preview .icon-orientation:before { + left: auto; + right: 9px; +} diff --git a/core/modules/responsive_preview/css/responsive-preview.icons.css b/core/modules/responsive_preview/css/responsive-preview.icons.css new file mode 100644 index 0000000..e616c2c --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.icons.css @@ -0,0 +1,95 @@ +/** + * @file + * Responsive preview icon styling. + */ +.toolbar-tab-responsive-preview .icon:before, +.responsive-preview .icon:before { + background-attachment: scroll; + background-color: transparent; + background-image: url("../images/responsive-preview-icons.png"); + background-repeat: no-repeat; + content: ''; + display: block; + position: absolute; + z-index: 1; +} +.toolbar .bar .toolbar-tab-responsive-preview .icon:before { + width: 13px; +} +.responsive-preview button.icon { + background-color: transparent; + border: 0; + font-size: 1em; +} + +/* Toolbar icon. */ +.toolbar .bar .icon.icon-responsive-preview { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + width: 5em; +} +.toolbar-tab-responsive-preview .icon.icon-responsive-preview:before { + background-position: center top; +} +.toolbar-tab-responsive-preview.open .icon-responsive-preview:before, +.toolbar-tab-responsive-preview .icon-responsive-preview.active:before, +.toolbar-tab-responsive-preview .icon-responsive-preview:hover:before { + background-position: center -22px; +} +.toolbar .bar .toolbar-tab-responsive-preview .icon-responsive-preview:before { + left: 1em; /* LTR */ + height: 22px; + top: 0.6667em; +} +.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active { + padding-left: 2.25em; +} +.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before { + background-position: -999px -999px; +} +.toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active.active:before { + background-position: center -110px; +} +@media only screen and (min-width: 16.5em) { + .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active { + padding: 0.5em 1.3333em 0.5em 2.25em; + text-indent: 0; + width: auto; + } + .toolbar .toolbar-tab-responsive-preview.tab .options .device.icon-active:before { + left: 0.667em; + } +} + +/** + * Responsive preview controls icons. + */ +.responsive-preview .control.icon:before { + height: 12px; + width: 12px; + top: 12px; +} +.responsive-preview .icon-close:before { + background-position: left -44px; + right: 9px; /* LTR */ +} +.responsive-preview .icon-close:active:before, +.responsive-preview .icon-close.active:before, +.responsive-preview .icon-close:hover:before { + background-position: left -56px; +} +.responsive-preview .icon-orientation:before { + background-position: left -68px; + left: 9px; /* LTR */ +} +.responsive-preview .icon-orientation:hover:before { + background-position: left -80px; +} +.responsive-preview .icon-orientation.rotated:before { + background-position: left -92px; +} +.responsive-preview .icon-orientation.rotated:hover:before { + background-position: left -104px; +} diff --git a/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css new file mode 100644 index 0000000..b8892ab --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css @@ -0,0 +1,30 @@ +/** + * @file + * RTL styling for responsive preview. + */ + +/** + * Toolbar tab. + */ + +/* Toolbar tab triangle toggle. */ +.toolbar-tab-responsive-preview .trigger:after { + left: 1em; + right: auto; +} +.toolbar-tab-responsive-preview.open:before { + left: 0; + right: auto; +} +.toolbar-tab-responsive-preview.open .trigger:after { + left: 0.7em; + right: auto; +} +.responsive-preview .control.close { + left: 0; + right: auto; +} +.responsive-preview .control.orientation { + left: auto; + right: 0; +} diff --git a/core/modules/responsive_preview/css/responsive-preview.theme.css b/core/modules/responsive_preview/css/responsive-preview.theme.css new file mode 100644 index 0000000..5b19b83 --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.theme.css @@ -0,0 +1,193 @@ +/** + * @file + * Styling for responsive preview. + */ + +/** + * Toolbar tab. + */ +.toolbar-tab-responsive-preview .options { + background-color: #0f0f0f; +} +/* Device preview options. */ +.toolbar-tab-responsive-preview .options { + box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75); +} +.toolbar-tab-responsive-preview .options li { + background-color: white; + padding: 0; +} +.toolbar-tab-responsive-preview .trigger { + cursor: pointer; + line-height: 1; + height: 3em; +} +.toolbar-tab-responsive-preview .trigger:hover { + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%); + background-image: linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%); +} +.toolbar-tab-responsive-preview .trigger.active, +.toolbar-tab-responsive-preview .trigger.active:hover { + background-image: -webkit-linear-gradient(top, rgb(78,159,234) 0%, rgb(69,132,221) 100%); + background-image: linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%); +} +.toolbar-tab-responsive-preview .trigger, +.toolbar-tab-responsive-preview .options .device { + padding-bottom: 1em; + padding-top: 1em; +} +.toolbar-tab-responsive-preview .options .device { + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: 1em; + padding: 0.5em 1.3333em +} +.toolbar .toolbar-tab-responsive-preview.tab .options .device { + color: #777; +} +.toolbar .toolbar-tab-responsive-preview.tab .options .device:hover, +.toolbar .toolbar-tab-responsive-preview.tab .options .device.active { + color: black; +} + +/* Toolbar tab triangle toggle. */ +.toolbar-tab-responsive-preview .trigger:after { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 0.4545em 0.4em 0; + color: #a0a0a0; + content: ' '; + display: block; + height: 0; + line-height: 0; + overflow: hidden; + position: absolute; + right: 1.6667em; /* LTR */ + top: 50%; + margin-top: -0.1666em; + width: 0; + z-index: 1 +} +.toolbar-tab-responsive-preview.open:before { + background-color: white; + bottom: 0; + content: ' '; + display: block; + position: absolute; + right: 0; /* LTR */ + top: 0; + width: 2em; + z-index: 1; +} +.toolbar-tab-responsive-preview.open .trigger:after { + border-bottom: 0.4545em solid; + border-top-color: transparent; + color: black; + right: 0.7em; /* LTR */ + top: 1.25em; +} +.toolbar-tab-responsive-preview:hover .trigger:after, +.toolbar-tab-responsive-preview .trigger.active:after, +.toolbar-tab-responsive-preview:hover .trigger.active:after { + color: white; +} +.toolbar-tab-responsive-preview.open:hover .trigger:after { + color: black; +} + +/** + * Preview container. + */ +.responsive-preview { + box-shadow: 0 0 10px 0 black; + opacity: 0; + -moz-transition: all 450ms; + -webkit-transition: all 450ms; + transition: all 450ms; +} +.responsive-preview.active { + opacity: 1; +} +.responsive-preview .modal-background { + background-color: black; + background-color: rgba(0,0,0,0.92); + background-image: -webkit-linear-gradient(left, black,rgb(20,20,20) 15%, rgb(45,45,45) 40%, rgb(45,45,45) 60%, rgb(20,20,20) 85%, black 100%); + background-image: linear-gradient(left, black, rgb(20,20,20) 15%, rgb(45,45,45) 40%, rgb(45,45,45) 60%, rgb(20,20,20) 85%, black 100%); +} + +/** + * Responsive preview control placement. + */ +.responsive-preview .control { + cursor: pointer; + height: 40px; + position: absolute; + top: 0; + width: 40px; +} +.responsive-preview .control.close { + right: 0; /* LTR */ +} +.responsive-preview .control.orientation { + left: 0; /* LTR */ +} +.responsive-preview .device-label { + color: #e0e0e0; + font-family: sans-serif; + font-size: 0.75em; + font-weight: normal; + left: 40px; + letter-spacing: 0.25ex; + line-height: 2.6667; + margin: 0; + overflow: hidden; + position: absolute; + right: 40px; + text-overflow: ellipsis; + top: 0; + white-space: nowrap; + width: auto; +} + +/** + * Responsive preview frame. + */ +.responsive-preview .frame-container { + background-color: #343434; + border-radius: 20px; + box-shadow: + 0 0 0px 1px #404040, + 2px 2px 0 0px black; + -webkit-transition: all 150ms ease-out; + -moz-transition: all 150ms ease-out; + -o-transition: all 150ms ease-out; + transition: all 150ms ease-out; + top: 1em; +} +.responsive-preview .frame-container iframe { + box-shadow: + 0 0 0 2px black, + 0 0 0 3px #404040; + -webkit-transition: all 150ms ease-out; + -moz-transition: all 150ms ease-out; + -o-transition: all 150ms ease-out; + transition: all 150ms ease-out; +} + +/** + * Control block styling. + */ +#block-responsive-preview-controls .content .device { + background: none; + border: none; + color: inherit; + cursor: pointer; + font: inherit; + line-height: 1; + margin: 0; + padding: 0.25em 0; +} diff --git a/core/modules/responsive_preview/images/responsive-preview-icons.png b/core/modules/responsive_preview/images/responsive-preview-icons.png new file mode 100644 index 0000000..6eedbbe --- /dev/null +++ b/core/modules/responsive_preview/images/responsive-preview-icons.png @@ -0,0 +1,10 @@ +PNG + + IHDR  BqtEXtSoftwareAdobe ImageReadyqe<IDATxXO#G]/6} :HH*>)]R:] +8E4A# +H`wwꄎD(/u"*MA"@m؛[~z(b|f{ofV+H ^z= +#_cccj[[[(LⱺaAcfN&x]㛯1YᴗxyYi-g`zG>9NQfH"rZUU3E{+űXDNs-d 1Y[ ɿҝt +aC N +B,VL巧hHzr:6uMnnnZߡ<w-¤,wvww 6?rJ/0^ǝ8??Ju hF٥6 Dtt*B!PzPX|B`D+`6kEAb;...>O&ߛBn<2Ibķ(D"- 0nB PGjfe|||Ay -}jy"S{GPI CXt`*'ANQ]iAj˩!ga)="'2jHW3cV !?QE <_  bxe\@ 0) { + var tabView = new Drupal.responsivePreview.views.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) { + var blockView = new Drupal.responsivePreview.views.BlockView({ + el: $block.get(), + model: previewModel, + envModel: envModel, + // Gutter size around preview frame. + gutter: options.gutter, + // Preview device frame width. + bleed: options.bleed + }); + } + // The preview container view. + var previewView = new Drupal.responsivePreview.views.PreviewView({ + el: Drupal.theme('responsivePreviewContainer'), + model: previewModel, + envModel: envModel, + // Gutter size around preview frame. + gutter: options.gutter, + // Preview device frame width. + bleed: options.bleed, + strings: options.strings + }); + + var setViewportWidth = function() { + envModel.set('viewportWidth', document.documentElement.clientWidth); + }; + + $(window) + // Update the viewport width whenever it is resized, but max 4 times/s. + .on('resize.responsivePreview', Drupal.debounce(setViewportWidth, 250)); + + // Allow other scripts to respond to responsive preview mode changes. + tabModel.on('change:isActive', function (model, isActive) { + $(document).trigger((isActive) ? 'drupalResponsivePreviewStarted' : 'drupalResponsivePreviewStopped'); + }); + + // Initialization: set the current viewport width. + setViewportWidth(); + } + // The main window is equivalent to window.parent and window.self. Inside, + // an iframe, these objects are not equivalent. If the parent window is + // itself in an iframe, check that the parent window has been processed. + // If it has been, this invocation of attach() is being called on the + // preview iframe, not its parent. + if ((window.parent !== window.self) && !$body.length) { + var $frameBody = $(window.self.document.body).once('responsive-preview'); + if ($frameBody.length > 0) { + $frameBody.get(0).className += ' responsive-preview-frame'; + } + } + }, + defaults: { + gutter: 60, + // The width of the device border around the iframe. This value is critical + // to determine the size and placement of the preview iframe container, + // therefore it must be defined here instead of in the CSS file. + bleed: 30, + strings: { + close: Drupal.t('close'), + orientation: Drupal.t('Change orientation'), + portrait: Drupal.t('portrait'), + landscape: Drupal.t('landscape') + } + } +}; + +Drupal.responsivePreview = Drupal.responsivePreview || {models: {}, views: {}}; + +/** + * Backbone Model for the environment in which the Responsive Preview operates. + */ +Drupal.responsivePreview.models.EnvironmentModel = Backbone.Model.extend({ + defaults: { + // The viewport width, within which the preview will have to fit. + viewportWidth: null, + // Text direction of the document, affects some positioning. + dir: 'ltr' + } +}); + +/** + * Backbone Model for the Responsive Preview toolbar tab state. + */ +Drupal.responsivePreview.models.TabStateModel = Backbone.Model.extend({ + defaults: { + // The state of toolbar list of available device previews. + isDeviceListOpen: false + } +}); + +/** + * Backbone Model for the Responsive Preview preview state. + */ +Drupal.responsivePreview.models.PreviewStateModel = Backbone.Model.extend({ + defaults: { + // The state of the preview. + isActive: false, + // Indicates whether the preview iframe has been built. + isBuilt: false, + // Indicates whether the device is portrait (false) or landscape (true). + isRotated: false, + // The number of devices that fit the current viewport (i.e. previewable). + fittingDeviceCount: 0, + // Currently selected device link. + activeDevice: null, + // Dimensions of the currently selected device to preview. + dimensions: { + // The width of the device to preview. + width: null, + // The height of the device to preview. + height: null, + // The dots per pixel of the device to preview. + dppx: null + } + } +}); + +/** + * Handles responsive preview toolbar tab interactions. + */ +Drupal.responsivePreview.views.TabView = Backbone.View.extend({ + + events: { + 'click': 'toggleDeviceList', + 'mouseleave': 'toggleDeviceList', + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function () { + this.gutter = this.options.gutter; + this.bleed = this.options.bleed; + this.tabModel = this.options.tabModel; + this.envModel = this.options.envModel; + + // The selectDevice function is declared outside of the view because it is + // shared among views. It must be bound to this for the correct context + // to obtain. + this.$el.on('click.responsivePreview', '.device', $.proxy(selectDevice, this)); + + this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this); + + this.tabModel.on('change:isDeviceListOpen', this.render, this); + + this.envModel.on('change:viewportWidth', updateDeviceList, this); + this.envModel.on('change:viewportWidth', this.correctDeviceListEdgeCollision, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + var $deviceLink = $(this.model.get('activeDevice')); + var name = $deviceLink.data('responsive-preview-name'); + var isActive = this.model.get('isActive'); + var isDeviceListOpen = this.tabModel.get('isDeviceListOpen'); + this.$el + // Render the visibility of the toolbar tab. + .toggle(this.model.get('fittingDeviceCount') > 0) + // Toggle the display of the device list. + .toggleClass('open', isDeviceListOpen); + + // Render the state of the toolbar tab button. + this.$el + .find('> button') + .toggleClass('active', isActive) + .attr('aria-pressed', isActive); + + // Clean the active class from the device list. + this.$el + .find('.device.active') + .removeClass('active'); + + this.$el + .find('[data-responsive-preview-name="' + name + '"]') + .toggleClass('active', isActive); + // When the preview is active, a class on the body is necessary to impose + // styling to aid in the display of the preview element. + $('body').toggleClass('responsive-preview-active', isActive); + // The list of devices might render outside the window. + if (isDeviceListOpen) { + this.correctDeviceListEdgeCollision(); + } + return this; + }, + + /** + * Toggles the list of devices available to preview from the toolbar tab. + * + * @param Object event + * jQuery Event object. + */ + 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.model.get('isDeviceListOpen')); + } + + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Model change handler; corrects possible device list window edge collision. + */ + correctDeviceListEdgeCollision: function () { + // The position of the dropdown depends on the language direction. + var dir = this.envModel.get('dir'); + var edge = (dir === 'rtl') ? 'left' : 'right'; + this.$el + .find('.item-list') + .position({ + 'my': edge +' top', + 'at': edge + ' bottom', + 'of': this.$el, + 'collision': 'flip fit' + }); + } +}); + +/** + * Handles responsive preview control block interactions. + */ +Drupal.responsivePreview.views.BlockView = Backbone.View.extend({ + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function () { + this.gutter = this.options.gutter; + this.bleed = this.options.bleed; + this.envModel = this.options.envModel; + + // The selectDevice function is declared outside of the view because it is + // shared among views. It must be bound to this for the correct context + // to obtain. + this.$el.on('click.responsivePreview', '.device', $.proxy(selectDevice, this)); + + this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this); + + this.envModel.on('change:viewportWidth', updateDeviceList, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + var $deviceLink = $(this.model.get('activeDevice')); + var name = $deviceLink.data('responsive-preview-name'); + var isActive = this.model.get('isActive'); + this.$el + // Render the visibility of the toolbar block. + .toggle(this.model.get('fittingDeviceCount') > 0) + .find('.device.active') + .removeClass('active'); + + this.$el + .find('[data-responsive-preview-name="' + name + '"]') + .addClass('active'); + // When the preview is active, a class on the body is necessary to impose + // styling to aid in the display of the preview element. + $('body').toggleClass('responsive-preview-active', isActive); + return this; + } +}); + +/** + * Functions that are common to both the TabView and BlockView. + */ + +/** + * Model change handler; hides devices that don't fit the current viewport. + */ +function updateDeviceList () { + var gutter = this.gutter; + var bleed = this.bleed; + var viewportWidth = this.envModel.get('viewportWidth'); + var $devices = this.$el.find('.device'); + + // Remove devices whose previews won't fit the current viewport. + $devices.each(function (index, element) { + var $this = $(this); + var width = parseInt($this.data('responsive-preview-width'), 10); + var dppx = parseFloat($this.data('responsive-preview-dppx'), 10); + var previewWidth = width / dppx; + var fits = ((previewWidth + (gutter * 2) + (bleed * 2)) <= viewportWidth); + $this.parent('li').toggleClass('element-hidden', !fits); + }); + // Set the number of devices that fit the current viewport. + this.model.set('fittingDeviceCount', $devices.parent('li').not('.element-hidden').length); +} + +/** + * Updates the model to reflect the properties of the chosen device. + * + * @param Object event + * A jQuery event object. + */ +function selectDevice (event) { + var $link = $(event.target); + // Update the device dimensions. + this.model.set({ + 'activeDevice': $link.get(0), + 'dimensions': { + 'width': parseInt($link.data('responsive-preview-width'), 10), + 'height': parseInt($link.data('responsive-preview-height'), 10), + 'dppx': parseFloat($link.data('responsive-preview-dppx'), 10) + } + }); + // Toggle the preview on. + this.model.set('isActive', true); + + event.preventDefault(); +} + +/** + * Handles the responsive preview element interactions. + */ +Drupal.responsivePreview.views.PreviewView = Backbone.View.extend({ + + events: { + 'click #responsive-preview-close': 'onClose', + 'click #responsive-preview-orientation': 'onRotate' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function () { + this.gutter = this.options.gutter; + this.bleed = this.options.bleed; + this.strings = this.options.strings; + this.tabModel = this.options.tabModel; + this.envModel = this.options.envModel; + + this.model.on('change:isActive change:isRotated change:dimensions change:activeDevice', this.render, this); + + // Recalculate the size of the preview container when the window resizes. + this.envModel.on('change:viewportWidth', this.render, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + var isActive = this.model.get('isActive'); + + // Build the preview if it doesn't exist. + if (isActive && !this.model.get('isBuilt')) { + this._build(); + } + + // Early-return if inactive. + if (isActive) { + // Refresh the preview. + this._refresh(); + } + + // Render the state of the preview. + var that = this; + // Wrap the call in a setTimeout so that it invokes in the next compute + // cycle, causing the CSS animations to render in the first pass. + window.setTimeout(function () { + that.$el.toggleClass('active', isActive); + }, 0); + + return this; + }, + + /** + * Closes the preview. + * + * @param Object event + * A jQuery event object. + */ + onClose: function (event) { + this.model.set('isActive', false); + }, + + /** + * Responds to rotation button presses. + * + * @param Object event + * A jQuery event object. + */ + onRotate: function (event) { + this.model.set('isRotated', !this.model.get('isRotated')); + }, + + /** + * Builds the preview iframe. + */ + _build: function () { + var $frameContainer = $(Drupal.theme('responsivePreviewFrameContainer')) + .find('#responsive-preview-close span') + .text(this.strings.close) + .end() + .find('#responsive-preview-orientation span') + .text(this.strings.orientation) + .end() + // The padding around the frame must be known in order to position it + // correctly, so the style property is defined in JavaScript rather than + // CSS. + .css('padding', this.bleed); + // Attach the iframe that will hold the preview. + var $frame = $(Drupal.theme('responsivePreviewFrame')) + .attr({ + 'data-loading': true, + src: drupalSettings.basePath + Drupal.encodePath(drupalSettings.currentPath), + width: '100%', + height: '100%' + }) + // Load the current page URI into the preview iframe. + .on('load.responsivePreview', $.proxy(this._refresh, this)) + // Add the frame to the preview container. + .appendTo($frameContainer); + // Adjust the placement of the preview container and insert it into the DOM. + this.$el + .css({ top: this._getDisplacement('top') }) + // Displace the top of the container. + .attr('data-offset-top', this._getDisplacement('top')) + // Apend the frame container. + .append($frameContainer) + // Append the container to the body to initialize the iframe document. + .appendTo('body'); + // Mark the preview element processed. + this.model.set('isBuilt', true); + }, + + /** + * Refreshes the preview based on the current state (device & viewport width). + */ + _refresh: function () { + var isRotated = this.model.get('isRotated'); + var $deviceLink = $(this.model.get('activeDevice')); + var $container = this.$el.find('#responsive-preview-frame-container'); + var $frame = $container.find('> iframe'); + + // Get the static state. + var edge = (this.envModel.get('dir') === 'rtl') ? 'right' : 'left'; + var minGutter = this.gutter; + + // Get current (dynamic) state. + var dimensions = this.model.get('dimensions'); + var isRotated = this.model.get('isRotated'); + var viewportWidth = this.envModel.get('viewportWidth'); + + // Calculate preview width & height. If the preview is rotated, swap width + // and height. + var 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 the left and right. + var bleed = this.bleed; + var spread = width + (bleed * 2); + + // Calculate gutter. + var gutterPercent = (1 - (spread / viewportWidth)) / 2; + var gutter = gutterPercent * viewportWidth; + gutter = (gutter < minGutter) ? minGutter : gutter; + + // The preview width plus gutters must fit within the viewport width. + width = (viewportWidth - (gutter * 2) < spread) ? viewportWidth - (gutter * 2) - (bleed * 2) : width; + + // Updated the state of the rotated icon. + this.$el.find('.control.orientation').toggleClass('rotated', isRotated); + + // Resize & reposition the iframe. + var position = {}; + position[edge] = gutter; // Depends on text direction. + $frame + .css({ + width: width, + height: height + }); + $container + .css(position); + + // Scale if not responsive. + this._scaleIfNotResponsive(); + + // Update the device label. + $container.find('.device-label').text(Drupal.t('@label (@widthpx by @heightpx, @dpidppx, @orientation)', { + '@label': $deviceLink.text(), + '@width': Math.ceil(displayWidth), + '@height': Math.ceil(displayHeight), + '@dpi': dimensions.dppx, + '@orientation': (isRotated) ? this.strings.landscape : this.strings.portrait + })); + }, + + /** + * Applies scaling in order to better approximate content display on a device. + */ + _scaleIfNotResponsive: function () { + var scalingCSS = this._calculateScalingCSS(); + if (scalingCSS === false) { + return; + } + + // Step 0: find DOM nodes we'll need to modify. + var $frame = this.$el.find('#responsive-preview-frame'); + var $html = $($frame[0].contentDocument || $frame[0].contentWindow.document).find('html'); + + // Step 1: When scaling (as we're about to do), the background (color and + // image) doesn't scale along. Fortunately, we can fix things in case of + // background color. + // @todo: figure out a work-around for background images, or somehow + // document this explicitly. + function isTransparent (color) { + // TRICKY: edge case for Firefox' "transparent" here; this is a + // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 + return (color === 'rgba(0, 0, 0, 0)' || color === 'transparent'); + } + var htmlBgColor = $html.css('background-color'); + var bodyBgColor = $html.find('body').css('background-color'); + if (!isTransparent(htmlBgColor) || !isTransparent(bodyBgColor)) { + var bgColor = isTransparent(htmlBgColor) ? bodyBgColor : htmlBgColor; + $frame.css('background-color', bgColor); + } + + // Step 2: apply scaling. + $html.css(scalingCSS); + }, + + /** + * Calculates scaling based on device dimensions and . + * + * 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; + }, + + /** + * Gets the total displacement of given region. + * + * @param String region + * Region name. Either "top" or "bottom". + * + * @return Number + * The total displacement of given region in pixels. + */ + _getDisplacement: function (region) { + var displacement = 0; + var lastDisplaced = $('[data-offset-' + region + ']'); + if (lastDisplaced.length) { + displacement = parseInt(lastDisplaced.attr('data-offset-' + region), 10); + } + return displacement; + } + +}); + +/** + * Registers theme templates with Drupal.theme(). + */ +$.extend(Drupal.theme, { + /** + * Theme function for the preview container element. + * + * @return + * The corresponding HTML. + */ + responsivePreviewContainer: function () { + return '
'; + }, + + /** + * Theme function for the close button for the preview container. + * + * @return + * The corresponding HTML. + */ + responsivePreviewFrameContainer: function () { + return '
' + + '' + + '' + + '' + + '
'; + }, + + /** + * Theme function for a responsive preview iframe element. + * + * @return + * The corresponding HTML. + */ + responsivePreviewFrame: function (url) { + return ''; + } +}); + +}(jQuery, Backbone, Drupal, drupalSettings, window, document)); diff --git a/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/block/block/ResponsivePreviewControlBlock.php b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/block/block/ResponsivePreviewControlBlock.php new file mode 100644 index 0000000..a884df4 --- /dev/null +++ b/core/modules/responsive_preview/lib/Drupal/responsive_preview/Plugin/block/block/ResponsivePreviewControlBlock.php @@ -0,0 +1,47 @@ + 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/responsive_preview.info.yml b/core/modules/responsive_preview/responsive_preview.info.yml new file mode 100644 index 0000000..61942d4 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.info.yml @@ -0,0 +1,5 @@ +name: 'Responsive Preview' +description: 'Provides a component that previews a page in various device dimensions.' +package: Core +version: VERSION +core: 8.x diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module new file mode 100644 index 0000000..59ca636 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.module @@ -0,0 +1,173 @@ +' . 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; + } +} + +/** + * Returns a list of devices and their properties from configuration. + */ +function responsive_preview_get_devices_list() { + $devices = config('responsive_preview.devices')->get(); + + $links = array(); + foreach($devices as $name => $info) { + $links[$name] = array( + '#theme' => 'html_tag', + '#tag' => 'button', + '#value' => $info['label'], + '#attributes' => array( + 'class' => array('device', 'icon', 'icon-active'), + 'data-responsive-preview-name' => $name, + 'data-responsive-preview-width' => (!empty($info['dimensions']['width'])) ? $info['dimensions']['width'] : '', + 'data-responsive-preview-height' => (!empty($info['dimensions']['height'])) ? $info['dimensions']['height'] : '', + 'data-responsive-preview-dppx' => (!empty($info['dimensions']['dppx'])) ? $info['dimensions']['dppx'] : '1', + ), + ); + } + + return $links; +} + +/** + * Prevents the preview tab from rendering on administration pages. + */ +function responsive_preview_access() { + return !path_is_admin(current_path()); +} + +/** + * Implements hook_toolbar(). + */ +function responsive_preview_toolbar() { + $items['responsive_preview'] = array( + '#type' => 'toolbar_item', + 'tab' => array( + 'trigger' => array( + '#theme' => 'html_tag', + '#tag' => 'button', + '#value' => t('Layout preview'), + '#value_prefix' => '', + '#value_suffix' => '', + '#attributes' => array( + 'title' => t('Preview page layout'), + 'class' => array('icon', 'icon-responsive-preview', 'trigger'), + ), + ), + 'device_options' => array( + '#theme' => 'item_list', + '#items' => responsive_preview_get_devices_list(), + '#attributes' => array( + 'class' => array('options'), + ), + ), + ), + '#wrapper_attributes' => array( + 'id' => 'responsive-preview-toolbar-tab', + 'class' => array('toolbar-tab-responsive-preview'), + ), + '#attached' => array( + 'library' => array( + array('responsive_preview', 'responsive-preview'), + ), + ), + '#weight' => 200, + '#access' => responsive_preview_access(), + ); + + return $items; +} + +/** + * Implements hook_library(). + */ +function responsive_preview_library_info() { + $path = drupal_get_path('module', 'responsive_preview'); + $options = array( + 'scope' => 'footer', + 'attributes' => array('defer' => TRUE), + ); + + $libraries['responsive-preview'] = array( + 'title' => 'Preview layouts', + 'version' => VERSION, + 'css' => array( + $path . '/css/responsive-preview.base.css', + $path . '/css/responsive-preview.theme.css', + $path . '/css/responsive-preview.icons.css', + ), + 'js' => array( + $path . '/js/responsive-preview.js' => $options, + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'backbone'), + array('system', 'jquery.ui.position'), + ), + ); + + return $libraries; +} + +/** + * Implements hook_testswarm_tests(). + */ +function responsive_preview_testswarm_tests() { + + $path = drupal_get_path('module', 'responsive_preview'); + + return array( + 'responsivePreview' => array( + 'module' => 'responsive_preview', + 'description' => 'Test the responsive preview module.', + 'js' => array( + $path . '/tests/testswarm/responsive_preview.tests.js' => array(), + array( + 'data' => array( + 'responsive_preview' => array( + 'devices' => config('responsive_preview.devices')->get() + ), + ), + 'type' => 'setting', + ), + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupalSettings'), + array('testswarm', 'jquery.simulate'), + ), + 'path' => '', + 'permissions' => array() + ), + 'responsivePreviewAdmin' => array( + 'module' => 'responsive_preview', + 'description' => 'Test the responsive preview module admin.', + 'js' => array( + $path . '/tests/testswarm/responsive_preview.admin.tests.js' => array(), + ), + 'dependencies' => array( + array('system', 'jquery'), + ), + 'path' => 'admin', + 'permissions' => array() + ), + ); +} diff --git a/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js b/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js new file mode 100644 index 0000000..b0466e9 --- /dev/null +++ b/core/modules/responsive_preview/tests/testswarm/responsive_preview.admin.tests.js @@ -0,0 +1,27 @@ +/*jshint strict:true, browser:true, curly:true, eqeqeq:true, expr:true, forin:true, latedef:true, newcap:true, noarg:true, trailing: true, undef:true, unused:true */ +/*global Drupal: true, jQuery: true, QUnit:true*/ +(function ($, Drupal, drupalSettings, window, document, undefined) { + "use strict"; + Drupal.tests.responsivePreviewAdmin = { + getInfo: function() { + return { + name: 'Responsive Preview', + description: 'Tests for the responsive preview admin.', + group: 'Core' + }; + }, + setup: function () {}, + teardown: function () {}, + tests: { + toolbarTab: function ($, Drupal, window, document, undefined) { + return function() { + QUnit.expect(1); + + // The toolbar tab should not be present on an admin path. + var $tab = $('.toolbar .toolbar-tab-responsive-preview'); + QUnit.equal($tab.length, 0, Drupal.t('The tab is not present.')); + }; + } + } + }; +})(jQuery, Drupal, drupalSettings, this, this.document); diff --git a/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js b/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js new file mode 100644 index 0000000..f3f66cc --- /dev/null +++ b/core/modules/responsive_preview/tests/testswarm/responsive_preview.tests.js @@ -0,0 +1,69 @@ +/*jshint strict:true, browser:true, curly:true, eqeqeq:true, expr:true, forin:true, latedef:true, newcap:true, noarg:true, trailing: true, undef:true, unused:true */ +/*global Drupal: true, jQuery: true, QUnit:true*/ +(function ($, Drupal, drupalSettings, window, document, undefined) { + "use strict"; + Drupal.tests.responsivePreview = { + getInfo: function() { + return { + name: 'Responsive Preview', + description: 'Tests for the responsive preview feature.', + group: 'Core' + }; + }, + setup: function () {}, + teardown: function () { + // Close the preview container. + $('#responsive-preview-close').trigger('click'); + }, + tests: { + toolbarTab: function ($, Drupal, window, document, undefined) { + return function() { + QUnit.expect(3); + + // Find the toolbar tab. + var $tab = $('.toolbar .toolbar-tab-responsive-preview'); + QUnit.equal($tab.length, 1, Drupal.t('The tab is present.')); + + // Verify the tab dropdown click functionality. + $tab.find('> .trigger').trigger('click'); + QUnit.ok($tab.hasClass('open'), Drupal.t('The tab dropdown list opens.')); + + // Verify the number of devices in the list. + var devices = drupalSettings.responsive_preview.devices; + var count = 0; + for (var device in devices) { + if (devices.hasOwnProperty(device)) { + count++; + } + } + var $devices = $tab.find('.options .device'); + QUnit.equal($devices.length, count, Drupal.t('The correct number of devices are listed.')); + }; + }, + previewLaunch: function ($, Drupal, window, document, undefined) { + return function () { + QUnit.expect(3); + + // Find the toolbar tab. + var $tab = $('.toolbar .toolbar-tab-responsive-preview'); + // Verify that the responsive preview container is not been built yet. + var $container = $('#responsive-preview'); + QUnit.equal($container.length, 0, Drupal.t('The preview container does not exist yet.')); + + // Verify that clicking a device link activates the preview container. + $tab.find('.options .device').first().trigger('click'); + QUnit.stop(); + window.setTimeout(function () { + $container = $('#responsive-preview'); + // Verify that the preview container exists. + QUnit.equal($container.length, 1, Drupal.t('The preview container exists.')); + + // Verify that preview container is active. + QUnit.ok($container.hasClass('active'), Drupal.t('The preview container is active.')); + QUnit.start(); + }, 500); + }; + } + } + }; +})(jQuery, Drupal, drupalSettings, this, this.document); -- 1.7.10.4