diff options
Diffstat (limited to 'core/modules/contextual/js/contextual.js')
-rw-r--r-- | core/modules/contextual/js/contextual.js | 280 |
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, +); |