From 8dcb91b91f448c1ac80a701ac209f9c681b4b1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Sun, 17 Feb 2013 01:32:04 -0500 Subject: [PATCH] Issue #1741498 by jessebeach, Wim Leers: Add a mobile preview bar to Drupal core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: J. Renée Beach --- core/modules/contextual/contextual.toolbar.js | 9 +- .../config/responsive_preview.devices.yml | 42 ++ .../css/responsive-preview.base-rtl.css | 38 ++ .../css/responsive-preview.base.css | 110 ++++ .../css/responsive-preview.theme-rtl.css | 36 ++ .../css/responsive-preview.theme.css | 141 ++++++ core/modules/responsive_preview/images/close.png | 3 + .../images/icon-responsive-preview-active.png | 3 + .../images/icon-responsive-preview.png | 4 + .../responsive_preview/js/responsive-preview.js | 524 ++++++++++++++++++++ .../responsive_preview/responsive_preview.info | 5 + .../responsive_preview/responsive_preview.module | 130 +++++ 12 files changed, 1044 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.theme-rtl.css create mode 100644 core/modules/responsive_preview/css/responsive-preview.theme.css create mode 100644 core/modules/responsive_preview/images/close.png create mode 100644 core/modules/responsive_preview/images/icon-responsive-preview-active.png create mode 100644 core/modules/responsive_preview/images/icon-responsive-preview.png create mode 100644 core/modules/responsive_preview/js/responsive-preview.js create mode 100644 core/modules/responsive_preview/responsive_preview.info create mode 100644 core/modules/responsive_preview/responsive_preview.module diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js index 45f9757..ada9f11 100644 --- a/core/modules/contextual/contextual.toolbar.js +++ b/core/modules/contextual/contextual.toolbar.js @@ -27,13 +27,20 @@ Drupal.behaviors.contextualToolbar = { model: model }); - // Update the model based on overlay events. $(document) + // Update the model based on Overlay events. .on('drupalOverlayOpen.contextualToolbar', function () { model.set('isVisible', false); }) .on('drupalOverlayClose.contextualToolbar', function () { model.set('isVisible', true); + }) + // Update the model based on Responsive Preview events. + .on('drupalResponsivePreviewStarted.contextualToolbar', function () { + model.set('isVisible', false); + }) + .on('drupalResponsivePreviewStopped.contextualToolbar', function () { + model.set('isVisible', true); }); // Update the model to show the edit tab if there's >=1 contextual link. diff --git a/core/modules/responsive_preview/config/responsive_preview.devices.yml b/core/modules/responsive_preview/config/responsive_preview.devices.yml new file mode 100644 index 0000000..3575161 --- /dev/null +++ b/core/modules/responsive_preview/config/responsive_preview.devices.yml @@ -0,0 +1,42 @@ +# 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 + +devices: + iphone: + label: iPhone 5 + dimensions: + width: 640 + height: 1136 + dppx: 2 + iphone4: + label: iPhone 4 + dimensions: + width: 640 + height: 960 + dppx: 2 + ipad: + label: iPad + dimensions: + width: 1536 + height: 2048 + dppx: 2 + nexus4: + label: Nexus 4 + dimensions: + width: 768 + height: 1280 + dppx: 2 + nexus7: + label: Nexus 7 + dimensions: + width: 800 + height: 1280 + dppx: 1.3 + desktop: + label: Typical desktop + dimensions: + width: 1366 + height: 768 + dppx: 1 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..eaf2504 --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.base-rtl.css @@ -0,0 +1,38 @@ +/** + * @file + * RTL base styling for responsive preview. + */ + +/** + * Toolbar tab. + */ + +/* At narrow screen widths, float the tab to the right so it falls in line with + * the rest of the toolbar tabs. */ +.js .toolbar .bar .responsive-preview-toolbar-tab.tab { + float: right; +} +/* At wide widths, float the tab to the left. */ +@media only screen and (min-width: 36em) { + .js .toolbar .bar .responsive-preview-toolbar-tab.tab { + float: left; + } +} +.responsive-preview-toolbar-tab .responsive-preview-options { + left: 0.3em; + right: auto; +} + +/** + * Preview container. + * + * The container is kept offscreen after it is built and has been disabled. + */ +#responsive-preview-container { + left: auto; + right: -200%; +} +#responsive-preview-container.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..0efa751 --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.base.css @@ -0,0 +1,110 @@ +/** + * @file + * Base styling for responsive preview. + */ + +/** + * Constrain the window height to the client height when the preview is active. + */ +.responsive-preview-active { + height: 100%; + overflow: hidden; +} + +/** + * Toolbar tab. + */ +.responsive-preview-toolbar-tab { + display: none; +} +/* At narrow screen widths, float the tab to the left so it falls in line with + * the rest of the toolbar tabs. */ +.js .toolbar .bar .responsive-preview-toolbar-tab.tab { + display: block; + float: left; /* LTR */ + position: relative; +} +/* At wide widths, float the tab to the right. */ +@media only screen and (min-width: 36em) { + .js .toolbar .bar .responsive-preview-toolbar-tab.tab { + float: right; /* LTR */ + } +} +.responsive-preview-toolbar-tab .trigger { + display: block; +} +.responsive-preview-toolbar-tab .responsive-preview-options { + display: none; + z-index: 1; +} +.responsive-preview-toolbar-tab.open .responsive-preview-options { + display: block; +} +.js .responsive-preview-toolbar-tab.tab .responsive-preview-options li { + float: none; +} + +/** + * Preview container. + * + * The container is kept offscreen after it is built and has been disabled. + */ +#responsive-preview-container { + bottom: 0; + display: none; + height: 100%; + left: -200%; /* LTR */ + position: fixed; + top: 0; + width: 100%; + z-index: 1050; +} +#responsive-preview-container.active { + display: block; + left: 0; /* LTR */ +} +#responsive-preview-close { + position: absolute; + z-index: 75; +} +.responsive-preview-modal-background { + bottom: 0; + height: 100%; + left: 0; + position: fixed; + right: 0; + top: 0; + width: 100%; + z-index: 1; +} + +/** + * Preview iframe. + */ +#responsive-preview-container iframe { + height: 100%; + position: relative; + width: 100%; + z-index: 100; + -webkit-transition: all 150ms ease-out; + -moz-transition: all 150ms ease-out; + -o-transition: all 150ms ease-out; + transition: all 150ms ease-out; +} + +/** + * 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-rtl.css b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css new file mode 100644 index 0000000..8add01e --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.theme-rtl.css @@ -0,0 +1,36 @@ +/** + * @file + * RTL styling for responsive preview. + */ + +/** + * Toolbar tab. + */ + +/* Toolbar icon. */ +.toolbar .bar .responsive-preview-toolbar-tab .icon-responsive-preview:before { + left: auto; + right: 1em; +} + +/* Toolbar tab triangle toggle. */ +.responsive-preview-toolbar-tab .trigger:after { + left: 1em; + right: auto; +} +.responsive-preview-toolbar-tab.open:before { + left: 0; + right: auto; +} +.responsive-preview-toolbar-tab.open .trigger:after { + left: 0.7em; + right: auto; +} + +/** + * Preview container. + */ +#responsive-preview-close { + margin-left: 0; + margin-right: 10px; +} 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..e0c537f --- /dev/null +++ b/core/modules/responsive_preview/css/responsive-preview.theme.css @@ -0,0 +1,141 @@ +/** + * @file + * Styling for responsive preview. + */ + +/** + * Toolbar tab. + */ +.responsive-preview-toolbar-tab .responsive-preview-options { + background-color: #0f0f0f; +} +/* Toolbar icon. */ +.toolbar .bar .icon.icon-responsive-preview { + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + width: 5em; +} +.icon-responsive-preview:before { + background-image: url("../images/icon-responsive-preview.png"); +} +.toolbar .bar .responsive-preview-toolbar-tab .icon-responsive-preview:before { + left: 1em; /* LTR */ +} +.responsive-preview-toolbar-tab.open .icon-responsive-preview:before, +.responsive-preview-toolbar-tab .icon-responsive-preview.active:before { + background-image: url("../images/icon-responsive-preview-active.png"); +} +@media only screen and (min-width: 16.5em) { + .toolbar .responsive-preview-toolbar-tab.tab .icon-responsive-preview:before { + width: 20px; + } +} +/* Device preview options. */ +.responsive-preview-toolbar-tab .responsive-preview-options { + box-shadow: 0 0.8em 2.5em -0.8em rgba(0, 0, 0, 0.75); + position: absolute; + white-space: nowrap; +} +.responsive-preview-toolbar-tab .responsive-preview-options li { + background-color: white; +} +.responsive-preview-toolbar-tab .trigger { + height: 3em; +} +.responsive-preview-toolbar-tab .trigger, +.responsive-preview-toolbar-tab .responsive-preview-options a { + padding-bottom: 1em; + padding-top: 1em; +} +.toolbar .responsive-preview-toolbar-tab.tab .responsive-preview-options a { + color: #777; +} +.toolbar .responsive-preview-toolbar-tab.tab .responsive-preview-options a:hover { + color: black; +} +/* Toolbar tab triangle toggle. */ +.responsive-preview-toolbar-tab .trigger:after { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 0.4545em 0.4em 0; + color: #a0a0a0; + content: ' '; + display: block; + height: 0; + line-height: 0; + overflow: hidden; + position: absolute; + right: 1em; /* LTR */ + top: 50%; + margin-top: -0.1666em; + width: 0; + z-index: 1 +} +.responsive-preview-toolbar-tab.open:before { + background-color: white; + bottom: 0; + content: ' '; + display: block; + position: absolute; + right: 0; /* LTR */ + top: 0; + width: 2em; + z-index: 1; +} +.responsive-preview-toolbar-tab.open .trigger:after { + border-bottom: 0.4545em solid; + border-top-color: transparent; + color: black; + right: 0.7em; /* LTR */ + top: 1.25em; +} +.responsive-preview-toolbar-tab .trigger.active:after { + color: black; +} +.responsive-preview-toolbar-tab .trigger.active { + 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%); +} + +/** + * Preview container. + */ +#responsive-preview-container { + box-shadow: 0 0 10px 0 black; +} +#responsive-preview-container iframe { + box-shadow: + 0 0 0 2px black, + 0 0 0 3px rgba(80,80,80,1); + top: 10px; +} +#responsive-preview-close { + background-attachment: scroll; + background-color: #a0a0a0; + background-image: url("../images/close.png"); + background-image: url("../images/close.png"), -webkit-linear-gradient(transparent, #787878 150%); + background-image: url("../images/close.png"), linear-gradient(transparent, #787878 150%); + background-position: center center; + background-repeat: no-repeat; + border: none; + border-radius: 3px; + cursor: pointer; + font-size: 1em; + height: 2.333em; + margin-left: 10px; /* LTR */ + margin-top: 9px; + width: 2.333em; +} +#responsive-preview-close:hover { + background-image: url("../images/close.png"); +} +.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%); +} diff --git a/core/modules/responsive_preview/images/close.png b/core/modules/responsive_preview/images/close.png new file mode 100644 index 0000000..538518e --- /dev/null +++ b/core/modules/responsive_preview/images/close.png @@ -0,0 +1,3 @@ +PNG + + IHDR7tEXtSoftwareAdobe ImageReadyqe<IDAT(SeKKBAx_iWjOhBREࣝ{E. BpEĽuy!h sVYkiֶ^pđ#^j[RPZaC̄>5Vdr2Q\mF*ToBKگ tJr?wϋ||PqkUݞ,/s_n ^z({!-sSS3oRlYgE/S 9d,H+_ϲ5}PrgY{ST⫑V k{,p2D*kfj5Nd2Z>%o=$4֘5FhhfPnoҗ1_M|6]!mf-!K}H2N|S \*6BQ1 #(&INʕr;Sd #Z IENDB` \ No newline at end of file diff --git a/core/modules/responsive_preview/images/icon-responsive-preview-active.png b/core/modules/responsive_preview/images/icon-responsive-preview-active.png new file mode 100644 index 0000000..f6e8a16 --- /dev/null +++ b/core/modules/responsive_preview/images/icon-responsive-preview-active.png @@ -0,0 +1,3 @@ +PNG + + IHDR $cIDAT8c L@HL0#! W?&m"mр0?|?! ?bG(!)dF,~(.teIENDB` \ No newline at end of file diff --git a/core/modules/responsive_preview/images/icon-responsive-preview.png b/core/modules/responsive_preview/images/icon-responsive-preview.png new file mode 100644 index 0000000..3e22cb8 --- /dev/null +++ b/core/modules/responsive_preview/images/icon-responsive-preview.png @@ -0,0 +1,4 @@ +PNG + + IHDR $pIDAT8cZx O? f 380L-\1 L %ϦQM߿@S;#ƐD +iڈEBCituJq4QIENDB` \ 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..406dc53 --- /dev/null +++ b/core/modules/responsive_preview/js/responsive-preview.js @@ -0,0 +1,524 @@ +/** + * @file + * + * Provides a component that previews the a page in various device dimensions. + */ + +(function ($, Backbone, Drupal, window, document) { + +"use strict"; + +/** + * Attaches behaviors to the toolbar tab and preview containers. + */ +Drupal.behaviors.responsivePreview = { + attach: function (context, settings) { + settings = settings || {}; + settings.responsivePreview = settings.responsivePreview || {}; + // 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'); + // Build the model and views. + var model = new Drupal.responsivePreview.models.StateModel(); + // Retain a reference to the parent window. + model.set({ + 'parentWindow': window, + 'dir': document.getElementsByTagName('html')[0].getAttribute('dir'), + 'edgeTolerance': settings.responsivePreview.gutter || 60 + }); + // The toolbar tab view. + var tabView = new Drupal.responsivePreview.views.TabView({ + el: $(context).find('#toolbar-tab-responsive-preview'), + model: model + }); + // The preview container view. + var previewView = new Drupal.responsivePreview.views.ResponsivePreviewView({ + el: Drupal.theme('layoutContainer'), + model: model + }); + } + // 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'; + } + } + } +}; + +Drupal.responsivePreview = Drupal.responsivePreview || {models: {}, views: {}}; + +/** + * Backbone Model for the Responsive Preview. + */ +Drupal.responsivePreview.models.StateModel = Backbone.Model.extend({ + defaults: { + // The state of the preview. + isActive: false, + // The state of toolbar list if available device previews. + isDeviceListOpen: false, + // The window that contains the body to which the preview container is + // attached. + parentWindow: null, + // The number of devices that can be previewed at the current page width. + optionsCount: 0, + 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 + }, + // Take RTL text direction into account. + dir: 'ltr', + // The gutter size around the preview frame. + edgeTolerance: 0 + } +}); + +/** + * Handles responsive preview toggle interactions. + */ +Drupal.responsivePreview.views.TabView = Backbone.View.extend({ + events: { + 'click': '_toggleConfigurationOptions', + 'mouseleave': '_toggleConfigurationOptions', + 'click .responsive-preview-device': '_updateDeviceDetails' + }, + + /** + * Implements Backbone Views' initialize(). + */ + initialize: function () { + this.model.on('change:isActive', this.render, this); + this.model.on('change:isDeviceListOpen', this.render, this); + this.model.on('change:optionsCount', this.render, this); + // Respond to window resizes. + $(this.model.get('parentWindow')) + .on('resize.responsivePreview.TabView', Drupal.debounce($.proxy(this._handleWindowToolbarResize, this), 250)) + // Trigger a resize to kick off some initial placements. + .trigger('resize.responsivePreview.TabView'); + }, + + /** + * Implements Backbone Views' render(). + */ + render: function () { + // Render the state. + var isActive = this.model.get('isActive'); + var isDeviceListOpen = this.model.get('isDeviceListOpen'); + // Toggle the display of the toolbar tab. + this.$el + .find('> button') + .toggleClass('active', isActive) + .attr('aria-pressed', isActive); + // When the preview is active, a class on the body is necessary to impose + // styling to aid in the display of the preview element. + $('body').toggleClass('responsive-preview-active', isActive); + // Toggle the display of the device list. + this.$el.toggleClass('open', isDeviceListOpen); + // The list of options might render outside the window. + if (isDeviceListOpen) { + this._correctEdgeCollisions(); + } + // Hide the tab if no options are visible. Show the tab if at least one is. + this.$el.toggle(this.model.get('optionsCount') > 0); + + return this; + }, + + /** + * Toggles the list of devices available to preview from the toolbar tab. + * + * @param Object event + * jQuery Event object. + */ + _toggleConfigurationOptions: function (event) { + // Force the options list closed on mouseleave. + var open = (event.type === 'mouseleave') ? false : !this.model.get('isDeviceListOpen'); + this.model.set('isDeviceListOpen', open); + + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Corrects element window edge collisions. + */ + _correctEdgeCollisions: function () { + // The position of the dropdown depends on the language direction. + var dir = this.model.get('dir'); + var edge = (dir === 'rtl') ? 'left' : 'right'; + // Correct edge collisions. + this.$el.find('.responsive-preview-options') + // Invoke jQuery UI position on the device options. + .position({ + 'my': edge +' top', + 'at': edge + ' bottom', + 'of': this.$el, + 'collision': 'flip fit' + }); + }, + + /** + * Hides device preview options that are too wide for the current window. + */ + _prunePreviewChoices: function () { + var $options = this.$el.find('.responsive-preview-device') + var tolerance = this.model.get('edgeTolerance'); + var docWidth = document.documentElement.clientWidth; + // Remove choices that are too large for the current screen. + $options.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 iframeWidth = width / dppx; + var fits = ((iframeWidth + (tolerance * 2)) < docWidth); + $this.parent('li').toggleClass('element-hidden', !fits); + }); + // Set the number of device options that are available. + this.model.set('optionsCount', $options.parent('li').not('.element-hidden').length); + }, + + /** + * Updates the model to reflect the dimensions of the chosen device. + * + * @param Object event + * A jQuery event object. + */ + _updateDeviceDetails: function (event) { + var $link = $(event.target); + // Update the device dimensions. + this.model.set('dimensions', { + 'width': parseInt($link.data('responsive-preview-width'), 10), + 'height': parseInt($link.data('responsive-preview-height'), 10), + 'dppx': parseFloat($link.data('responsive-preview-dppx'), 10) + }); + // Toggle the preview on. + this.model.set('isActive', true); + + event.preventDefault(); + }, + + /** + * Handles refreshing the layout toolbar tab on screen resize. + * + * @param Object event + * jQuery Event object. + */ + _handleWindowToolbarResize: function (event) { + this._correctEdgeCollisions(); + this._prunePreviewChoices(); + } + +}); + +/** + * Handles the responsive preview element interactions. + */ +Drupal.responsivePreview.views.ResponsivePreviewView = Backbone.View.extend({ + events: { + 'click #responsive-preview-close': 'remove' + }, + + /** + * Implements Backbone Views' initialize(). + */ + initialize: function () { + this.model.on('change:isActive', this.render, this); + this.model.on('change:dimensions', this.render, this); + + // Recalculate the size of the preview container when the window resizes. + $(this.model.get('parentWindow')) + .on('resize.responsivePreview.ResponsivePreviewView', Drupal.debounce($.proxy(this.render, this), 250)); + }, + + /** + * Implements Backbone Views' render(). + */ + render: function () { + // Render the state. + var isActive = this.model.get('isActive'); + // Build the preview if it doesn't exist. + if (!this.$el.hasClass('processed')) { + this._build(); + } + // Mark the preview element active. + this.$el.toggleClass('active', isActive); + // Refresh the dimensions of the preview container. + if (isActive) { + // Allow other scripts to respond to responsive preview events. + $(document).trigger('drupalResponsivePreviewStarted'); + this._refresh(); + } + + return this; + }, + + /** + * Implements Backbone Views' remove(). + */ + remove: function () { + // Inactivate the previewer. + this.model.set('isActive', false); + // Allow other scripts to respond to responsive preview events. + $(document).trigger('drupalResponsivePreviewStopped'); + }, + + /** + * Builds the iframe preview. + */ + _build: function () { + this.$el.append(Drupal.theme('layoutClose')); + // Attach the iframe that will hold the preview. + var $frame = $(Drupal.theme('layoutFrame')) + .attr('data-loading', true) + .appendTo(this.$el); + // Displace the top of the container. + this.$el + .css({ top: this._getDisplacement('top') }) + .attr('data-offset-top', this._getDisplacement('top')) + // Append the container to the body to initialize the iframe document. + .appendTo(this.model.get('parentWindow').document.body); + // The contentDocument property is not supported in IE until IE8. + var iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document; + // Load the current page URI into the preview iframe. + var path = Drupal.settings.currentPath; + if (path.charAt(0) !== '/') { + path = '/' + path; + } + iframeDocument.location.href = Drupal.encodePath(path); + // Mark the preview element processed. + this.$el.addClass('processed'); + }, + + /** + * Redraws the layout preview component based on the stored dimensions. + * + * @param Object event + * A jQuery event object. + */ + _refresh: function (event) { + var $frame = this.$el.find('#responsive-preview'); + var dir = this.model.get('dir'); + var edge = (dir === 'rtl') ? 'right' : 'left'; + var dimensions = this.model.get('dimensions'); + var tolerance = this.model.get('edgeTolerance'); + var max = document.documentElement.clientWidth; + var width = dimensions.width / dimensions.dppx; + var height = dimensions.height / dimensions.dppx; + var gutterPercent = (1 - (width / max)) / 2; + var offset = gutterPercent * max; + // Set the offset of the frame. + // The gutters must be at least the width of the tolerance. + offset = (offset < tolerance) ? tolerance : offset; + // The frame width must fit within the difference of the gutters and the + // page width. + width = (max - (offset * 2) < width) ? max - (offset * 2) : width; + // Position the iframe. + $frame + .stop(true, true) + .animate({ + width: width, + height: height + }, 'fast'); + var options = {}; + // The edge depends on the text direction. + options[edge] = offset; + $frame.css(options); + // Position the close button. + this.$el.find('#responsive-preview-close').css(edge, offset + width); + // Calculate the CSS properties to scale the preview appropriately. + var scalingCSS = this._calculateScaling(); + // The first time we need to apply scaling magic, we must wait until the + // frame has loaded. + var view = this; + $frame.on('load.responsivePreview.ResponsivePreviewView', function() { + $frame.removeAttr('data-loading'); + view._applyScaling(scalingCSS); + }); + // We don't have to wait for the frame to load anymore. + if ($frame.attr('data-loading') === undefined) { + this._applyScaling(scalingCSS); + } + }, + + /** + * Determines device scaling ratios from the viewport information. + */ + _calculateScaling: function () { + var settings = {}; + + // Parse , if any. + var $viewportMeta = $(document).find('meta[name=viewport][content]'); + if ($viewportMeta.length > 0) { + // Parse something like this: + // + // into this: + // { + // width: 'device-width', + // initial-scale: '1', + // maximum-scale: '5', + // minimum-scale: '1', + // user-scalable: 'yes' + // } + $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]; + }); + } + + var pageWidth; + if (settings.width) { + if (settings.width === 'device-width') { + // Don't scale if the page is marked to be as wide as the device. + return {}; + } + else { + pageWidth = parseInt(settings.width, 10); + } + } + else { + // Viewport default width of iPhone. + pageWidth = 980; + } + + var pageHeight = undefined; + if (settings.height && settings.height !== 'device-height') { + pageHeight = parseInt(settings.height, 10); + } + + var initialScale = 1; + if (settings['initial-scale']) { + initialScale = parseFloat(settings['initial-scale'], 10); + if (initialScale < 1) { + // Viewport default width of iPhone. + pageWidth = 980; + } + } + + // Calculate the scale, ensure it lies in the (0.25, 2) range. + var scale = initialScale * (100 / pageWidth) * (size / 100); + scale = Math.min(scale, 2); + scale = Math.max(scale, 0.25); + + var transform; + var origin; + transform = "scale(" + scale + ")"; + origin = "0 0"; + return { + 'min-width': pageWidth + 'px', + 'min-height': pageHeight + 'px', + '-webkit-transform': transform, + '-ms-transform': transform, + 'transform': transform, + '-webkit-transform-origin': origin, + '-ms-transform-origin': origin, + 'transform-origin': origin + }; + }, + + /** + * Applies scaling in order to bette approximate content display on a device. + */ + _applyScaling: function (scalingCSS) { + var $frame = this.$el.find('#responsive-preview'); + var $html = $($frame[0].contentDocument || $frame[0].contentWindow.document).find('html'); + + 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'); + } + + // Scale if necessary. + $html.css(scalingCSS); + + // When scaling (as we did), 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. + 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); + } + }, + + /** + * 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. + */ + layoutContainer: function () { + return '
'; + }, + + /** + * Theme function for the close button for the preview container. + * + * @return + * The corresponding HTML. + */ + layoutClose: function () { + return ''; + }, + + /** + * Theme function for a responsive preview iframe element. + * + * @return + * The corresponding HTML. + */ + layoutFrame: function (url) { + return ''; + } +}); + +}(jQuery, Backbone, Drupal, window, document)); diff --git a/core/modules/responsive_preview/responsive_preview.info b/core/modules/responsive_preview/responsive_preview.info new file mode 100644 index 0000000..06e86eb --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.info @@ -0,0 +1,5 @@ +name = Responsive Preview +description = Provides a component that previews the 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..23729d5 --- /dev/null +++ b/core/modules/responsive_preview/responsive_preview.module @@ -0,0 +1,130 @@ +' . 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('devices'); + + $links = array(); + foreach($devices as $name => $info) { + $links[$name] = array( + 'title' => $info['label'], + 'href' => '', + 'options' => array( + 'fragment' => '!', + 'external' => TRUE, + ), + 'attributes' => array( + 'class' => array('responsive-preview-device'), + 'data-responsive-preview-width' => ($info['dimensions']['width']) ? $info['dimensions']['width'] : '', + 'data-responsive-preview-height' => ($info['dimensions']['height']) ? $info['dimensions']['height'] : '', + 'data-responsive-preview-dppx' => ($info['dimensions']['dppx']) ? $info['dimensions']['dppx'] : '1', + ), + ); + } + + return $links; +} + +/** + * Prevents the preview tab from rendering on administration pages. + */ +function responsive_preview_access() { + return !path_is_admin(current_path()); +} + +/** + * Implements hook_toolbar(). + */ +function responsive_preview_toolbar() { + $items['responsive_preview'] = array( + '#type' => 'toolbar_item', + 'tab' => array( + 'trigger' => array( + '#theme' => 'html_tag', + '#tag' => 'button', + '#value' => t('Layout preview'), + '#value_prefix' => '', + '#value_suffix' => '', + '#attributes' => array( + 'id' => 'responsive-preview', + 'title' => t('Preview page layout'), + 'class' => array('icon', 'icon-responsive-preview', 'trigger'), + ), + ), + 'device_options' => array( + '#theme' => 'links', + '#links' => responsive_preview_get_devices_list(), + '#attributes' => array( + 'class' => array('responsive-preview-options'), + ), + ), + ), + '#wrapper_attributes' => array( + 'id' => 'toolbar-tab-responsive-preview', + 'class' => array('responsive-preview-toolbar-tab'), + ), + '#attached' => array( + 'library' => array( + array('responsive_preview', 'responsive-preview'), + ), + ), + '#weight' => 200, + '#access' => responsive_preview_access(), + ); + + return $items; +} + +/** + * Implements hook_library(). + */ +function responsive_preview_library_info() { + $path = drupal_get_path('module', 'responsive_preview'); + $options = array( + 'scope' => 'footer', + 'attributes' => array('defer' => TRUE), + ); + + $libraries['responsive-preview'] = array( + 'title' => 'Preview layouts', + 'version' => VERSION, + 'css' => array( + $path . '/css/responsive-preview.base.css', + $path . '/css/responsive-preview.theme.css', + ), + 'js' => array( + $path . '/js/responsive-preview.js' => $options, + ), + 'dependencies' => array( + array('system', 'jquery'), + array('system', 'drupal'), + array('system', 'backbone'), + array('system', 'jquery.ui.position'), + ), + ); + + return $libraries; +} -- 1.7.10.4