diff --git a/core/modules/responsive_preview/js/responsive-preview.js b/core/modules/responsive_preview/js/responsive-preview.js index 16b4b64..b8d60d3 100644 --- a/core/modules/responsive_preview/js/responsive-preview.js +++ b/core/modules/responsive_preview/js/responsive-preview.js @@ -3,23 +3,24 @@ * Provides a component that previews the page in various device dimensions. */ -(function ($, Backbone, Drupal, drupalSettings, window, document) { +(function ($, Backbone, Drupal, drupalSettings) { "use strict"; +var previewModel, tabModel, appView, tabView, blockView, keyboardView; + /** * Attaches behaviors to the toolbar tab and preview containers. */ Drupal.behaviors.responsivePreview = { attach: function (context) { - var defaults = this.defaults; // 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) { - var options = $.extend(defaults, drupalSettings.responsivePreview || {}); + var options = $.extend(this.defaults, drupalSettings.responsivePreview || {}); // 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 @@ -30,13 +31,25 @@ Drupal.behaviors.responsivePreview = { var envModel = new Drupal.responsivePreview.models.EnvironmentModel({ dir: document.getElementsByTagName('html')[0].getAttribute('dir') }); - var tabModel = new Drupal.responsivePreview.models.TabStateModel(); - var previewModel = new Drupal.responsivePreview.models.PreviewStateModel(); + tabModel = new Drupal.responsivePreview.models.TabStateModel(); + previewModel = new Drupal.responsivePreview.models.PreviewStateModel(); + + // Manages the PreviewView. + appView = new Drupal.responsivePreview.views.AppView({ + // The previewView model. + model: previewModel, + envModel: envModel, + // Gutter size around preview frame. + gutter: options.gutter, + // Preview device frame width. + bleed: options.bleed, + strings: options.strings + }); // The toolbar tab view. var $tab = $(context).find('#responsive-preview-toolbar-tab'); if ($tab.length > 0) { - var tabView = new Drupal.responsivePreview.views.TabView({ + tabView = new Drupal.responsivePreview.views.TabView({ el: $tab.get(), model: previewModel, tabModel: tabModel, @@ -50,7 +63,7 @@ Drupal.behaviors.responsivePreview = { // The control block view. var $block = $(context).find('#block-responsive-preview-controls'); if ($block.length > 0) { - var blockView = new Drupal.responsivePreview.views.BlockView({ + blockView = new Drupal.responsivePreview.views.BlockView({ el: $block.get(), model: previewModel, envModel: envModel, @@ -60,16 +73,11 @@ Drupal.behaviors.responsivePreview = { 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 + + // Keyboard controls view. + keyboardView = new Drupal.responsivePreview.views.KeyboardView({ + el: $block.get(), + model: previewModel }); var setViewportWidth = function() { @@ -78,14 +86,14 @@ Drupal.behaviors.responsivePreview = { $(window) // Update the viewport width whenever it is resized, but max 4 times/s. - .on('resize.responsivePreview', Drupal.debounce(setViewportWidth, 250)); + .on('resize.responsivepreview', Drupal.debounce(setViewportWidth, 250)); $(document) // Respond to viewport offsetting elements like the Toolbar. - .on('drupalViewportOffsetChange.responsivePreview', function (event, offsets) { + .on('drupalViewportOffsetChange.responsivepreview', function (event, offsets) { envModel.set('offsets', offsets); }) - .on('keyup.responsivePreview', function (event) { + .on('keyup.responsivepreview', function (event) { // Close the preview if the Esc key is pressed. if (event.keyCode === 27) { previewModel.set('isActive', false); @@ -112,6 +120,22 @@ Drupal.behaviors.responsivePreview = { } } }, + detach: function (context) { + // Remove listeners on the window and document. + $(window).add(document).off('.responsivepreview'); + // Set the preview to an inactive state. + previewModel.set('isActive', false); + // Remove listener on the tabModel. + tabModel.off(); + // Remove any views + (appView && appView.remove()); + (blockView && blockView.remove()); + (tabView && tabView.remove()); + (keyboardView && keyboardView.remove()); + // Set the scope variables to an undefined value to remove references to + // the views. + previewModel = tabModel = appView = blockView = tabView = keyboardView = this.undef; + }, defaults: { gutter: 60, // The width of the device border around the iframe. This value is critical @@ -186,6 +210,57 @@ Drupal.responsivePreview.models.PreviewStateModel = Backbone.Model.extend({ }); /** + * + */ +Drupal.responsivePreview.views.AppView = Backbone.View.extend({ + + /** + * {@inheritdoc} + */ + initialize: function () { + this.envModel = this.options.envModel; + // Listen to changes on the previewModel. + this.model.on('change:isActive', this.render, this); + }, + + /** + * {@inheritdoc} + */ + render: function (previewModel, isActive, options) { + // The preview container view. + if (isActive && !this.previewView) { + // Holds the Backbone View of the preview. This view is created and destroyed + // when the preview is enabled or disabled respectively. + this.previewView = new Drupal.responsivePreview.views.PreviewView({ + el: Drupal.theme('responsivePreviewContainer'), + // The previewView model. + model: this.model, + envModel: this.envModel, + // Gutter size around preview frame. + gutter: this.options.gutter, + // Preview device frame width. + bleed: this.options.bleed, + strings: this.options.strings + }); + } + else if (!isActive && this.previewView) { + this.previewView.remove(); + delete this.previewView; + } + }, + + /** + * {@inheritdoc} + */ + remove: function () { + // Remove the previewView if it exists. + (this.previewView && this.previewView.remove()); + // Call the parent remove method on this view. + Backbone.View.prototype.remove.call(this); + } +}); + +/** * Handles responsive preview toolbar tab interactions. */ Drupal.responsivePreview.views.TabView = Backbone.View.extend({ @@ -196,7 +271,7 @@ Drupal.responsivePreview.views.TabView = Backbone.View.extend({ }, /** - * Implements Backbone.View.prototype.initialize(). + * {@inheritdoc} */ initialize: function () { this.gutter = this.options.gutter; @@ -207,7 +282,7 @@ Drupal.responsivePreview.views.TabView = Backbone.View.extend({ // 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.$el.on('click.responsivepreview', '.device', $.proxy(selectDevice, this)); this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this); @@ -218,7 +293,7 @@ Drupal.responsivePreview.views.TabView = Backbone.View.extend({ }, /** - * Implements Backbone.View.prototype.render(). + * {@inheritdoc} */ render: function () { var $deviceLink = $(this.model.get('activeDevice')); @@ -298,7 +373,7 @@ Drupal.responsivePreview.views.TabView = Backbone.View.extend({ Drupal.responsivePreview.views.BlockView = Backbone.View.extend({ /** - * Implements Backbone.View.prototype.initialize(). + * {@inheritdoc} */ initialize: function () { this.gutter = this.options.gutter; @@ -308,7 +383,7 @@ Drupal.responsivePreview.views.BlockView = Backbone.View.extend({ // 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.$el.on('click.responsivepreview', '.device', $.proxy(selectDevice, this)); this.model.on('change:isActive change:dimensions change:activeDevice change:fittingDeviceCount', this.render, this); @@ -316,7 +391,7 @@ Drupal.responsivePreview.views.BlockView = Backbone.View.extend({ }, /** - * Implements Backbone.View.prototype.render(). + * {@inheritdoc} */ render: function () { var $deviceLink = $(this.model.get('activeDevice')); @@ -339,53 +414,39 @@ Drupal.responsivePreview.views.BlockView = Backbone.View.extend({ }); /** - * Functions that are common to both the TabView and BlockView. - */ - -/** - * Model change handler; hides devices that don't fit the current viewport. + * Handles keyboard input. */ -function updateDeviceList () { - var gutter = this.gutter; - var bleed = this.bleed; - var viewportWidth = this.envModel.get('viewportWidth'); - var $devices = this.$el.find('.device'); +Drupal.responsivePreview.views.KeyboardView = Backbone.View.extend({ - // 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); -} + /* + * {@inheritdoc} + */ + initialize: function () { + $(document).on('keyup.responsivepreview', _.bind(this.onKeypress, this)); + }, -/** - * 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) + /** + * Responds to esc key press events. + * + * @param jQuery.Event event + */ + onKeypress: function (event) { + if (event.keyCode === 27) { + this.model.set('isActive', false); } - }); - // Toggle the preview on. - this.model.set('isActive', true); + }, - event.preventDefault(); -} + /** + * Removes a listener on the document; calls the standard Backbone remove. + */ + remove: function () { + // Unbind the keyup listener. + $(document).off('keyup.responsivepreview'); + // Call the standard remove method on this. + Backbone.View.prototype.remove.call(this); + } + +}); /** * Handles the responsive preview element interactions. @@ -398,44 +459,40 @@ Drupal.responsivePreview.views.PreviewView = Backbone.View.extend({ }, /** - * Implements Backbone.View.prototype.initialize(). + * {@inheritdoc} */ 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); + this.model.on('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 change:offsets', this.render, this); + + // Build the preview. + this._build(); + + // Call an initial render. + this.render(); }, /** - * Implements Backbone.View.prototype.render(). + * {@inheritdoc} */ render: function () { - var isActive = this.model.get('isActive'); - - // Build the preview if it doesn't exist. - if (isActive && !this.model.get('isBuilt')) { - this._build(); - } - - if (isActive) { - // Refresh the preview. - this._refresh(); - Drupal.displace(); - } + // Refresh the preview. + this._refresh(); + Drupal.displace(); // Render the state of the preview. var that = this; // Wrap the call in a setTimeout so that it invokes in the next compute // cycle, causing the CSS animations to render in the first pass. window.setTimeout(function () { - that.$el.toggleClass('active', isActive); + that.$el.toggleClass('active', that.model.get('isActive')); }, 0); return this; @@ -486,7 +543,7 @@ Drupal.responsivePreview.views.PreviewView = Backbone.View.extend({ height: '100%' }) // Load the current page URI into the preview iframe. - .on('load.responsivePreview', $.proxy(this._refresh, this)) + .on('load.responsivepreview', $.proxy(this._refresh, this)) // Add the frame to the preview container. .appendTo($frameContainer); // Insert the container into the DOM. @@ -731,6 +788,55 @@ Drupal.responsivePreview.views.PreviewView = Backbone.View.extend({ }); /** + * 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(); +} + +/** * Registers theme templates with Drupal.theme(). */ $.extend(Drupal.theme, { @@ -769,4 +875,4 @@ $.extend(Drupal.theme, { } }); -}(jQuery, Backbone, Drupal, drupalSettings, window, document)); +}(jQuery, Backbone, Drupal, drupalSettings));