diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index a465e47..4a0aca7 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -121,7 +121,13 @@ function contextual_library_info() { 'title' => 'Contextual Links Toolbar Tab', 'version' => \Drupal::VERSION, 'js' => array( + // Core. $path . '/js/contextual.toolbar.js' => array('group' => JS_LIBRARY), + // Model. + $path . '/js/models/Model.js' => array('group' => JS_LIBRARY), + // Views. + $path . '/js/views/AuralView.js' => array('group' => JS_LIBRARY), + $path . '/js/views/VisualView.js' => array('group' => JS_LIBRARY), ), 'css' => array( $path . '/css/contextual.toolbar.css' => array(), diff --git a/core/modules/contextual/js/contextual.toolbar.js b/core/modules/contextual/js/contextual.toolbar.js index 0c6a8b8..7f799ad 100644 --- a/core/modules/contextual/js/contextual.toolbar.js +++ b/core/modules/contextual/js/contextual.toolbar.js @@ -3,7 +3,7 @@ * Attaches behaviors for the Contextual module's edit toolbar tab. */ -(function ($, Drupal, Backbone) { +(function ($, Drupal) { "use strict"; @@ -59,234 +59,7 @@ Drupal.behaviors.contextualToolbar = { */ Drupal.contextualToolbar = { // The Drupal.contextualToolbar.Model instance. - model: null, - - /** - * Models the state of the edit mode toggle. - */ - Model: Backbone.Model.extend({ - defaults: { - // Indicates whether the toggle is currently in "view" or "edit" mode. - isViewing: true, - // Indicates whether the toggle should be visible or hidden. Automatically - // calculated, depends on contextualCount. - isVisible: false, - // Tracks how many contextual links exist on the page. - contextualCount: 0, - // A TabbingContext object as returned by Drupal.TabbingManager: the set - // of tabbable elements when edit mode is enabled. - tabbingContext: null - }, - - /** - * {@inheritdoc} - * - * @param Object attrs - * @param Object options - * An object with the following option: - * - Backbone.collection contextualCollection: the collection of - * Drupal.contextual.Model models that represent the contextual links - * on the page. - */ - initialize: function (attrs, options) { - // Respond to new/removed contextual links. - this.listenTo(options.contextualCollection, { - 'reset remove add': this.countCountextualLinks, - 'add': this.lockNewContextualLinks - }); - - this.listenTo(this, { - // Automatically determine visibility. - 'change:contextualCount': this.updateVisibility, - // Whenever edit mode is toggled, lock all contextual links. - 'change:isViewing': function (model, isViewing) { - options.contextualCollection.each(function (contextualModel) { - contextualModel.set('isLocked', !isViewing); - }); - } - }); - }, - - /** - * Tracks the number of contextual link models in the collection. - * - * @param Drupal.contextual.Model affectedModel - * The contextual links model that was added or removed. - * @param Backbone.Collection contextualCollection - * The collection of contextual link models. - */ - countCountextualLinks: function (contextualModel, contextualCollection) { - this.set('contextualCount', contextualCollection.length); - }, - - /** - * Lock newly added contextual links if edit mode is enabled. - * - * @param Drupal.contextual.Model addedContextualModel - * The contextual links model that was added. - * @param Backbone.Collection contextualCollection - * The collection of contextual link models. - */ - lockNewContextualLinks: function (contextualModel, contextualCollection) { - if (!this.get('isViewing')) { - contextualModel.set('isLocked', true); - } - }, - - /** - * Automatically updates visibility of the view/edit mode toggle. - */ - updateVisibility: function () { - this.set('isVisible', this.get('contextualCount') > 0); - } - }), - - /** - * Renders the visual view of the edit mode toggle. Listens to mouse & touch. - * - * Handles edit mode toggle interactions. - */ - VisualView: Backbone.View.extend({ - events: function () { - // Prevents delay and simulated mouse events. - var touchEndToClick = function (event) { - event.preventDefault(); - event.target.click(); - }; - - return { - 'click': function () { - this.model.set('isViewing', !this.model.get('isViewing')); - }, - 'touchend': touchEndToClick - }; - }, - - /** - * {@inheritdoc} - */ - initialize: function () { - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model, 'change:isViewing', this.persist); - }, - - /** - * {@inheritdoc} - */ - render: function () { - // Render the visibility. - this.$el.toggleClass('hidden', !this.model.get('isVisible')); - // Render the state. - this.$el.find('button').toggleClass('active', !this.model.get('isViewing')); - - return this; - }, - - /** - * Model change handler; persists the isViewing value to localStorage. - * - * isViewing === true is the default, so only stores in localStorage when - * it's not the default value (i.e. false). - * - * @param Drupal.contextualToolbar.Model model - * A Drupal.contextualToolbar.Model model. - * @param bool isViewing - * The value of the isViewing attribute in the model. - */ - persist: function (model, isViewing) { - if (!isViewing) { - localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false'); - } - else { - localStorage.removeItem('Drupal.contextualToolbar.isViewing'); - } - } - }), - - /** - * Renders the aural view of the edit mode toggle (i.e.screen reader support). - */ - AuralView: Backbone.View.extend({ - // Tracks whether the tabbing constraint announcement has been read once yet. - announcedOnce: false, - - /* - * {@inheritdoc} - */ - initialize: function (options) { - this.options = options; - - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model, 'change:isViewing', this.manageTabbing); - - $(document).on('keyup', _.bind(this.onKeypress, this)); - }, - - /** - * {@inheritdoc} - */ - render: function () { - // Render the state. - this.$el.find('button').attr('aria-pressed', !this.model.get('isViewing')); - - return this; - }, - - /** - * Limits tabbing to the contextual links and edit mode toolbar tab. - * - * @param Drupal.contextualToolbar.Model model - * A Drupal.contextualToolbar.Model model. - * @param bool isViewing - * The value of the isViewing attribute in the model. - */ - manageTabbing: function () { - var tabbingContext = this.model.get('tabbingContext'); - // Always release an existing tabbing context. - if (tabbingContext) { - tabbingContext.release(); - Drupal.announce(this.options.strings.tabbingReleased); - } - // Create a new tabbing context when edit mode is enabled. - if (!this.model.get('isViewing')) { - tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual')); - this.model.set('tabbingContext', tabbingContext); - this.announceTabbingConstraint(); - this.announcedOnce = true; - } - }, - - /** - * Announces the current tabbing constraint. - */ - announceTabbingConstraint: function () { - var strings = this.options.strings; - Drupal.announce(Drupal.formatString(strings.tabbingConstrained, { - '@contextualsCount': Drupal.formatPlural(Drupal.contextual.collection.length, '@count contextual link', '@count contextual links') - })); - Drupal.announce(strings.pressEsc); - }, - - /** - * Responds to esc and tab key press events. - * - * @param jQuery.Event event - */ - onKeypress: function (event) { - // The first tab key press is tracked so that an annoucement about tabbing - // constraints can be raised if edit mode is enabled when the page is - // loaded. - if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) { - this.announceTabbingConstraint(); - // Set announce to true so that this conditional block won't run again. - this.announcedOnce = true; - } - // Respond to the ESC key. Exit out of edit mode. - if (event.keyCode === 27) { - this.model.set('isViewing', true); - } - } - }) + model: null }; -})(jQuery, Drupal, Backbone); +})(jQuery, Drupal); diff --git a/core/modules/contextual/js/models/Model.js b/core/modules/contextual/js/models/Model.js new file mode 100644 index 0000000..a2c0501 --- /dev/null +++ b/core/modules/contextual/js/models/Model.js @@ -0,0 +1,90 @@ +/** + * @file + * Attaches behaviors for the Contextual module's edit toolbar tab. + */ + +(function (Drupal, Backbone) { + +"use strict"; + +/** + * Models the state of the edit mode toggle. + */ +Drupal.contextualToolbar.Model = Backbone.Model.extend({ + defaults: { + // Indicates whether the toggle is currently in "view" or "edit" mode. + isViewing: true, + // Indicates whether the toggle should be visible or hidden. Automatically + // calculated, depends on contextualCount. + isVisible: false, + // Tracks how many contextual links exist on the page. + contextualCount: 0, + // A TabbingContext object as returned by Drupal.TabbingManager: the set + // of tabbable elements when edit mode is enabled. + tabbingContext: null + }, + + /** + * {@inheritdoc} + * + * @param Object attrs + * @param Object options + * An object with the following option: + * - Backbone.collection contextualCollection: the collection of + * Drupal.contextual.Model models that represent the contextual links + * on the page. + */ + initialize: function (attrs, options) { + // Respond to new/removed contextual links. + this.listenTo(options.contextualCollection, { + 'reset remove add': this.countCountextualLinks, + 'add': this.lockNewContextualLinks + }); + + this.listenTo(this, { + // Automatically determine visibility. + 'change:contextualCount': this.updateVisibility, + // Whenever edit mode is toggled, lock all contextual links. + 'change:isViewing': function (model, isViewing) { + options.contextualCollection.each(function (contextualModel) { + contextualModel.set('isLocked', !isViewing); + }); + } + }); + }, + + /** + * Tracks the number of contextual link models in the collection. + * + * @param Drupal.contextual.Model affectedModel + * The contextual links model that was added or removed. + * @param Backbone.Collection contextualCollection + * The collection of contextual link models. + */ + countCountextualLinks: function (contextualModel, contextualCollection) { + this.set('contextualCount', contextualCollection.length); + }, + + /** + * Lock newly added contextual links if edit mode is enabled. + * + * @param Drupal.contextual.Model addedContextualModel + * The contextual links model that was added. + * @param Backbone.Collection contextualCollection + * The collection of contextual link models. + */ + lockNewContextualLinks: function (contextualModel, contextualCollection) { + if (!this.get('isViewing')) { + contextualModel.set('isLocked', true); + } + }, + + /** + * Automatically updates visibility of the view/edit mode toggle. + */ + updateVisibility: function () { + this.set('isVisible', this.get('contextualCount') > 0); + } +}); + +})(Drupal, Backbone); diff --git a/core/modules/contextual/js/views/AuralView.js b/core/modules/contextual/js/views/AuralView.js new file mode 100644 index 0000000..3690c96 --- /dev/null +++ b/core/modules/contextual/js/views/AuralView.js @@ -0,0 +1,93 @@ +/** + * @file + * A Backbone View that renders the aural view of the edit mode toggle. + */ + +(function ($, Drupal, Backbone) { + +"use strict"; + +/** + * Renders the aural view of the edit mode toggle (i.e.screen reader support). + */ +Drupal.contextualToolbar.AuralView = Backbone.View.extend({ + // Tracks whether the tabbing constraint announcement has been read once yet. + announcedOnce: false, + + /* + * {@inheritdoc} + */ + initialize: function () { + this.model.on('change', this.render, this); + this.model.on('change:isViewing', this.manageTabbing, this); + + $(document).on('keyup', _.bind(this.onKeypress, this)); + }, + + /** + * {@inheritdoc} + */ + render: function () { + // Render the state. + this.$el.find('button').attr('aria-pressed', !this.model.get('isViewing')); + + return this; + }, + + /** + * Limits tabbing to the contextual links and edit mode toolbar tab. + * + * @param Drupal.contextualToolbar.Model model + * A Drupal.contextualToolbar.Model model. + * @param bool isViewing + * The value of the isViewing attribute in the model. + */ + manageTabbing: function () { + var tabbingContext = this.model.get('tabbingContext'); + // Always release an existing tabbing context. + if (tabbingContext) { + tabbingContext.release(); + Drupal.announce(this.options.strings.tabbingReleased); + } + // Create a new tabbing context when edit mode is enabled. + if (!this.model.get('isViewing')) { + tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual')); + this.model.set('tabbingContext', tabbingContext); + this.announceTabbingConstraint(); + this.announcedOnce = true; + } + }, + + /** + * Announces the current tabbing constraint. + */ + announceTabbingConstraint: function () { + var strings = this.options.strings; + Drupal.announce(Drupal.formatString(strings.tabbingConstrained, { + '@contextualsCount': Drupal.formatPlural(Drupal.contextual.collection.length, '@count contextual link', '@count contextual links') + })); + Drupal.announce(strings.pressEsc); + }, + + /** + * Responds to esc and tab key press events. + * + * @param jQuery.Event event + */ + onKeypress: function (event) { + // The first tab key press is tracked so that an annoucement about tabbing + // constraints can be raised if edit mode is enabled when the page is + // loaded. + if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) { + this.announceTabbingConstraint(); + // Set announce to true so that this conditional block won't run again. + this.announcedOnce = true; + } + // Respond to the ESC key. Exit out of edit mode. + if (event.keyCode === 27) { + this.model.set('isViewing', true); + } + } +}); + +})(jQuery, Drupal, Backbone); diff --git a/core/modules/contextual/js/views/VisualView.js b/core/modules/contextual/js/views/VisualView.js new file mode 100644 index 0000000..91a7c62 --- /dev/null +++ b/core/modules/contextual/js/views/VisualView.js @@ -0,0 +1,72 @@ +/** + * @file + * A Backbone View that renders the visual view of the edit mode toggle. + */ + +(function (Drupal, Backbone) { + +"use strict"; + +/** + * Renders the visual view of the edit mode toggle. Listens to mouse & touch. + * + * Handles edit mode toggle interactions. + */ +Drupal.contextualToolbar.VisualView = Backbone.View.extend({ + events: function () { + // Prevents delay and simulated mouse events. + var touchEndToClick = function (event) { + event.preventDefault(); + event.target.click(); + }; + + return { + 'click': function () { + this.model.set('isViewing', !this.model.get('isViewing')); + }, + 'touchend': touchEndToClick + }; + }, + + /** + * {@inheritdoc} + */ + initialize: function () { + this.model.on('change', this.render, this); + this.model.on('change:isViewing', this.persist, this); + }, + + /** + * {@inheritdoc} + */ + render: function () { + // Render the visibility. + this.$el.toggleClass('hidden', !this.model.get('isVisible')); + // Render the state. + this.$el.find('button').toggleClass('active', !this.model.get('isViewing')); + + return this; + }, + + /** + * Model change handler; persists the isViewing value to localStorage. + * + * isViewing === true is the default, so only stores in localStorage when + * it's not the default value (i.e. false). + * + * @param Drupal.contextualToolbar.Model model + * A Drupal.contextualToolbar.Model model. + * @param bool isViewing + * The value of the isViewing attribute in the model. + */ + persist: function (model, isViewing) { + if (!isViewing) { + localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false'); + } + else { + localStorage.removeItem('Drupal.contextualToolbar.isViewing'); + } + } +}); + +})(Drupal, Backbone);