summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/contextual/js/contextual.js
diff options
context:
space:
mode:
Diffstat (limited to 'core/modules/contextual/js/contextual.js')
-rw-r--r--core/modules/contextual/js/contextual.js280
1 files changed, 213 insertions, 67 deletions
diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index f4abf7ec157..b5fe7d094fb 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -1,175 +1,321 @@
/**
-* DO NOT EDIT THIS FILE.
-* See the following change record for more information,
-* https://www.drupal.org/node/2815083
-* @preserve
-**/
+ * @file
+ * Attaches behaviors for the Contextual module.
+ */
(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
- const options = $.extend(drupalSettings.contextual, {
- strings: {
- open: Drupal.t('Open'),
- close: Drupal.t('Close')
- }
- });
- const cachedPermissionsHash = storage.getItem('Drupal.contextual.permissionsHash');
- const permissionsHash = drupalSettings.user.permissionsHash;
+ 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.substring(0, 18) === 'Drupal.contextual.') {
- storage.removeItem(key);
- }
- });
+ _.chain(storage)
+ .keys()
+ .each((key) => {
+ if (key.substring(0, 18) === '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.parents('.contextual-region').eq(-1).find('.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');
- $nestedContextual.css({
- top: $nestedContextual.position().top + height
- });
+
+ // Adjust nested contextual link's position.
+ $nestedContextual.css({ 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.html(html).addClass('contextual').prepend(Drupal.theme('contextualTrigger'));
- const destination = `destination=${Drupal.encodePath(Drupal.url(drupalSettings.path.currentPath))}`;
+
+ $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),
+ )}`;
$contextual.find('.contextual-links a').each(function () {
const url = this.getAttribute('href');
const glue = url.indexOf('?') === -1 ? '?' : '&';
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
+ title,
});
- const viewOptions = $.extend({
- el: $contextual,
- model
- }, options);
+ 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)
+ keyboard: new contextual.KeyboardView(viewOptions),
});
- contextual.regionViews.push(new contextual.RegionView($.extend({
- el: $region,
- model
- }, options)));
+ 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);
- $(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:10.0.0. There is no replacement.'
- }));
+
+ // 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:10.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);
- let $placeholders = $(once('contextual-render', '[data-contextual-id]', 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')
+ token: $(this).attr('data-contextual-token'),
});
});
+
const uncachedIDs = [];
const uncachedTokens = [];
- ids.forEach(contextualID => {
+ 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);
+ 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
- },
+ 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) {
- $placeholders = $context.find(`[data-contextual-id="${contextualID}"]`);
+ // 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:10.0.0. There is no
+ * replacement.
+ */
views: [],
- regionViews: []
+
+ /**
+ * 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:10.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:10.0.0. There is no
+ * replacement.
+ */
Drupal.contextual.collection = new Backbone.Collection([], {
- model: Drupal.contextual.StateModel
+ 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); \ No newline at end of file
+})(
+ jQuery,
+ Drupal,
+ drupalSettings,
+ _,
+ Backbone,
+ window.JSON,
+ window.sessionStorage,
+);