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 );
|