first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
/**
* @file
* Attaches behaviors for the Contextual module.
*/
(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
const options = $.extend(
drupalSettings.contextual,
// Merge strings on top of drupalSettings so that they are not mutable.
{
strings: {
open: Drupal.t('Open'),
close: Drupal.t('Close'),
},
},
);
// Clear the cached contextual links whenever the current user's set of
// permissions changes.
const cachedPermissionsHash = storage.getItem(
'Drupal.contextual.permissionsHash',
);
const permissionsHash = drupalSettings.user.permissionsHash;
if (cachedPermissionsHash !== permissionsHash) {
if (typeof permissionsHash === 'string') {
_.chain(storage)
.keys()
.each((key) => {
if (key.startsWith('Drupal.contextual.')) {
storage.removeItem(key);
}
});
}
storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
}
/**
* Determines if a contextual link is nested & overlapping, if so: adjusts it.
*
* This only deals with two levels of nesting; deeper levels are not touched.
*
* @param {jQuery} $contextual
* A contextual links placeholder DOM element, containing the actual
* contextual links as rendered by the server.
*/
function adjustIfNestedAndOverlapping($contextual) {
const $contextuals = $contextual
// @todo confirm that .closest() is not sufficient
.parents('.contextual-region')
.eq(-1)
.find('.contextual');
// Early-return when there's no nesting.
if ($contextuals.length <= 1) {
return;
}
// If the two contextual links overlap, then we move the second one.
const firstTop = $contextuals.eq(0).offset().top;
const secondTop = $contextuals.eq(1).offset().top;
if (firstTop === secondTop) {
const $nestedContextual = $contextuals.eq(1);
// Retrieve height of nested contextual link.
let height = 0;
const $trigger = $nestedContextual.find('.trigger');
// Elements with the .visually-hidden class have no dimensions, so this
// class must be temporarily removed to the calculate the height.
$trigger.removeClass('visually-hidden');
height = $nestedContextual.height();
$trigger.addClass('visually-hidden');
// Adjust nested contextual link's position.
$nestedContextual[0].style.top =
$nestedContextual.position().top + height;
}
}
/**
* Initializes a contextual link: updates its DOM, sets up model and views.
*
* @param {jQuery} $contextual
* A contextual links placeholder DOM element, containing the actual
* contextual links as rendered by the server.
* @param {string} html
* The server-side rendered HTML for this contextual link.
*/
function initContextual($contextual, html) {
const $region = $contextual.closest('.contextual-region');
const contextual = Drupal.contextual;
$contextual
// Update the placeholder to contain its rendered contextual links.
.html(html)
// Use the placeholder as a wrapper with a specific class to provide
// positioning and behavior attachment context.
.addClass('contextual')
// Ensure a trigger element exists before the actual contextual links.
.prepend(Drupal.theme('contextualTrigger'));
// Set the destination parameter on each of the contextual links.
const destination = `destination=${Drupal.encodePath(
Drupal.url(drupalSettings.path.currentPath + window.location.search),
)}`;
$contextual.find('.contextual-links a').each(function () {
const url = this.getAttribute('href');
const glue = url.includes('?') ? '&' : '?';
this.setAttribute('href', url + glue + destination);
});
let title = '';
const $regionHeading = $region.find('h2');
if ($regionHeading.length) {
title = $regionHeading[0].textContent.trim();
}
// Create a model and the appropriate views.
const model = new contextual.StateModel({
title,
});
const viewOptions = $.extend({ el: $contextual, model }, options);
contextual.views.push({
visual: new contextual.VisualView(viewOptions),
aural: new contextual.AuralView(viewOptions),
keyboard: new contextual.KeyboardView(viewOptions),
});
contextual.regionViews.push(
new contextual.RegionView($.extend({ el: $region, model }, options)),
);
// Add the model to the collection. This must happen after the views have
// been associated with it, otherwise collection change event handlers can't
// trigger the model change event handler in its views.
contextual.collection.add(model);
// Let other JavaScript react to the adding of a new contextual link.
$(document).trigger(
'drupalContextualLinkAdded',
Drupal.deprecatedProperty({
target: {
$el: $contextual,
$region,
model,
},
deprecatedProperty: 'model',
message:
'The model property is deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no replacement.',
}),
);
// Fix visual collisions between contextual link triggers.
adjustIfNestedAndOverlapping($contextual);
}
/**
* Attaches outline behavior for regions associated with contextual links.
*
* Events
* Contextual triggers an event that can be used by other scripts.
* - drupalContextualLinkAdded: Triggered when a contextual link is added.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the outline behavior to the right context.
*/
Drupal.behaviors.contextual = {
attach(context) {
const $context = $(context);
// Find all contextual links placeholders, if any.
let $placeholders = $(
once('contextual-render', '[data-contextual-id]', context),
);
if ($placeholders.length === 0) {
return;
}
// Collect the IDs for all contextual links placeholders.
const ids = [];
$placeholders.each(function () {
ids.push({
id: $(this).attr('data-contextual-id'),
token: $(this).attr('data-contextual-token'),
});
});
const uncachedIDs = [];
const uncachedTokens = [];
ids.forEach((contextualID) => {
const html = storage.getItem(`Drupal.contextual.${contextualID.id}`);
if (html && html.length) {
// Initialize after the current execution cycle, to make the AJAX
// request for retrieving the uncached contextual links as soon as
// possible, but also to ensure that other Drupal behaviors have had
// the chance to set up an event listener on the Backbone collection
// Drupal.contextual.collection.
window.setTimeout(() => {
initContextual(
$context
.find(`[data-contextual-id="${contextualID.id}"]:empty`)
.eq(0),
html,
);
});
return;
}
uncachedIDs.push(contextualID.id);
uncachedTokens.push(contextualID.token);
});
// Perform an AJAX request to let the server render the contextual links
// for each of the placeholders.
if (uncachedIDs.length > 0) {
$.ajax({
url: Drupal.url('contextual/render'),
type: 'POST',
data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
dataType: 'json',
success(results) {
_.each(results, (html, contextualID) => {
// Store the metadata.
storage.setItem(`Drupal.contextual.${contextualID}`, html);
// If the rendered contextual links are empty, then the current
// user does not have permission to access the associated links:
// don't render anything.
if (html.length > 0) {
// Update the placeholders to contain its rendered contextual
// links. Usually there will only be one placeholder, but it's
// possible for multiple identical placeholders exist on the
// page (probably because the same content appears more than
// once).
$placeholders = $context.find(
`[data-contextual-id="${contextualID}"]`,
);
// Initialize the contextual links.
for (let i = 0; i < $placeholders.length; i++) {
initContextual($placeholders.eq(i), html);
}
}
});
},
});
}
},
};
/**
* Namespace for contextual related functionality.
*
* @namespace
*
* @private
*/
Drupal.contextual = {
/**
* The {@link Drupal.contextual.View} instances associated with each list
* element of contextual links.
*
* @type {Array}
*
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
views: [],
/**
* The {@link Drupal.contextual.RegionView} instances associated with each
* contextual region element.
*
* @type {Array}
*
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
regionViews: [],
};
/**
* A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
*
* @type {Backbone.Collection}
*
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextual.collection = new Backbone.Collection([], {
model: Drupal.contextual.StateModel,
});
/**
* A trigger is an interactive element often bound to a click handler.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.contextualTrigger = function () {
return '<button class="trigger visually-hidden focusable" type="button"></button>';
};
/**
* Bind Ajax contextual links when added.
*
* @param {jQuery.Event} event
* The `drupalContextualLinkAdded` event.
* @param {object} data
* An object containing the data relevant to the event.
*
* @listens event:drupalContextualLinkAdded
*/
$(document).on('drupalContextualLinkAdded', (event, data) => {
Drupal.ajax.bindAjaxLinks(data.$el[0]);
});
})(
jQuery,
Drupal,
drupalSettings,
_,
Backbone,
window.JSON,
window.sessionStorage,
);

View File

@@ -0,0 +1,87 @@
/**
* @file
* Attaches behaviors for the Contextual module's edit toolbar tab.
*/
(function ($, Drupal, Backbone) {
const strings = {
tabbingReleased: Drupal.t(
'Tabbing is no longer constrained by the Contextual module.',
),
tabbingConstrained: Drupal.t(
'Tabbing is constrained to a set of @contextualsCount and the edit mode toggle.',
),
pressEsc: Drupal.t('Press the esc key to exit.'),
};
/**
* Initializes a contextual link: updates its DOM, sets up model and views.
*
* @param {HTMLElement} context
* A contextual links DOM element as rendered by the server.
*/
function initContextualToolbar(context) {
if (!Drupal.contextual || !Drupal.contextual.collection) {
return;
}
const contextualToolbar = Drupal.contextualToolbar;
contextualToolbar.model = new contextualToolbar.StateModel(
{
// Checks whether localStorage indicates we should start in edit mode
// rather than view mode.
// @see Drupal.contextualToolbar.VisualView.persist
isViewing:
document.querySelector('body .contextual-region') === null ||
localStorage.getItem('Drupal.contextualToolbar.isViewing') !==
'false',
},
{
contextualCollection: Drupal.contextual.collection,
},
);
const viewOptions = {
el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'),
model: contextualToolbar.model,
strings,
};
new contextualToolbar.VisualView(viewOptions);
new contextualToolbar.AuralView(viewOptions);
}
/**
* Attaches contextual's edit toolbar tab behavior.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches contextual toolbar behavior on a contextualToolbar-init event.
*/
Drupal.behaviors.contextualToolbar = {
attach(context) {
if (once('contextualToolbar-init', 'body').length) {
initContextualToolbar(context);
}
},
};
/**
* Namespace for the contextual toolbar.
*
* @namespace
*
* @private
*/
Drupal.contextualToolbar = {
/**
* The {@link Drupal.contextualToolbar.StateModel} instance.
*
* @type {?Drupal.contextualToolbar.StateModel}
*
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is
* no replacement.
*/
model: null,
};
})(jQuery, Drupal, Backbone);

View File

@@ -0,0 +1,130 @@
/**
* @file
* A Backbone Model for the state of a contextual link's trigger, list & region.
*/
(function (Drupal, Backbone) {
/**
* Models the state of a contextual link's trigger, list & region.
*
* @constructor
*
* @augments Backbone.Model
*
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextual.StateModel = Backbone.Model.extend(
/** @lends Drupal.contextual.StateModel# */ {
/**
* @type {object}
*
* @prop {string} title
* @prop {boolean} regionIsHovered
* @prop {boolean} hasFocus
* @prop {boolean} isOpen
* @prop {boolean} isLocked
*/
defaults: /** @lends Drupal.contextual.StateModel# */ {
/**
* The title of the entity to which these contextual links apply.
*
* @type {string}
*/
title: '',
/**
* Represents if the contextual region is being hovered.
*
* @type {boolean}
*/
regionIsHovered: false,
/**
* Represents if the contextual trigger or options have focus.
*
* @type {boolean}
*/
hasFocus: false,
/**
* Represents if the contextual options for an entity are available to
* be selected (i.e. whether the list of options is visible).
*
* @type {boolean}
*/
isOpen: false,
/**
* When the model is locked, the trigger remains active.
*
* @type {boolean}
*/
isLocked: false,
},
/**
* Opens or closes the contextual link.
*
* If it is opened, then also give focus.
*
* @return {Drupal.contextual.StateModel}
* The current contextual state model.
*/
toggleOpen() {
const 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.
*
* @return {Drupal.contextual.StateModel}
* The current contextual state model.
*/
close() {
this.set('isOpen', false);
return this;
},
/**
* Gives focus to this contextual link.
*
* Also closes + removes focus from every other contextual link.
*
* @return {Drupal.contextual.StateModel}
* The current contextual state model.
*/
focus() {
this.set('hasFocus', true);
const cid = this.cid;
this.collection.each((model) => {
if (model.cid !== cid) {
model.close().blur();
}
});
return this;
},
/**
* Removes focus from this contextual link, unless it is open.
*
* @return {Drupal.contextual.StateModel}
* The current contextual state model.
*/
blur() {
if (!this.get('isOpen')) {
this.set('hasFocus', false);
}
return this;
},
},
);
})(Drupal, Backbone);

View File

@@ -0,0 +1,126 @@
/**
* @file
* A Backbone Model for the state of Contextual module's edit toolbar tab.
*/
(function (Drupal, Backbone) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextualToolbar.StateModel = Backbone.Model.extend(
/** @lends Drupal.contextualToolbar.StateModel# */ {
/**
* @type {object}
*
* @prop {boolean} isViewing
* @prop {boolean} isVisible
* @prop {number} contextualCount
* @prop {Drupal~TabbingContext} tabbingContext
*/
defaults: /** @lends Drupal.contextualToolbar.StateModel# */ {
/**
* Indicates whether the toggle is currently in "view" or "edit" mode.
*
* @type {boolean}
*/
isViewing: true,
/**
* Indicates whether the toggle should be visible or hidden. Automatically
* calculated, depends on contextualCount.
*
* @type {boolean}
*/
isVisible: false,
/**
* Tracks how many contextual links exist on the page.
*
* @type {number}
*/
contextualCount: 0,
/**
* A TabbingContext object as returned by {@link Drupal~TabbingManager}:
* the set of tabbable elements when edit mode is enabled.
*
* @type {?Drupal~TabbingContext}
*/
tabbingContext: null,
},
/**
* Models the state of the edit mode toggle.
*
* @constructs
*
* @augments Backbone.Model
*
* @param {object} attrs
* Attributes for the backbone model.
* @param {object} options
* An object with the following option:
* @param {Backbone.collection} options.contextualCollection
* The collection of {@link Drupal.contextual.StateModel} models that
* represent the contextual links on the page.
*/
initialize(attrs, options) {
// Respond to new/removed contextual links.
this.listenTo(
options.contextualCollection,
'reset remove add',
this.countContextualLinks,
);
this.listenTo(
options.contextualCollection,
'add',
this.lockNewContextualLinks,
);
// Automatically determine visibility.
this.listenTo(this, 'change:contextualCount', this.updateVisibility);
// Whenever edit mode is toggled, lock all contextual links.
this.listenTo(this, 'change:isViewing', (model, isViewing) => {
options.contextualCollection.each((contextualModel) => {
contextualModel.set('isLocked', !isViewing);
});
});
},
/**
* Tracks the number of contextual link models in the collection.
*
* @param {Drupal.contextual.StateModel} contextualModel
* The contextual links model that was added or removed.
* @param {Backbone.Collection} contextualCollection
* The collection of contextual link models.
*/
countContextualLinks(contextualModel, contextualCollection) {
this.set('contextualCount', contextualCollection.length);
},
/**
* Lock newly added contextual links if edit mode is enabled.
*
* @param {Drupal.contextual.StateModel} contextualModel
* The contextual links model that was added.
* @param {Backbone.Collection} [contextualCollection]
* The collection of contextual link models.
*/
lockNewContextualLinks(contextualModel, contextualCollection) {
if (!this.get('isViewing')) {
contextualModel.set('isLocked', true);
}
},
/**
* Automatically updates visibility of the view/edit mode toggle.
*/
updateVisibility() {
this.set('isVisible', this.get('contextualCount') > 0);
},
},
);
})(Drupal, Backbone);

View File

@@ -0,0 +1,122 @@
/**
* @file
* A Backbone View that provides the aural view of the edit mode toggle.
*/
(function ($, Drupal, Backbone, _) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextualToolbar.AuralView = Backbone.View.extend(
/** @lends Drupal.contextualToolbar.AuralView# */ {
/**
* Tracks whether the tabbing constraint announcement has been read once.
*
* @type {boolean}
*/
announcedOnce: false,
/**
* Renders the aural view of the edit mode toggle (screen reader support).
*
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* Options for the view.
*/
initialize(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));
this.manageTabbing();
},
/**
* {@inheritdoc}
*
* @return {Drupal.contextualToolbar.AuralView}
* The current contextual toolbar aural view.
*/
render() {
// 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.
*/
manageTabbing() {
let tabbingContext = this.model.get('tabbingContext');
// Always release an existing tabbing context.
if (tabbingContext) {
// Only announce release when the context was active.
if (tabbingContext.active) {
Drupal.announce(this.options.strings.tabbingReleased);
}
tabbingContext.release();
}
// 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() {
const 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
* The keypress event.
*/
onKeypress(event) {
// The first tab key press is tracked so that an announcement 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, _);

View File

@@ -0,0 +1,85 @@
/**
* @file
* A Backbone View that provides the visual view of the edit mode toggle.
*/
(function (Drupal, Backbone) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextualToolbar.VisualView = Backbone.View.extend(
/** @lends Drupal.contextualToolbar.VisualView# */ {
/**
* Events for the Backbone view.
*
* @return {object}
* A mapping of events to be used in the view.
*/
events() {
// Prevents delay and simulated mouse events.
const touchEndToClick = function (event) {
event.preventDefault();
event.target.click();
};
return {
click() {
this.model.set('isViewing', !this.model.get('isViewing'));
},
touchend: touchEndToClick,
};
},
/**
* Renders the visual view of the edit mode toggle.
*
* Listens to mouse & touch and handles edit mode toggle interactions.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'change:isViewing', this.persist);
},
/**
* {@inheritdoc}
*
* @return {Drupal.contextualToolbar.VisualView}
* The current contextual toolbar visual view.
*/
render() {
// Render the visibility.
this.$el.toggleClass('hidden', !this.model.get('isVisible'));
// Render the state.
this.$el
.find('button')
.toggleClass('is-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.StateModel} model
* A {@link Drupal.contextualToolbar.StateModel} model.
* @param {boolean} isViewing
* The value of the isViewing attribute in the model.
*/
persist(model, isViewing) {
if (!isViewing) {
localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false');
} else {
localStorage.removeItem('Drupal.contextualToolbar.isViewing');
}
},
},
);
})(Drupal, Backbone);

View File

@@ -0,0 +1,59 @@
/**
* @file
* A Backbone View that provides the aural view of a contextual link.
*/
(function (Drupal, Backbone) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextual.AuralView = Backbone.View.extend(
/** @lends Drupal.contextual.AuralView# */ {
/**
* Renders the aural view of a contextual link (i.e. screen reader support).
*
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* Options for the view.
*/
initialize(options) {
this.options = options;
this.listenTo(this.model, 'change', this.render);
// Initial render.
this.render();
},
/**
* {@inheritdoc}
*/
render() {
const 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.
const $trigger = this.$el.find('.trigger');
$trigger
.each((index, element) => {
element.textContent = 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);

View File

@@ -0,0 +1,62 @@
/**
* @file
* A Backbone View that provides keyboard interaction for a contextual link.
*/
(function (Drupal, Backbone) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextual.KeyboardView = Backbone.View.extend(
/** @lends Drupal.contextual.KeyboardView# */ {
/**
* @type {object}
*/
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.
const that = this;
this.timer = window.setTimeout(() => {
that.model.close().blur();
}, 150);
},
},
/**
* Provides keyboard interaction for a contextual link.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
/**
* 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.
*
* @type {NaN|number}
*/
this.timer = NaN;
},
/**
* Sets focus on the model; Clears the timer that dismisses the links.
*/
focus() {
// Clear the timeout that might have been set by blurring a link.
window.clearTimeout(this.timer);
this.model.focus();
},
},
);
})(Drupal, Backbone);

View File

@@ -0,0 +1,75 @@
/**
* @file
* A Backbone View that renders the visual view of a contextual region element.
*/
(function (Drupal, Backbone) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextual.RegionView = Backbone.View.extend(
/** @lends Drupal.contextual.RegionView# */ {
/**
* Events for the Backbone view.
*
* @return {object}
* A mapping of events to be used in the view.
*/
events() {
// Used for tracking the presence of touch events. When true, the
// mousemove and mouseenter event handlers are effectively disabled.
// This is used instead of preventDefault() on touchstart as some
// touchstart events are not cancelable.
let touchStart = false;
return {
touchstart() {
// Set to true so the mouseenter and mouseleave events that follow
// know to not execute any hover related logic.
touchStart = true;
},
mouseenter() {
if (!touchStart) {
this.model.set('regionIsHovered', true);
}
},
mouseleave() {
if (!touchStart) {
this.model.close().blur().set('regionIsHovered', false);
}
},
mousemove() {
// Because there are scenarios where there are both touchscreens
// and pointer devices, the touchStart flag should be set back to
// false after mouseenter and mouseleave complete. It will be set to
// true if another touchstart event occurs.
touchStart = false;
},
};
},
/**
* Renders the visual view of a contextual region element.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
this.listenTo(this.model, 'change:hasFocus', this.render);
},
/**
* {@inheritdoc}
*
* @return {Drupal.contextual.RegionView}
* The current contextual region view.
*/
render() {
this.$el.toggleClass('focus', this.model.get('hasFocus'));
return this;
},
},
);
})(Drupal, Backbone);

View File

@@ -0,0 +1,109 @@
/**
* @file
* A Backbone View that provides the visual view of a contextual link.
*/
(function (Drupal, Backbone) {
/**
* @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
* replacement.
*/
Drupal.contextual.VisualView = Backbone.View.extend(
/** @lends Drupal.contextual.VisualView# */ {
/**
* Events for the Backbone view.
*
* @return {object}
* A mapping of events to be used in the view.
*/
events() {
// Prevents delay and simulated mouse events.
const touchEndToClick = function (event) {
event.preventDefault();
event.target.click();
};
// Used for tracking the presence of touch events. When true, the
// mousemove and mouseenter event handlers are effectively disabled.
// This is used instead of preventDefault() on touchstart as some
// touchstart events are not cancelable.
let touchStart = false;
return {
touchstart() {
// Set to true so the mouseenter events that follows knows to not
// execute any hover related logic.
touchStart = true;
},
mouseenter() {
// We only want mouse hover events on non-touch.
if (!touchStart) {
this.model.focus();
}
},
mousemove() {
// Because there are scenarios where there are both touchscreens
// and pointer devices, the touchStart flag should be set back to
// false after mouseenter and mouseleave complete. It will be set to
// true if another touchstart event occurs.
touchStart = false;
},
'click .trigger': function () {
this.model.toggleOpen();
},
'touchend .trigger': touchEndToClick,
'click .contextual-links a': function () {
this.model.close().blur();
},
'touchend .contextual-links a': touchEndToClick,
};
},
/**
* Renders the visual view of a contextual link. Listens to mouse & touch.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
this.listenTo(this.model, 'change', this.render);
},
/**
* {@inheritdoc}
*
* @return {Drupal.contextual.VisualView}
* The current contextual visual view.
*/
render() {
const 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.
const 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);