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 index dd4f7a0..3575161 100644 --- a/core/modules/responsive_preview/config/responsive_preview.devices.yml +++ b/core/modules/responsive_preview/config/responsive_preview.devices.yml @@ -1,61 +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 - iphone3gs: - label: iPhone 3GS - dimensions: - width: 320 - height: 480 - ipadmini: - label: iPad mini - dimensions: - width: 768 - height: 1024 - ipad4: - label: iPad 3/4 + dppx: 2 + ipad: + label: iPad dimensions: width: 1536 height: 2048 - ipad2: - label: iPad 2 + dppx: 2 + nexus4: + label: Nexus 4 dimensions: width: 768 - height: 1024 - kindlefire: - label: Kindle Fire - dimensions: - width: 600 - height: 1024 - kindlefirehd: - label: Kindle Fire HD - dimensions: - width: 800 height: 1280 - nooktablet: - label: Nook Tablet - dimensions: - width: 600 - height: 1024 + dppx: 2 nexus7: label: Nexus 7 dimensions: width: 800 height: 1280 - surface: - label: Microsoft Surface - dimensions: - width: 768 - height: 1366 + 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.css b/core/modules/responsive_preview/css/responsive-preview.base.css index 6586f57..0efa751 100644 --- a/core/modules/responsive_preview/css/responsive-preview.base.css +++ b/core/modules/responsive_preview/css/responsive-preview.base.css @@ -30,6 +30,9 @@ float: right; /* LTR */ } } +.responsive-preview-toolbar-tab .trigger { + display: block; +} .responsive-preview-toolbar-tab .responsive-preview-options { display: none; z-index: 1; @@ -70,7 +73,7 @@ left: 0; position: fixed; right: 0; - top: 3em; + top: 0; width: 100%; z-index: 1; } @@ -83,6 +86,10 @@ 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; } /** diff --git a/core/modules/responsive_preview/css/responsive-preview.theme.css b/core/modules/responsive_preview/css/responsive-preview.theme.css index 3dce264..e0c537f 100644 --- a/core/modules/responsive_preview/css/responsive-preview.theme.css +++ b/core/modules/responsive_preview/css/responsive-preview.theme.css @@ -93,6 +93,13 @@ 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. @@ -100,6 +107,12 @@ #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; @@ -123,4 +136,6 @@ .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/js/responsive-preview.js b/core/modules/responsive_preview/js/responsive-preview.js index a58d040..406dc53 100644 --- a/core/modules/responsive_preview/js/responsive-preview.js +++ b/core/modules/responsive_preview/js/responsive-preview.js @@ -4,72 +4,143 @@ * Provides a component that previews the a page in various device dimensions. */ -(function ($, Drupal) { - - "use strict"; - - Drupal.responsivePreview = Drupal.responsivePreview || {}; - - var $toolbarTab = $(); - var $container; // The container of the page preview component. - var $frame; // The iframe that contains the previewed page. - var iframeDocument; // The document of the iframe that contains the preview. - var size; // The width of the iframe container. - var offset; // The left value of the iframe container. - var device = { - width: null, // The width of the device to preview. - height: null // The height of the device to preview. - }; - var edgeTolerance = 60; - // Take RTL text direction into account. - var dir = document.getElementsByTagName('html')[0].getAttribute('dir'); - var parentWindow; +(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' + }, /** - * Attaches behaviors to the toolbar tab and preview containers. + * Implements Backbone Views' initialize(). */ - Drupal.behaviors.responsivePreview = { - attach: function (context, settings) { - // 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'); - // Retain a reference to the parent window. - parentWindow = window; - // Assign behaviors to the toolbar tab. - $toolbarTab = $('.responsive-preview-toolbar-tab') - .on('click.responsivePreview', toggleConfigurationOptions) - .on('click.responsivePreview', '#responsive-preview', toggleConfigurationOptions) - .on('mouseleave.responsivePreview', {open: false}, toggleConfigurationOptions) - .on('click.responsivePreview', '.responsive-preview-options .responsive-preview-device', {open: false}, toggleConfigurationOptions) - .on('click.responsivePreview', '.responsive-preview-device', loadDevicePreview); - // Hide layout options that are wider than the current screen - prunePreviewChoices.call($toolbarTab.find('.responsive-preview-device'), edgeTolerance); - // Register a handler on window resize to reposition the tab dropdown. - $(window) - .on('resize.responsivePreview.tab', handleWindowToolbarResize); - } - // 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'; - } - } + 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. @@ -77,147 +148,164 @@ * @param Object event * jQuery Event object. */ - function toggleConfigurationOptions (event) { + _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(); - var open = (event.data && typeof event.data.open === 'boolean') ? event.data.open : undefined; - var $list = $toolbarTab - // Set an open class on the tab wrapper. - .toggleClass('open', open) - .find('.responsive-preview-options'); - // The list of options might render outside the window. - correctEdgeCollisions.call($list); - } + }, /** - * Toggles the layout preview component on or off. - * - * When first toggled on, the layout preview component is built. All - * subsequent toggles hide or show the built component. - * - * @param Object event - * jQuery Event object. - * - * @param Boolean activate - * A boolean that forces the preview to show (true) or to hide (false). + * Corrects element window edge collisions. */ - function toggleLayoutPreview (event, activate) { - event.preventDefault(); - // Build the preview if it doesn't exist. - if (!$container) { - buildpreview(); - } - $toolbarTab - .find('> button') - .toggleClass('active', activate); - $container - .toggleClass('active', activate); - $('body') - .toggleClass('responsive-preview-active', activate); - } + _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' + }); + }, /** - * Assembles a layout preview. + * Hides device preview options that are too wide for the current window. */ - function buildpreview () { - $(parentWindow.document.body).once('responsive-preview-container', function (index, element) { - $container = $(Drupal.theme('layoutContainer')); + _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); + }, - // Add a close button. - $container - .append(Drupal.theme('layoutClose')); + /** + * 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); - // Attach the iframe that will hold the preview. - $frame = $(Drupal.theme('layoutFrame')) - .css({ width: size }) - .appendTo($container); + event.preventDefault(); + }, - // Append the container to the window. - $container.appendTo(parentWindow.document.body); - // Displace the top of the container. - $container - .css({ top: getDisplacement('top') }) - .attr('data-offset-top', getDisplacement('top')); + /** + * Handles refreshing the layout toolbar tab on screen resize. + * + * @param Object event + * jQuery Event object. + */ + _handleWindowToolbarResize: function (event) { + this._correctEdgeCollisions(); + this._prunePreviewChoices(); + } - // The contentDocument property is not supported in IE until IE8. - iframeDocument = $frame[0].contentDocument || $frame[0].contentWindow.document; +}); - $container - .on('click.responsivePreview', '#responsive-preview-close', {activate: false}, toggleLayoutPreview) - .on('sizeUpdate.responsivePreview', refreshPreviewSizing); +/** + * Handles the responsive preview element interactions. + */ +Drupal.responsivePreview.views.ResponsivePreviewView = Backbone.View.extend({ + events: { + 'click #responsive-preview-close': 'remove' + }, - // Trigger a resize to kick off some initial placements. - $(parentWindow) - .on('resize.responsivePreview', updateDimensions) - .trigger('resize.responsivePreview'); + /** + * Implements Backbone Views' initialize(). + */ + initialize: function () { + this.model.on('change:isActive', this.render, this); + this.model.on('change:dimensions', this.render, this); - // Load the current page URI into the preview iframe. - iframeDocument.location.href = Drupal.encodePath(Drupal.settings.currentPath); - }); - } + // 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)); + }, /** - * Updates the dimension variables of the preview components. - * - * @param Object dimensions - * An object with the following properties: - * - Number width: The width the preview should be set to. - * - Number height (optional): The height the preview should be set to. - * - * @todo dimensions.height is not yet being used. + * Implements Backbone Views' render(). */ - function updateDimensions () { - var width = device.width || NaN; - var height = device.height || NaN; - var max = document.documentElement.clientWidth; - var gutterPercent = (1 - (width / max)) / 2; - var left = gutterPercent * max; - // Set the left offset of the frame. - // The gutters must be at least the width of the edgeTolerance - left = (left < edgeTolerance) ? edgeTolerance : left; - // The frame width must fit within the difference of the gutters and the - // page width. - width = (max - (left * 2) < width) ? max - (left * 2) : width; - // Set the dimension variables in the closure. - offset = left; - size = width; - // Trigger a dimension change. - $container.trigger('sizeUpdate.responsivePreview'); - } + 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; + }, /** - * Handles refreshing the layout toolbar tab positioning. - * - * @param Object event - * jQuery Event object. + * Implements Backbone Views' remove(). */ - function handleWindowToolbarResize (event) { - var $list = $toolbarTab.find('.responsive-preview-options'); - // Move the list back onto the screen. - correctEdgeCollisions.call($list); - // Update the display of the option items. - var $options = $list.find('.responsive-preview-device'); - // Hide layout options that are wider than the current screen - prunePreviewChoices.call($options, edgeTolerance); - // Hide the tab if no options are visible. Show the tab if at least one is. - $toolbarTab.toggle($options.parent('li').not('.element-hidden').length > 0); - } + remove: function () { + // Inactivate the previewer. + this.model.set('isActive', false); + // Allow other scripts to respond to responsive preview events. + $(document).trigger('drupalResponsivePreviewStopped'); + }, /** - * Resizes the preview iframe to the configured dimensions of a device. - * - * @param Object event - * A jQuery event object. + * Builds the iframe preview. */ - function loadDevicePreview (event) { - event.preventDefault(); - var $link = $(event.target); - device.width = $link.data('responsive-preview-width'); - device.height = $link.data('responsive-preview-height'); - // Toggle the preview on. - toggleLayoutPreview(event, true); - updateDimensions(); - } + _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. @@ -225,22 +313,162 @@ * @param Object event * A jQuery event object. */ - function refreshPreviewSizing (event) { + _refresh: function (event) { + var $frame = this.$el.find('#responsive-preview'); + var dir = this.model.get('dir'); var edge = (dir === 'rtl') ? 'right' : 'left'; - var options = { - width: size - }; - options[edge] = offset; + 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(options, 'fast'); - // Reposition the close button. - $('#responsive-preview-close') - .css(edge, offset + size); - } + .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); + } + }, /** - * Get the total displacement of given region. + * Gets the total displacement of given region. * * @param String region * Region name. Either "top" or "bottom". @@ -248,7 +476,7 @@ * @return Number * The total displacement of given region in pixels. */ - function getDisplacement (region) { + _getDisplacement: function (region) { var displacement = 0; var lastDisplaced = $('[data-offset-' + region + ']'); if (lastDisplaced.length) { @@ -256,86 +484,41 @@ } return displacement; } +}); +/** + * Registers theme templates with Drupal.theme(). + */ +$.extend(Drupal.theme, { /** - * Corrects element window edge collisions. + * Theme function for the preview container element. * - * Elements are moved back into the window if part of the element is - * rendered outside the visible window. - * - * This should be invoked against a jQuery set like this: - * correctEdgeCollisions.call($('div')); + * @return + * The corresponding HTML. */ - function correctEdgeCollisions () { - // The position of the dropdown depends on the language direction. - var edge = (dir === 'rtl') ? 'left' : 'right'; - // Go through each element and correct edge collisions. - return this.each(function (index, element) { - $(this) - // Invoke jQuery UI position on the device options. - .position({ - 'my': edge +' top', - 'at': edge + ' bottom', - 'of': $toolbarTab, - 'collision': 'flip fit' - }); - }); - } + layoutContainer: function () { + return '
'; + }, /** - * Hides device preview options that are too wide for the current window. + * Theme function for the close button for the preview container. * - * This should be invoked against a jQuery set like this: - * prunePreviewChoices.call($('div'), 20); - * - * @param Number tolerance - * The distance from the edge of the window that a device cannot exceed - * or it will be pruned from the list. + * @return + * The corresponding HTML. */ - function prunePreviewChoices (tolerance) { - var docWidth = document.documentElement.clientWidth; - tolerance = (typeof tolerance === 'number' && tolerance > 0) ? tolerance : 0; - return this.each(function () { - var $this = $(this); - var width = parseInt($this.data('responsive-preview-width'), 10); - var fits = ((width + (tolerance * 2)) < docWidth); - $this.parent('li').toggleClass('element-hidden', !fits); - }); - } + layoutClose: function () { + return ''; + }, /** - * Registers theme templates with Drupal.theme(). + * Theme function for a responsive preview iframe element. + * + * @return + * The corresponding HTML. */ - $.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 ''; - } - }); + layoutFrame: function (url) { + return ''; + } +}); -}(jQuery, Drupal)); +}(jQuery, Backbone, Drupal, window, document)); diff --git a/core/modules/responsive_preview/responsive_preview.module b/core/modules/responsive_preview/responsive_preview.module index 43abbeb..23729d5 100644 --- a/core/modules/responsive_preview/responsive_preview.module +++ b/core/modules/responsive_preview/responsive_preview.module @@ -28,21 +28,19 @@ 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' => '', - 'fragment' => '!', - 'exteranl' => TRUE, 'options' => array( 'fragment' => '!', - 'exteranl' => TRUE, + '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', ), ); } @@ -61,7 +59,6 @@ function responsive_preview_access() { * Implements hook_toolbar(). */ function responsive_preview_toolbar() { - $items['responsive_preview'] = array( '#type' => 'toolbar_item', 'tab' => array( @@ -73,7 +70,7 @@ function responsive_preview_toolbar() { '#value_suffix' => '', '#attributes' => array( 'id' => 'responsive-preview', - 'title' => "Preview page layout", + 'title' => t('Preview page layout'), 'class' => array('icon', 'icon-responsive-preview', 'trigger'), ), ), @@ -86,6 +83,7 @@ function responsive_preview_toolbar() { ), ), '#wrapper_attributes' => array( + 'id' => 'toolbar-tab-responsive-preview', 'class' => array('responsive-preview-toolbar-tab'), ), '#attached' => array( @@ -104,7 +102,6 @@ function responsive_preview_toolbar() { * Implements hook_library(). */ function responsive_preview_library_info() { - $libraries = array(); $path = drupal_get_path('module', 'responsive_preview'); $options = array( 'scope' => 'footer', @@ -113,7 +110,6 @@ function responsive_preview_library_info() { $libraries['responsive-preview'] = array( 'title' => 'Preview layouts', - 'website' => 'http://drupal.org/project/responsive_preview', 'version' => VERSION, 'css' => array( $path . '/css/responsive-preview.base.css', @@ -125,6 +121,7 @@ function responsive_preview_library_info() { 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), + array('system', 'backbone'), array('system', 'jquery.ui.position'), ), );