annotate core/modules/contextual/js/contextual.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 /**
Chris@0 2 * @file
Chris@0 3 * Attaches behaviors for the Contextual module.
Chris@0 4 */
Chris@0 5
Chris@17 6 (function($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
Chris@17 7 const options = $.extend(
Chris@17 8 drupalSettings.contextual,
Chris@0 9 // Merge strings on top of drupalSettings so that they are not mutable.
Chris@0 10 {
Chris@0 11 strings: {
Chris@0 12 open: Drupal.t('Open'),
Chris@0 13 close: Drupal.t('Close'),
Chris@0 14 },
Chris@0 15 },
Chris@0 16 );
Chris@0 17
Chris@0 18 // Clear the cached contextual links whenever the current user's set of
Chris@0 19 // permissions changes.
Chris@17 20 const cachedPermissionsHash = storage.getItem(
Chris@17 21 'Drupal.contextual.permissionsHash',
Chris@17 22 );
Chris@0 23 const permissionsHash = drupalSettings.user.permissionsHash;
Chris@0 24 if (cachedPermissionsHash !== permissionsHash) {
Chris@0 25 if (typeof permissionsHash === 'string') {
Chris@17 26 _.chain(storage)
Chris@17 27 .keys()
Chris@17 28 .each(key => {
Chris@17 29 if (key.substring(0, 18) === 'Drupal.contextual.') {
Chris@17 30 storage.removeItem(key);
Chris@17 31 }
Chris@17 32 });
Chris@0 33 }
Chris@0 34 storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
Chris@0 35 }
Chris@0 36
Chris@0 37 /**
Chris@0 38 * Determines if a contextual link is nested & overlapping, if so: adjusts it.
Chris@0 39 *
Chris@0 40 * This only deals with two levels of nesting; deeper levels are not touched.
Chris@0 41 *
Chris@0 42 * @param {jQuery} $contextual
Chris@0 43 * A contextual links placeholder DOM element, containing the actual
Chris@0 44 * contextual links as rendered by the server.
Chris@0 45 */
Chris@0 46 function adjustIfNestedAndOverlapping($contextual) {
Chris@0 47 const $contextuals = $contextual
Chris@0 48 // @todo confirm that .closest() is not sufficient
Chris@17 49 .parents('.contextual-region')
Chris@17 50 .eq(-1)
Chris@0 51 .find('.contextual');
Chris@0 52
Chris@0 53 // Early-return when there's no nesting.
Chris@0 54 if ($contextuals.length <= 1) {
Chris@0 55 return;
Chris@0 56 }
Chris@0 57
Chris@0 58 // If the two contextual links overlap, then we move the second one.
Chris@0 59 const firstTop = $contextuals.eq(0).offset().top;
Chris@0 60 const secondTop = $contextuals.eq(1).offset().top;
Chris@0 61 if (firstTop === secondTop) {
Chris@0 62 const $nestedContextual = $contextuals.eq(1);
Chris@0 63
Chris@0 64 // Retrieve height of nested contextual link.
Chris@0 65 let height = 0;
Chris@0 66 const $trigger = $nestedContextual.find('.trigger');
Chris@0 67 // Elements with the .visually-hidden class have no dimensions, so this
Chris@0 68 // class must be temporarily removed to the calculate the height.
Chris@0 69 $trigger.removeClass('visually-hidden');
Chris@0 70 height = $nestedContextual.height();
Chris@0 71 $trigger.addClass('visually-hidden');
Chris@0 72
Chris@0 73 // Adjust nested contextual link's position.
Chris@0 74 $nestedContextual.css({ top: $nestedContextual.position().top + height });
Chris@0 75 }
Chris@0 76 }
Chris@0 77
Chris@0 78 /**
Chris@17 79 * Initializes a contextual link: updates its DOM, sets up model and views.
Chris@17 80 *
Chris@17 81 * @param {jQuery} $contextual
Chris@17 82 * A contextual links placeholder DOM element, containing the actual
Chris@17 83 * contextual links as rendered by the server.
Chris@17 84 * @param {string} html
Chris@17 85 * The server-side rendered HTML for this contextual link.
Chris@17 86 */
Chris@17 87 function initContextual($contextual, html) {
Chris@17 88 const $region = $contextual.closest('.contextual-region');
Chris@17 89 const contextual = Drupal.contextual;
Chris@17 90
Chris@17 91 $contextual
Chris@17 92 // Update the placeholder to contain its rendered contextual links.
Chris@17 93 .html(html)
Chris@17 94 // Use the placeholder as a wrapper with a specific class to provide
Chris@17 95 // positioning and behavior attachment context.
Chris@17 96 .addClass('contextual')
Chris@17 97 // Ensure a trigger element exists before the actual contextual links.
Chris@17 98 .prepend(Drupal.theme('contextualTrigger'));
Chris@17 99
Chris@17 100 // Set the destination parameter on each of the contextual links.
Chris@17 101 const destination = `destination=${Drupal.encodePath(
Chris@17 102 Drupal.url(drupalSettings.path.currentPath),
Chris@17 103 )}`;
Chris@17 104 $contextual.find('.contextual-links a').each(function() {
Chris@17 105 const url = this.getAttribute('href');
Chris@17 106 const glue = url.indexOf('?') === -1 ? '?' : '&';
Chris@17 107 this.setAttribute('href', url + glue + destination);
Chris@17 108 });
Chris@17 109
Chris@17 110 // Create a model and the appropriate views.
Chris@17 111 const model = new contextual.StateModel({
Chris@17 112 title: $region
Chris@17 113 .find('h2')
Chris@17 114 .eq(0)
Chris@17 115 .text()
Chris@17 116 .trim(),
Chris@17 117 });
Chris@17 118 const viewOptions = $.extend({ el: $contextual, model }, options);
Chris@17 119 contextual.views.push({
Chris@17 120 visual: new contextual.VisualView(viewOptions),
Chris@17 121 aural: new contextual.AuralView(viewOptions),
Chris@17 122 keyboard: new contextual.KeyboardView(viewOptions),
Chris@17 123 });
Chris@17 124 contextual.regionViews.push(
Chris@17 125 new contextual.RegionView($.extend({ el: $region, model }, options)),
Chris@17 126 );
Chris@17 127
Chris@17 128 // Add the model to the collection. This must happen after the views have
Chris@17 129 // been associated with it, otherwise collection change event handlers can't
Chris@17 130 // trigger the model change event handler in its views.
Chris@17 131 contextual.collection.add(model);
Chris@17 132
Chris@17 133 // Let other JavaScript react to the adding of a new contextual link.
Chris@17 134 $(document).trigger('drupalContextualLinkAdded', {
Chris@17 135 $el: $contextual,
Chris@17 136 $region,
Chris@17 137 model,
Chris@17 138 });
Chris@17 139
Chris@17 140 // Fix visual collisions between contextual link triggers.
Chris@17 141 adjustIfNestedAndOverlapping($contextual);
Chris@17 142 }
Chris@17 143
Chris@17 144 /**
Chris@0 145 * Attaches outline behavior for regions associated with contextual links.
Chris@0 146 *
Chris@0 147 * Events
Chris@0 148 * Contextual triggers an event that can be used by other scripts.
Chris@0 149 * - drupalContextualLinkAdded: Triggered when a contextual link is added.
Chris@0 150 *
Chris@0 151 * @type {Drupal~behavior}
Chris@0 152 *
Chris@0 153 * @prop {Drupal~behaviorAttach} attach
Chris@0 154 * Attaches the outline behavior to the right context.
Chris@0 155 */
Chris@0 156 Drupal.behaviors.contextual = {
Chris@0 157 attach(context) {
Chris@0 158 const $context = $(context);
Chris@0 159
Chris@0 160 // Find all contextual links placeholders, if any.
Chris@17 161 let $placeholders = $context
Chris@17 162 .find('[data-contextual-id]')
Chris@17 163 .once('contextual-render');
Chris@0 164 if ($placeholders.length === 0) {
Chris@0 165 return;
Chris@0 166 }
Chris@0 167
Chris@0 168 // Collect the IDs for all contextual links placeholders.
Chris@0 169 const ids = [];
Chris@17 170 $placeholders.each(function() {
Chris@17 171 ids.push({
Chris@17 172 id: $(this).attr('data-contextual-id'),
Chris@17 173 token: $(this).attr('data-contextual-token'),
Chris@17 174 });
Chris@0 175 });
Chris@0 176
Chris@17 177 const uncachedIDs = [];
Chris@17 178 const uncachedTokens = [];
Chris@17 179 ids.forEach(contextualID => {
Chris@17 180 const html = storage.getItem(`Drupal.contextual.${contextualID.id}`);
Chris@0 181 if (html && html.length) {
Chris@0 182 // Initialize after the current execution cycle, to make the AJAX
Chris@0 183 // request for retrieving the uncached contextual links as soon as
Chris@0 184 // possible, but also to ensure that other Drupal behaviors have had
Chris@0 185 // the chance to set up an event listener on the Backbone collection
Chris@0 186 // Drupal.contextual.collection.
Chris@0 187 window.setTimeout(() => {
Chris@17 188 initContextual(
Chris@17 189 $context.find(`[data-contextual-id="${contextualID.id}"]`),
Chris@17 190 html,
Chris@17 191 );
Chris@0 192 });
Chris@17 193 return;
Chris@0 194 }
Chris@17 195 uncachedIDs.push(contextualID.id);
Chris@17 196 uncachedTokens.push(contextualID.token);
Chris@0 197 });
Chris@0 198
Chris@0 199 // Perform an AJAX request to let the server render the contextual links
Chris@0 200 // for each of the placeholders.
Chris@0 201 if (uncachedIDs.length > 0) {
Chris@0 202 $.ajax({
Chris@0 203 url: Drupal.url('contextual/render'),
Chris@0 204 type: 'POST',
Chris@17 205 data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
Chris@0 206 dataType: 'json',
Chris@0 207 success(results) {
Chris@0 208 _.each(results, (html, contextualID) => {
Chris@0 209 // Store the metadata.
Chris@0 210 storage.setItem(`Drupal.contextual.${contextualID}`, html);
Chris@0 211 // If the rendered contextual links are empty, then the current
Chris@0 212 // user does not have permission to access the associated links:
Chris@0 213 // don't render anything.
Chris@0 214 if (html.length > 0) {
Chris@0 215 // Update the placeholders to contain its rendered contextual
Chris@0 216 // links. Usually there will only be one placeholder, but it's
Chris@0 217 // possible for multiple identical placeholders exist on the
Chris@0 218 // page (probably because the same content appears more than
Chris@0 219 // once).
Chris@17 220 $placeholders = $context.find(
Chris@17 221 `[data-contextual-id="${contextualID}"]`,
Chris@17 222 );
Chris@0 223
Chris@0 224 // Initialize the contextual links.
Chris@0 225 for (let i = 0; i < $placeholders.length; i++) {
Chris@0 226 initContextual($placeholders.eq(i), html);
Chris@0 227 }
Chris@0 228 }
Chris@0 229 });
Chris@0 230 },
Chris@0 231 });
Chris@0 232 }
Chris@0 233 },
Chris@0 234 };
Chris@0 235
Chris@0 236 /**
Chris@0 237 * Namespace for contextual related functionality.
Chris@0 238 *
Chris@0 239 * @namespace
Chris@0 240 */
Chris@0 241 Drupal.contextual = {
Chris@0 242 /**
Chris@0 243 * The {@link Drupal.contextual.View} instances associated with each list
Chris@0 244 * element of contextual links.
Chris@0 245 *
Chris@0 246 * @type {Array}
Chris@0 247 */
Chris@0 248 views: [],
Chris@0 249
Chris@0 250 /**
Chris@0 251 * The {@link Drupal.contextual.RegionView} instances associated with each
Chris@0 252 * contextual region element.
Chris@0 253 *
Chris@0 254 * @type {Array}
Chris@0 255 */
Chris@0 256 regionViews: [],
Chris@0 257 };
Chris@0 258
Chris@0 259 /**
Chris@0 260 * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
Chris@0 261 *
Chris@0 262 * @type {Backbone.Collection}
Chris@0 263 */
Chris@17 264 Drupal.contextual.collection = new Backbone.Collection([], {
Chris@17 265 model: Drupal.contextual.StateModel,
Chris@17 266 });
Chris@0 267
Chris@0 268 /**
Chris@0 269 * A trigger is an interactive element often bound to a click handler.
Chris@0 270 *
Chris@0 271 * @return {string}
Chris@0 272 * A string representing a DOM fragment.
Chris@0 273 */
Chris@17 274 Drupal.theme.contextualTrigger = function() {
Chris@0 275 return '<button class="trigger visually-hidden focusable" type="button"></button>';
Chris@0 276 };
Chris@14 277
Chris@14 278 /**
Chris@14 279 * Bind Ajax contextual links when added.
Chris@14 280 *
Chris@14 281 * @param {jQuery.Event} event
Chris@14 282 * The `drupalContextualLinkAdded` event.
Chris@14 283 * @param {object} data
Chris@14 284 * An object containing the data relevant to the event.
Chris@14 285 *
Chris@14 286 * @listens event:drupalContextualLinkAdded
Chris@14 287 */
Chris@14 288 $(document).on('drupalContextualLinkAdded', (event, data) => {
Chris@14 289 Drupal.ajax.bindAjaxLinks(data.$el[0]);
Chris@14 290 });
Chris@17 291 })(
Chris@17 292 jQuery,
Chris@17 293 Drupal,
Chris@17 294 drupalSettings,
Chris@17 295 _,
Chris@17 296 Backbone,
Chris@17 297 window.JSON,
Chris@17 298 window.sessionStorage,
Chris@17 299 );