Skip to content
Snippets Groups Projects
Commit 4161356d authored by Angie Byron's avatar Angie Byron
Browse files

Issue #2162409 by jibran, Wim Leers, nod_: Split up contextual.js.

parent cf784d09
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
......@@ -94,14 +94,26 @@ function contextual_permission() {
*/
function contextual_library_info() {
$path = drupal_get_path('module', 'contextual');
// Add the JavaScript, with a group and weight such that it will run
// before core/modules/contextual/js/contextual.toolbar.js.
$options = array(
'group' => JS_LIBRARY,
'weight' => -2,
);
$libraries['drupal.contextual-links'] = array(
'title' => 'Contextual Links',
'website' => 'http://drupal.org/node/473268',
'version' => \Drupal::VERSION,
'js' => array(
// Add the JavaScript, with a group and weight such that it will run
// before modules/contextual/js/contextual.toolbar.js.
$path . '/js/contextual.js' => array('group' => JS_LIBRARY, 'weight' => -2),
// Core.
$path . '/js/contextual.js' => $options,
// Models.
$path . '/js/models/StateModel.js' => $options,
// Views.
$path . '/js/views/AuralView.js' => $options,
$path . '/js/views/KeyboardView.js' => $options,
$path . '/js/views/RegionView.js' => $options,
$path . '/js/views/VisualView.js' => $options,
),
'css' => array(
$path . '/css/contextual.module.css' => array(),
......
......@@ -3,7 +3,7 @@
* Attaches behaviors for the Contextual module.
*/
(function ($, Drupal, drupalSettings, Backbone, Modernizr) {
(function ($, Drupal, drupalSettings, Backbone) {
"use strict";
......@@ -36,7 +36,7 @@ function initContextual ($contextual) {
.prepend(Drupal.theme('contextualTrigger'));
// Create a model and the appropriate views.
var model = new contextual.Model({
var model = new contextual.StateModel({
title: $region.find('h2:first').text().trim()
});
var viewOptions = $.extend({ el: $contextual, model: model }, options);
......@@ -160,9 +160,6 @@ Drupal.behaviors.contextual = {
}
};
/**
* Model and View definitions.
*/
Drupal.contextual = {
// The Drupal.contextual.View instances associated with each list element of
// contextual links.
......@@ -170,253 +167,11 @@ Drupal.contextual = {
// The Drupal.contextual.RegionView instances associated with each contextual
// region element.
regionViews: [],
/**
* Models the state of a contextual link's trigger and list.
*/
Model: Backbone.Model.extend({
defaults: {
// The title of the entity to which these contextual links apply.
title: '',
// Represents if the contextual region is being hovered.
regionIsHovered: false,
// Represents if the contextual trigger or options have focus.
hasFocus: false,
// Represents if the contextual options for an entity are available to
// be selected.
isOpen: false,
// When the model is locked, the trigger remains active.
isLocked: false
},
/**
* Opens or closes the contextual link.
*
* If it is opened, then also give focus.
*/
toggleOpen: function () {
var newIsOpen = !this.get('isOpen');
this.set('isOpen', newIsOpen);
if (newIsOpen) {
this.focus();
}
return this;
},
/**
* Closes this contextual link.
*
* Does not call blur() because we want to allow a contextual link to have
* focus, yet be closed for example when hovering.
*/
close: function () {
this.set('isOpen', false);
return this;
},
/**
* Gives focus to this contextual link.
*
* Also closes + removes focus from every other contextual link.
*/
focus: function () {
this.set('hasFocus', true);
var cid = this.cid;
this.collection.each(function (model) {
if (model.cid !== cid) {
model.close().blur();
}
});
return this;
},
/**
* Removes focus from this contextual link, unless it is open.
*/
blur: function () {
if (!this.get('isOpen')) {
this.set('hasFocus', false);
}
return this;
}
}),
/**
* Renders the visual view of a contextual link. Listens to mouse & touch.
*/
VisualView: Backbone.View.extend({
events: function () {
// Prevents delay and simulated mouse events.
var touchEndToClick = function (event) {
event.preventDefault();
event.target.click();
};
var mapping = {
'click .trigger': function () { this.model.toggleOpen(); },
'touchend .trigger': touchEndToClick,
'click .contextual-links a': function () { this.model.close().blur(); },
'touchend .contextual-links a': touchEndToClick
};
// We only want mouse hover events on non-touch.
if (!Modernizr.touch) {
mapping.mouseenter = function () { this.model.focus(); };
}
return mapping;
},
/**
* {@inheritdoc}
*/
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
/**
* {@inheritdoc}
*/
render: function () {
var isOpen = this.model.get('isOpen');
// The trigger should be visible when:
// - the mouse hovered over the region,
// - the trigger is locked,
// - and for as long as the contextual menu is open.
var isVisible = this.model.get('isLocked') || this.model.get('regionIsHovered') || isOpen;
this.$el
// The open state determines if the links are visible.
.toggleClass('open', isOpen)
// Update the visibility of the trigger.
.find('.trigger').toggleClass('visually-hidden', !isVisible);
// Nested contextual region handling: hide any nested contextual triggers.
if ('isOpen' in this.model.changed) {
this.$el.closest('.contextual-region')
.find('.contextual .trigger:not(:first)')
.toggle(!isOpen);
}
return this;
}
}),
/**
* Renders the aural view of a contextual link (i.e. screen reader support).
*/
AuralView: Backbone.View.extend({
/**
* {@inheritdoc}
*/
initialize: function (options) {
this.options = options;
this.listenTo(this.model, 'change', this.render);
// Use aria-role form so that the number of items in the list is spoken.
this.$el.attr('role', 'form');
// Initial render.
this.render();
},
/**
* {@inheritdoc}
*/
render: function () {
var isOpen = this.model.get('isOpen');
// Set the hidden property of the links.
this.$el.find('.contextual-links')
.prop('hidden', !isOpen);
// Update the view of the trigger.
this.$el.find('.trigger')
.text(Drupal.t('@action @title configuration options', {
'@action': (!isOpen) ? this.options.strings.open : this.options.strings.close,
'@title': this.model.get('title')
}))
.attr('aria-pressed', isOpen);
}
}),
/**
* Listens to keyboard.
*/
KeyboardView: Backbone.View.extend({
events: {
'focus .trigger': 'focus',
'focus .contextual-links a': 'focus',
'blur .trigger': function () { this.model.blur(); },
'blur .contextual-links a': function () {
// Set up a timeout to allow a user to tab between the trigger and the
// contextual links without the menu dismissing.
var that = this;
this.timer = window.setTimeout(function () {
that.model.close().blur();
}, 150);
}
},
/**
* {@inheritdoc}
*/
initialize: function () {
// The timer is used to create a delay before dismissing the contextual
// links on blur. This is only necessary when keyboard users tab into
// contextual links without edit mode (i.e. without TabbingManager).
// That means that if we decide to disable tabbing of contextual links
// without edit mode, all this timer logic can go away.
this.timer = NaN;
},
/**
* Sets focus on the model; Clears the timer that dismisses the links.
*/
focus: function () {
// Clear the timeout that might have been set by blurring a link.
window.clearTimeout(this.timer);
this.model.focus();
}
}),
/**
* Renders the visual view of a contextual region element.
*/
RegionView: Backbone.View.extend({
events: function () {
var mapping = {
mouseenter: function () { this.model.set('regionIsHovered', true); },
mouseleave: function () {
this.model.close().blur().set('regionIsHovered', false);
}
};
// We don't want mouse hover events on touch.
if (Modernizr.touch) {
mapping = {};
}
return mapping;
},
/**
* {@inheritdoc}
*/
initialize: function () {
this.listenTo(this.model, 'change:hasFocus', this.render);
},
/**
* {@inheritdoc}
*/
render: function () {
this.$el.toggleClass('focus', this.model.get('hasFocus'));
return this;
}
})
regionViews: []
};
// A Backbone.Collection of Drupal.contextual.Model instances.
Drupal.contextual.collection = new Backbone.Collection([], { model: Drupal.contextual.Model });
// A Backbone.Collection of Drupal.contextual.StateModel instances.
Drupal.contextual.collection = new Backbone.Collection([], { model: Drupal.contextual.StateModel });
/**
* A trigger is an interactive element often bound to a click handler.
......@@ -428,4 +183,4 @@ Drupal.theme.contextualTrigger = function () {
return '<button class="trigger visually-hidden focusable" type="button"></button>';
};
})(jQuery, Drupal, drupalSettings, Backbone, Modernizr);
})(jQuery, Drupal, drupalSettings, Backbone);
/**
* @file
* A Backbone Model for the state of a contextual link's trigger, list & region.
*/
(function (Drupal, Backbone) {
"use strict";
/**
* Models the state of a contextual link's trigger, list & region.
*/
Drupal.contextual.StateModel = Backbone.Model.extend({
defaults: {
// The title of the entity to which these contextual links apply.
title: '',
// Represents if the contextual region is being hovered.
regionIsHovered: false,
// Represents if the contextual trigger or options have focus.
hasFocus: false,
// Represents if the contextual options for an entity are available to
// be selected (i.e. whether the list of options is visible).
isOpen: false,
// When the model is locked, the trigger remains active.
isLocked: false
},
/**
* Opens or closes the contextual link.
*
* If it is opened, then also give focus.
*/
toggleOpen: function () {
var newIsOpen = !this.get('isOpen');
this.set('isOpen', newIsOpen);
if (newIsOpen) {
this.focus();
}
return this;
},
/**
* Closes this contextual link.
*
* Does not call blur() because we want to allow a contextual link to have
* focus, yet be closed for example when hovering.
*/
close: function () {
this.set('isOpen', false);
return this;
},
/**
* Gives focus to this contextual link.
*
* Also closes + removes focus from every other contextual link.
*/
focus: function () {
this.set('hasFocus', true);
var cid = this.cid;
this.collection.each(function (model) {
if (model.cid !== cid) {
model.close().blur();
}
});
return this;
},
/**
* Removes focus from this contextual link, unless it is open.
*/
blur: function () {
if (!this.get('isOpen')) {
this.set('hasFocus', false);
}
return this;
}
});
})(Drupal, Backbone);
/**
* @file
* A Backbone View that provides the aural view of a contextual link.
*/
(function (Drupal, Backbone) {
"use strict";
/**
* Renders the aural view of a contextual link (i.e. screen reader support).
*/
Drupal.contextual.AuralView = Backbone.View.extend({
/**
* {@inheritdoc}
*/
initialize: function (options) {
this.options = options;
this.listenTo(this.model, 'change', this.render);
// Use aria-role form so that the number of items in the list is spoken.
this.$el.attr('role', 'form');
// Initial render.
this.render();
},
/**
* {@inheritdoc}
*/
render: function () {
var isOpen = this.model.get('isOpen');
// Set the hidden property of the links.
this.$el.find('.contextual-links')
.prop('hidden', !isOpen);
// Update the view of the trigger.
this.$el.find('.trigger')
.text(Drupal.t('@action @title configuration options', {
'@action': (!isOpen) ? this.options.strings.open : this.options.strings.close,
'@title': this.model.get('title')
}))
.attr('aria-pressed', isOpen);
}
});
})(Drupal, Backbone);
/**
* @file
* A Backbone View that provides keyboard interaction for a contextual link.
*/
(function (Drupal, Backbone) {
"use strict";
/**
* Provides keyboard interaction for a contextual link.
*/
Drupal.contextual.KeyboardView = Backbone.View.extend({
events: {
'focus .trigger': 'focus',
'focus .contextual-links a': 'focus',
'blur .trigger': function () { this.model.blur(); },
'blur .contextual-links a': function () {
// Set up a timeout to allow a user to tab between the trigger and the
// contextual links without the menu dismissing.
var that = this;
this.timer = window.setTimeout(function () {
that.model.close().blur();
}, 150);
}
},
/**
* {@inheritdoc}
*/
initialize: function () {
// The timer is used to create a delay before dismissing the contextual
// links on blur. This is only necessary when keyboard users tab into
// contextual links without edit mode (i.e. without TabbingManager).
// That means that if we decide to disable tabbing of contextual links
// without edit mode, all this timer logic can go away.
this.timer = NaN;
},
/**
* Sets focus on the model; Clears the timer that dismisses the links.
*/
focus: function () {
// Clear the timeout that might have been set by blurring a link.
window.clearTimeout(this.timer);
this.model.focus();
}
});
})(Drupal, Backbone);
/**
* @file
* A Backbone View that renders the visual view of a contextual region element.
*/
(function (Drupal, Backbone, Modernizr) {
"use strict";
/**
* Renders the visual view of a contextual region element.
*/
Drupal.contextual.RegionView = Backbone.View.extend({
events: function () {
var mapping = {
mouseenter: function () { this.model.set('regionIsHovered', true); },
mouseleave: function () {
this.model.close().blur().set('regionIsHovered', false);
}
};
// We don't want mouse hover events on touch.
if (Modernizr.touch) {
mapping = {};
}
return mapping;
},
/**
* {@inheritdoc}
*/
initialize: function () {
this.listenTo(this.model, 'change:hasFocus', this.render);
},
/**
* {@inheritdoc}
*/
render: function () {
this.$el.toggleClass('focus', this.model.get('hasFocus'));
return this;
}
});
})(Drupal, Backbone, Modernizr);
/**
* @file
* A Backbone View that provides the visual view of a contextual link.
*/
(function (Drupal, Backbone, Modernizr) {
"use strict";
/**
* Renders the visual view of a contextual link. Listens to mouse & touch.
*/
Drupal.contextual.VisualView = Backbone.View.extend({
events: function () {
// Prevents delay and simulated mouse events.
var touchEndToClick = function (event) {
event.preventDefault();
event.target.click();
};
var mapping = {
'click .trigger': function () { this.model.toggleOpen(); },
'touchend .trigger': touchEndToClick,
'click .contextual-links a': function () { this.model.close().blur(); },
'touchend .contextual-links a': touchEndToClick
};
// We only want mouse hover events on non-touch.
if (!Modernizr.touch) {
mapping.mouseenter = function () { this.model.focus(); };
}
return mapping;
},
/**
* {@inheritdoc}
*/
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
/**
* {@inheritdoc}
*/
render: function () {
var isOpen = this.model.get('isOpen');
// The trigger should be visible when:
// - the mouse hovered over the region,
// - the trigger is locked,
// - and for as long as the contextual menu is open.
var isVisible = this.model.get('isLocked') || this.model.get('regionIsHovered') || isOpen;
this.$el
// The open state determines if the links are visible.
.toggleClass('open', isOpen)
// Update the visibility of the trigger.
.find('.trigger').toggleClass('visually-hidden', !isVisible);
// Nested contextual region handling: hide any nested contextual triggers.
if ('isOpen' in this.model.changed) {
this.$el.closest('.contextual-region')
.find('.contextual .trigger:not(:first)')
.toggle(!isOpen);
}
return this;
}
});
})(Drupal, Backbone, Modernizr);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment