Mercurial > hg > isophonics-drupal-site
comparison core/misc/tabledrag.es6.js @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 /** | |
2 * @file | |
3 * Provide dragging capabilities to admin uis. | |
4 */ | |
5 | |
6 /** | |
7 * Triggers when weights columns are toggled. | |
8 * | |
9 * @event columnschange | |
10 */ | |
11 | |
12 (function ($, Drupal, drupalSettings) { | |
13 /** | |
14 * Store the state of weight columns display for all tables. | |
15 * | |
16 * Default value is to hide weight columns. | |
17 */ | |
18 let showWeight = JSON.parse(localStorage.getItem('Drupal.tableDrag.showWeight')); | |
19 | |
20 /** | |
21 * Drag and drop table rows with field manipulation. | |
22 * | |
23 * Using the drupal_attach_tabledrag() function, any table with weights or | |
24 * parent relationships may be made into draggable tables. Columns containing | |
25 * a field may optionally be hidden, providing a better user experience. | |
26 * | |
27 * Created tableDrag instances may be modified with custom behaviors by | |
28 * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods. | |
29 * See blocks.js for an example of adding additional functionality to | |
30 * tableDrag. | |
31 * | |
32 * @type {Drupal~behavior} | |
33 */ | |
34 Drupal.behaviors.tableDrag = { | |
35 attach(context, settings) { | |
36 function initTableDrag(table, base) { | |
37 if (table.length) { | |
38 // Create the new tableDrag instance. Save in the Drupal variable | |
39 // to allow other scripts access to the object. | |
40 Drupal.tableDrag[base] = new Drupal.tableDrag(table[0], settings.tableDrag[base]); | |
41 } | |
42 } | |
43 | |
44 for (const base in settings.tableDrag) { | |
45 if (settings.tableDrag.hasOwnProperty(base)) { | |
46 initTableDrag($(context).find(`#${base}`).once('tabledrag'), base); | |
47 } | |
48 } | |
49 }, | |
50 }; | |
51 | |
52 /** | |
53 * Provides table and field manipulation. | |
54 * | |
55 * @constructor | |
56 * | |
57 * @param {HTMLElement} table | |
58 * DOM object for the table to be made draggable. | |
59 * @param {object} tableSettings | |
60 * Settings for the table added via drupal_add_dragtable(). | |
61 */ | |
62 Drupal.tableDrag = function (table, tableSettings) { | |
63 const self = this; | |
64 const $table = $(table); | |
65 | |
66 /** | |
67 * @type {jQuery} | |
68 */ | |
69 this.$table = $(table); | |
70 | |
71 /** | |
72 * | |
73 * @type {HTMLElement} | |
74 */ | |
75 this.table = table; | |
76 | |
77 /** | |
78 * @type {object} | |
79 */ | |
80 this.tableSettings = tableSettings; | |
81 | |
82 /** | |
83 * Used to hold information about a current drag operation. | |
84 * | |
85 * @type {?HTMLElement} | |
86 */ | |
87 this.dragObject = null; | |
88 | |
89 /** | |
90 * Provides operations for row manipulation. | |
91 * | |
92 * @type {?HTMLElement} | |
93 */ | |
94 this.rowObject = null; | |
95 | |
96 /** | |
97 * Remember the previous element. | |
98 * | |
99 * @type {?HTMLElement} | |
100 */ | |
101 this.oldRowElement = null; | |
102 | |
103 /** | |
104 * Used to determine up or down direction from last mouse move. | |
105 * | |
106 * @type {number} | |
107 */ | |
108 this.oldY = 0; | |
109 | |
110 /** | |
111 * Whether anything in the entire table has changed. | |
112 * | |
113 * @type {bool} | |
114 */ | |
115 this.changed = false; | |
116 | |
117 /** | |
118 * Maximum amount of allowed parenting. | |
119 * | |
120 * @type {number} | |
121 */ | |
122 this.maxDepth = 0; | |
123 | |
124 /** | |
125 * Direction of the table. | |
126 * | |
127 * @type {number} | |
128 */ | |
129 this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1; | |
130 | |
131 /** | |
132 * | |
133 * @type {bool} | |
134 */ | |
135 this.striping = $(this.table).data('striping') === 1; | |
136 | |
137 /** | |
138 * Configure the scroll settings. | |
139 * | |
140 * @type {object} | |
141 * | |
142 * @prop {number} amount | |
143 * @prop {number} interval | |
144 * @prop {number} trigger | |
145 */ | |
146 this.scrollSettings = { amount: 4, interval: 50, trigger: 70 }; | |
147 | |
148 /** | |
149 * | |
150 * @type {?number} | |
151 */ | |
152 this.scrollInterval = null; | |
153 | |
154 /** | |
155 * | |
156 * @type {number} | |
157 */ | |
158 this.scrollY = 0; | |
159 | |
160 /** | |
161 * | |
162 * @type {number} | |
163 */ | |
164 this.windowHeight = 0; | |
165 | |
166 /** | |
167 * Check this table's settings for parent relationships. | |
168 * | |
169 * For efficiency, large sections of code can be skipped if we don't need to | |
170 * track horizontal movement and indentations. | |
171 * | |
172 * @type {bool} | |
173 */ | |
174 this.indentEnabled = false; | |
175 for (const group in tableSettings) { | |
176 if (tableSettings.hasOwnProperty(group)) { | |
177 for (const n in tableSettings[group]) { | |
178 if (tableSettings[group].hasOwnProperty(n)) { | |
179 if (tableSettings[group][n].relationship === 'parent') { | |
180 this.indentEnabled = true; | |
181 } | |
182 if (tableSettings[group][n].limit > 0) { | |
183 this.maxDepth = tableSettings[group][n].limit; | |
184 } | |
185 } | |
186 } | |
187 } | |
188 } | |
189 if (this.indentEnabled) { | |
190 /** | |
191 * Total width of indents, set in makeDraggable. | |
192 * | |
193 * @type {number} | |
194 */ | |
195 this.indentCount = 1; | |
196 // Find the width of indentations to measure mouse movements against. | |
197 // Because the table doesn't need to start with any indentations, we | |
198 // manually append 2 indentations in the first draggable row, measure | |
199 // the offset, then remove. | |
200 const indent = Drupal.theme('tableDragIndentation'); | |
201 const testRow = $('<tr/>').addClass('draggable').appendTo(table); | |
202 const testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent); | |
203 const $indentation = testCell.find('.js-indentation'); | |
204 | |
205 /** | |
206 * | |
207 * @type {number} | |
208 */ | |
209 this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft; | |
210 testRow.remove(); | |
211 } | |
212 | |
213 // Make each applicable row draggable. | |
214 // Match immediate children of the parent element to allow nesting. | |
215 $table.find('> tr.draggable, > tbody > tr.draggable').each(function () { | |
216 self.makeDraggable(this); | |
217 }); | |
218 | |
219 // Add a link before the table for users to show or hide weight columns. | |
220 $table.before($('<button type="button" class="link tabledrag-toggle-weight"></button>') | |
221 .attr('title', Drupal.t('Re-order rows by numerical weight instead of dragging.')) | |
222 .on('click', $.proxy(function (e) { | |
223 e.preventDefault(); | |
224 this.toggleColumns(); | |
225 }, this)) | |
226 .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>') | |
227 .parent(), | |
228 ); | |
229 | |
230 // Initialize the specified columns (for example, weight or parent columns) | |
231 // to show or hide according to user preference. This aids accessibility | |
232 // so that, e.g., screen reader users can choose to enter weight values and | |
233 // manipulate form elements directly, rather than using drag-and-drop.. | |
234 self.initColumns(); | |
235 | |
236 // Add event bindings to the document. The self variable is passed along | |
237 // as event handlers do not have direct access to the tableDrag object. | |
238 $(document).on('touchmove', event => self.dragRow(event.originalEvent.touches[0], self)); | |
239 $(document).on('touchend', event => self.dropRow(event.originalEvent.touches[0], self)); | |
240 $(document).on('mousemove pointermove', event => self.dragRow(event, self)); | |
241 $(document).on('mouseup pointerup', event => self.dropRow(event, self)); | |
242 | |
243 // React to localStorage event showing or hiding weight columns. | |
244 $(window).on('storage', $.proxy(function (e) { | |
245 // Only react to 'Drupal.tableDrag.showWeight' value change. | |
246 if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') { | |
247 // This was changed in another window, get the new value for this | |
248 // window. | |
249 showWeight = JSON.parse(e.originalEvent.newValue); | |
250 this.displayColumns(showWeight); | |
251 } | |
252 }, this)); | |
253 }; | |
254 | |
255 /** | |
256 * Initialize columns containing form elements to be hidden by default. | |
257 * | |
258 * Identify and mark each cell with a CSS class so we can easily toggle | |
259 * show/hide it. Finally, hide columns if user does not have a | |
260 * 'Drupal.tableDrag.showWeight' localStorage value. | |
261 */ | |
262 Drupal.tableDrag.prototype.initColumns = function () { | |
263 const $table = this.$table; | |
264 let hidden; | |
265 let cell; | |
266 let columnIndex; | |
267 for (const group in this.tableSettings) { | |
268 if (this.tableSettings.hasOwnProperty(group)) { | |
269 // Find the first field in this group. | |
270 for (const d in this.tableSettings[group]) { | |
271 if (this.tableSettings[group].hasOwnProperty(d)) { | |
272 const field = $table.find(`.${this.tableSettings[group][d].target}`).eq(0); | |
273 if (field.length && this.tableSettings[group][d].hidden) { | |
274 hidden = this.tableSettings[group][d].hidden; | |
275 cell = field.closest('td'); | |
276 break; | |
277 } | |
278 } | |
279 } | |
280 | |
281 // Mark the column containing this field so it can be hidden. | |
282 if (hidden && cell[0]) { | |
283 // Add 1 to our indexes. The nth-child selector is 1 based, not 0 | |
284 // based. Match immediate children of the parent element to allow | |
285 // nesting. | |
286 columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; | |
287 $table.find('> thead > tr, > tbody > tr, > tr').each(this.addColspanClass(columnIndex)); | |
288 } | |
289 } | |
290 } | |
291 this.displayColumns(showWeight); | |
292 }; | |
293 | |
294 /** | |
295 * Mark cells that have colspan. | |
296 * | |
297 * In order to adjust the colspan instead of hiding them altogether. | |
298 * | |
299 * @param {number} columnIndex | |
300 * The column index to add colspan class to. | |
301 * | |
302 * @return {function} | |
303 * Function to add colspan class. | |
304 */ | |
305 Drupal.tableDrag.prototype.addColspanClass = function (columnIndex) { | |
306 return function () { | |
307 // Get the columnIndex and adjust for any colspans in this row. | |
308 const $row = $(this); | |
309 let index = columnIndex; | |
310 const cells = $row.children(); | |
311 let cell; | |
312 cells.each(function (n) { | |
313 if (n < index && this.colSpan && this.colSpan > 1) { | |
314 index -= this.colSpan - 1; | |
315 } | |
316 }); | |
317 if (index > 0) { | |
318 cell = cells.filter(`:nth-child(${index})`); | |
319 if (cell[0].colSpan && cell[0].colSpan > 1) { | |
320 // If this cell has a colspan, mark it so we can reduce the colspan. | |
321 cell.addClass('tabledrag-has-colspan'); | |
322 } | |
323 else { | |
324 // Mark this cell so we can hide it. | |
325 cell.addClass('tabledrag-hide'); | |
326 } | |
327 } | |
328 }; | |
329 }; | |
330 | |
331 /** | |
332 * Hide or display weight columns. Triggers an event on change. | |
333 * | |
334 * @fires event:columnschange | |
335 * | |
336 * @param {bool} displayWeight | |
337 * 'true' will show weight columns. | |
338 */ | |
339 Drupal.tableDrag.prototype.displayColumns = function (displayWeight) { | |
340 if (displayWeight) { | |
341 this.showColumns(); | |
342 } | |
343 // Default action is to hide columns. | |
344 else { | |
345 this.hideColumns(); | |
346 } | |
347 // Trigger an event to allow other scripts to react to this display change. | |
348 // Force the extra parameter as a bool. | |
349 $('table').findOnce('tabledrag').trigger('columnschange', !!displayWeight); | |
350 }; | |
351 | |
352 /** | |
353 * Toggle the weight column depending on 'showWeight' value. | |
354 * | |
355 * Store only default override. | |
356 */ | |
357 Drupal.tableDrag.prototype.toggleColumns = function () { | |
358 showWeight = !showWeight; | |
359 this.displayColumns(showWeight); | |
360 if (showWeight) { | |
361 // Save default override. | |
362 localStorage.setItem('Drupal.tableDrag.showWeight', showWeight); | |
363 } | |
364 else { | |
365 // Reset the value to its default. | |
366 localStorage.removeItem('Drupal.tableDrag.showWeight'); | |
367 } | |
368 }; | |
369 | |
370 /** | |
371 * Hide the columns containing weight/parent form elements. | |
372 * | |
373 * Undo showColumns(). | |
374 */ | |
375 Drupal.tableDrag.prototype.hideColumns = function () { | |
376 const $tables = $('table').findOnce('tabledrag'); | |
377 // Hide weight/parent cells and headers. | |
378 $tables.find('.tabledrag-hide').css('display', 'none'); | |
379 // Show TableDrag handles. | |
380 $tables.find('.tabledrag-handle').css('display', ''); | |
381 // Reduce the colspan of any effected multi-span columns. | |
382 $tables.find('.tabledrag-has-colspan').each(function () { | |
383 this.colSpan = this.colSpan - 1; | |
384 }); | |
385 // Change link text. | |
386 $('.tabledrag-toggle-weight').text(Drupal.t('Show row weights')); | |
387 }; | |
388 | |
389 /** | |
390 * Show the columns containing weight/parent form elements. | |
391 * | |
392 * Undo hideColumns(). | |
393 */ | |
394 Drupal.tableDrag.prototype.showColumns = function () { | |
395 const $tables = $('table').findOnce('tabledrag'); | |
396 // Show weight/parent cells and headers. | |
397 $tables.find('.tabledrag-hide').css('display', ''); | |
398 // Hide TableDrag handles. | |
399 $tables.find('.tabledrag-handle').css('display', 'none'); | |
400 // Increase the colspan for any columns where it was previously reduced. | |
401 $tables.find('.tabledrag-has-colspan').each(function () { | |
402 this.colSpan = this.colSpan + 1; | |
403 }); | |
404 // Change link text. | |
405 $('.tabledrag-toggle-weight').text(Drupal.t('Hide row weights')); | |
406 }; | |
407 | |
408 /** | |
409 * Find the target used within a particular row and group. | |
410 * | |
411 * @param {string} group | |
412 * Group selector. | |
413 * @param {HTMLElement} row | |
414 * The row HTML element. | |
415 * | |
416 * @return {object} | |
417 * The table row settings. | |
418 */ | |
419 Drupal.tableDrag.prototype.rowSettings = function (group, row) { | |
420 const field = $(row).find(`.${group}`); | |
421 const tableSettingsGroup = this.tableSettings[group]; | |
422 for (const delta in tableSettingsGroup) { | |
423 if (tableSettingsGroup.hasOwnProperty(delta)) { | |
424 const targetClass = tableSettingsGroup[delta].target; | |
425 if (field.is(`.${targetClass}`)) { | |
426 // Return a copy of the row settings. | |
427 const rowSettings = {}; | |
428 for (const n in tableSettingsGroup[delta]) { | |
429 if (tableSettingsGroup[delta].hasOwnProperty(n)) { | |
430 rowSettings[n] = tableSettingsGroup[delta][n]; | |
431 } | |
432 } | |
433 return rowSettings; | |
434 } | |
435 } | |
436 } | |
437 }; | |
438 | |
439 /** | |
440 * Take an item and add event handlers to make it become draggable. | |
441 * | |
442 * @param {HTMLElement} item | |
443 * The item to add event handlers to. | |
444 */ | |
445 Drupal.tableDrag.prototype.makeDraggable = function (item) { | |
446 const self = this; | |
447 const $item = $(item); | |
448 // Add a class to the title link. | |
449 $item.find('td:first-of-type').find('a').addClass('menu-item__link'); | |
450 // Create the handle. | |
451 const handle = $('<a href="#" class="tabledrag-handle"><div class="handle"> </div></a>').attr('title', Drupal.t('Drag to re-order')); | |
452 // Insert the handle after indentations (if any). | |
453 const $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1); | |
454 if ($indentationLast.length) { | |
455 $indentationLast.after(handle); | |
456 // Update the total width of indentation in this entire table. | |
457 self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount); | |
458 } | |
459 else { | |
460 $item.find('td').eq(0).prepend(handle); | |
461 } | |
462 | |
463 handle.on('mousedown touchstart pointerdown', (event) => { | |
464 event.preventDefault(); | |
465 if (event.originalEvent.type === 'touchstart') { | |
466 event = event.originalEvent.touches[0]; | |
467 } | |
468 self.dragStart(event, self, item); | |
469 }); | |
470 | |
471 // Prevent the anchor tag from jumping us to the top of the page. | |
472 handle.on('click', (e) => { | |
473 e.preventDefault(); | |
474 }); | |
475 | |
476 // Set blur cleanup when a handle is focused. | |
477 handle.on('focus', () => { | |
478 self.safeBlur = true; | |
479 }); | |
480 | |
481 // On blur, fire the same function as a touchend/mouseup. This is used to | |
482 // update values after a row has been moved through the keyboard support. | |
483 handle.on('blur', (event) => { | |
484 if (self.rowObject && self.safeBlur) { | |
485 self.dropRow(event, self); | |
486 } | |
487 }); | |
488 | |
489 // Add arrow-key support to the handle. | |
490 handle.on('keydown', (event) => { | |
491 // If a rowObject doesn't yet exist and this isn't the tab key. | |
492 if (event.keyCode !== 9 && !self.rowObject) { | |
493 self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true); | |
494 } | |
495 | |
496 let keyChange = false; | |
497 let groupHeight; | |
498 | |
499 /* eslint-disable no-fallthrough */ | |
500 | |
501 switch (event.keyCode) { | |
502 // Left arrow. | |
503 case 37: | |
504 // Safari left arrow. | |
505 case 63234: | |
506 keyChange = true; | |
507 self.rowObject.indent(-1 * self.rtl); | |
508 break; | |
509 | |
510 // Up arrow. | |
511 case 38: | |
512 // Safari up arrow. | |
513 case 63232: | |
514 var $previousRow = $(self.rowObject.element).prev('tr:first-of-type'); | |
515 var previousRow = $previousRow.get(0); | |
516 while (previousRow && $previousRow.is(':hidden')) { | |
517 $previousRow = $(previousRow).prev('tr:first-of-type'); | |
518 previousRow = $previousRow.get(0); | |
519 } | |
520 if (previousRow) { | |
521 // Do not allow the onBlur cleanup. | |
522 self.safeBlur = false; | |
523 self.rowObject.direction = 'up'; | |
524 keyChange = true; | |
525 | |
526 if ($(item).is('.tabledrag-root')) { | |
527 // Swap with the previous top-level row. | |
528 groupHeight = 0; | |
529 while (previousRow && $previousRow.find('.js-indentation').length) { | |
530 $previousRow = $(previousRow).prev('tr:first-of-type'); | |
531 previousRow = $previousRow.get(0); | |
532 groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight; | |
533 } | |
534 if (previousRow) { | |
535 self.rowObject.swap('before', previousRow); | |
536 // No need to check for indentation, 0 is the only valid one. | |
537 window.scrollBy(0, -groupHeight); | |
538 } | |
539 } | |
540 else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) { | |
541 // Swap with the previous row (unless previous row is the first | |
542 // one and undraggable). | |
543 self.rowObject.swap('before', previousRow); | |
544 self.rowObject.interval = null; | |
545 self.rowObject.indent(0); | |
546 window.scrollBy(0, -parseInt(item.offsetHeight, 10)); | |
547 } | |
548 // Regain focus after the DOM manipulation. | |
549 handle.trigger('focus'); | |
550 } | |
551 break; | |
552 | |
553 // Right arrow. | |
554 case 39: | |
555 // Safari right arrow. | |
556 case 63235: | |
557 keyChange = true; | |
558 self.rowObject.indent(self.rtl); | |
559 break; | |
560 | |
561 // Down arrow. | |
562 case 40: | |
563 // Safari down arrow. | |
564 case 63233: | |
565 var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type'); | |
566 var nextRow = $nextRow.get(0); | |
567 while (nextRow && $nextRow.is(':hidden')) { | |
568 $nextRow = $(nextRow).next('tr:first-of-type'); | |
569 nextRow = $nextRow.get(0); | |
570 } | |
571 if (nextRow) { | |
572 // Do not allow the onBlur cleanup. | |
573 self.safeBlur = false; | |
574 self.rowObject.direction = 'down'; | |
575 keyChange = true; | |
576 | |
577 if ($(item).is('.tabledrag-root')) { | |
578 // Swap with the next group (necessarily a top-level one). | |
579 groupHeight = 0; | |
580 const nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false); | |
581 if (nextGroup) { | |
582 $(nextGroup.group).each(function () { | |
583 groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight; | |
584 }); | |
585 const nextGroupRow = $(nextGroup.group).eq(-1).get(0); | |
586 self.rowObject.swap('after', nextGroupRow); | |
587 // No need to check for indentation, 0 is the only valid one. | |
588 window.scrollBy(0, parseInt(groupHeight, 10)); | |
589 } | |
590 } | |
591 else { | |
592 // Swap with the next row. | |
593 self.rowObject.swap('after', nextRow); | |
594 self.rowObject.interval = null; | |
595 self.rowObject.indent(0); | |
596 window.scrollBy(0, parseInt(item.offsetHeight, 10)); | |
597 } | |
598 // Regain focus after the DOM manipulation. | |
599 handle.trigger('focus'); | |
600 } | |
601 break; | |
602 } | |
603 | |
604 /* eslint-enable no-fallthrough */ | |
605 | |
606 if (self.rowObject && self.rowObject.changed === true) { | |
607 $(item).addClass('drag'); | |
608 if (self.oldRowElement) { | |
609 $(self.oldRowElement).removeClass('drag-previous'); | |
610 } | |
611 self.oldRowElement = item; | |
612 if (self.striping === true) { | |
613 self.restripeTable(); | |
614 } | |
615 self.onDrag(); | |
616 } | |
617 | |
618 // Returning false if we have an arrow key to prevent scrolling. | |
619 if (keyChange) { | |
620 return false; | |
621 } | |
622 }); | |
623 | |
624 // Compatibility addition, return false on keypress to prevent unwanted | |
625 // scrolling. IE and Safari will suppress scrolling on keydown, but all | |
626 // other browsers need to return false on keypress. | |
627 // http://www.quirksmode.org/js/keys.html | |
628 handle.on('keypress', (event) => { | |
629 /* eslint-disable no-fallthrough */ | |
630 | |
631 switch (event.keyCode) { | |
632 // Left arrow. | |
633 case 37: | |
634 // Up arrow. | |
635 case 38: | |
636 // Right arrow. | |
637 case 39: | |
638 // Down arrow. | |
639 case 40: | |
640 return false; | |
641 } | |
642 | |
643 /* eslint-enable no-fallthrough */ | |
644 }); | |
645 }; | |
646 | |
647 /** | |
648 * Pointer event initiator, creates drag object and information. | |
649 * | |
650 * @param {jQuery.Event} event | |
651 * The event object that trigger the drag. | |
652 * @param {Drupal.tableDrag} self | |
653 * The drag handle. | |
654 * @param {HTMLElement} item | |
655 * The item that that is being dragged. | |
656 */ | |
657 Drupal.tableDrag.prototype.dragStart = function (event, self, item) { | |
658 // Create a new dragObject recording the pointer information. | |
659 self.dragObject = {}; | |
660 self.dragObject.initOffset = self.getPointerOffset(item, event); | |
661 self.dragObject.initPointerCoords = self.pointerCoords(event); | |
662 if (self.indentEnabled) { | |
663 self.dragObject.indentPointerPos = self.dragObject.initPointerCoords; | |
664 } | |
665 | |
666 // If there's a lingering row object from the keyboard, remove its focus. | |
667 if (self.rowObject) { | |
668 $(self.rowObject.element).find('a.tabledrag-handle').trigger('blur'); | |
669 } | |
670 | |
671 // Create a new rowObject for manipulation of this row. | |
672 self.rowObject = new self.row(item, 'pointer', self.indentEnabled, self.maxDepth, true); | |
673 | |
674 // Save the position of the table. | |
675 self.table.topY = $(self.table).offset().top; | |
676 self.table.bottomY = self.table.topY + self.table.offsetHeight; | |
677 | |
678 // Add classes to the handle and row. | |
679 $(item).addClass('drag'); | |
680 | |
681 // Set the document to use the move cursor during drag. | |
682 $('body').addClass('drag'); | |
683 if (self.oldRowElement) { | |
684 $(self.oldRowElement).removeClass('drag-previous'); | |
685 } | |
686 }; | |
687 | |
688 /** | |
689 * Pointer movement handler, bound to document. | |
690 * | |
691 * @param {jQuery.Event} event | |
692 * The pointer event. | |
693 * @param {Drupal.tableDrag} self | |
694 * The tableDrag instance. | |
695 * | |
696 * @return {bool|undefined} | |
697 * Undefined if no dragObject is defined, false otherwise. | |
698 */ | |
699 Drupal.tableDrag.prototype.dragRow = function (event, self) { | |
700 if (self.dragObject) { | |
701 self.currentPointerCoords = self.pointerCoords(event); | |
702 const y = self.currentPointerCoords.y - self.dragObject.initOffset.y; | |
703 const x = self.currentPointerCoords.x - self.dragObject.initOffset.x; | |
704 | |
705 // Check for row swapping and vertical scrolling. | |
706 if (y !== self.oldY) { | |
707 self.rowObject.direction = y > self.oldY ? 'down' : 'up'; | |
708 // Update the old value. | |
709 self.oldY = y; | |
710 // Check if the window should be scrolled (and how fast). | |
711 const scrollAmount = self.checkScroll(self.currentPointerCoords.y); | |
712 // Stop any current scrolling. | |
713 clearInterval(self.scrollInterval); | |
714 // Continue scrolling if the mouse has moved in the scroll direction. | |
715 if (scrollAmount > 0 && self.rowObject.direction === 'down' || scrollAmount < 0 && self.rowObject.direction === 'up') { | |
716 self.setScroll(scrollAmount); | |
717 } | |
718 | |
719 // If we have a valid target, perform the swap and restripe the table. | |
720 const currentRow = self.findDropTargetRow(x, y); | |
721 if (currentRow) { | |
722 if (self.rowObject.direction === 'down') { | |
723 self.rowObject.swap('after', currentRow, self); | |
724 } | |
725 else { | |
726 self.rowObject.swap('before', currentRow, self); | |
727 } | |
728 if (self.striping === true) { | |
729 self.restripeTable(); | |
730 } | |
731 } | |
732 } | |
733 | |
734 // Similar to row swapping, handle indentations. | |
735 if (self.indentEnabled) { | |
736 const xDiff = self.currentPointerCoords.x - self.dragObject.indentPointerPos.x; | |
737 // Set the number of indentations the pointer has been moved left or | |
738 // right. | |
739 const indentDiff = Math.round(xDiff / self.indentAmount); | |
740 // Indent the row with our estimated diff, which may be further | |
741 // restricted according to the rows around this row. | |
742 const indentChange = self.rowObject.indent(indentDiff); | |
743 // Update table and pointer indentations. | |
744 self.dragObject.indentPointerPos.x += self.indentAmount * indentChange * self.rtl; | |
745 self.indentCount = Math.max(self.indentCount, self.rowObject.indents); | |
746 } | |
747 | |
748 return false; | |
749 } | |
750 }; | |
751 | |
752 /** | |
753 * Pointerup behavior. | |
754 * | |
755 * @param {jQuery.Event} event | |
756 * The pointer event. | |
757 * @param {Drupal.tableDrag} self | |
758 * The tableDrag instance. | |
759 */ | |
760 Drupal.tableDrag.prototype.dropRow = function (event, self) { | |
761 let droppedRow; | |
762 let $droppedRow; | |
763 | |
764 // Drop row functionality. | |
765 if (self.rowObject !== null) { | |
766 droppedRow = self.rowObject.element; | |
767 $droppedRow = $(droppedRow); | |
768 // The row is already in the right place so we just release it. | |
769 if (self.rowObject.changed === true) { | |
770 // Update the fields in the dropped row. | |
771 self.updateFields(droppedRow); | |
772 | |
773 // If a setting exists for affecting the entire group, update all the | |
774 // fields in the entire dragged group. | |
775 for (const group in self.tableSettings) { | |
776 if (self.tableSettings.hasOwnProperty(group)) { | |
777 const rowSettings = self.rowSettings(group, droppedRow); | |
778 if (rowSettings.relationship === 'group') { | |
779 for (const n in self.rowObject.children) { | |
780 if (self.rowObject.children.hasOwnProperty(n)) { | |
781 self.updateField(self.rowObject.children[n], group); | |
782 } | |
783 } | |
784 } | |
785 } | |
786 } | |
787 | |
788 self.rowObject.markChanged(); | |
789 if (self.changed === false) { | |
790 $(Drupal.theme('tableDragChangedWarning')).insertBefore(self.table).hide().fadeIn('slow'); | |
791 self.changed = true; | |
792 } | |
793 } | |
794 | |
795 if (self.indentEnabled) { | |
796 self.rowObject.removeIndentClasses(); | |
797 } | |
798 if (self.oldRowElement) { | |
799 $(self.oldRowElement).removeClass('drag-previous'); | |
800 } | |
801 $droppedRow.removeClass('drag').addClass('drag-previous'); | |
802 self.oldRowElement = droppedRow; | |
803 self.onDrop(); | |
804 self.rowObject = null; | |
805 } | |
806 | |
807 // Functionality specific only to pointerup events. | |
808 if (self.dragObject !== null) { | |
809 self.dragObject = null; | |
810 $('body').removeClass('drag'); | |
811 clearInterval(self.scrollInterval); | |
812 } | |
813 }; | |
814 | |
815 /** | |
816 * Get the coordinates from the event (allowing for browser differences). | |
817 * | |
818 * @param {jQuery.Event} event | |
819 * The pointer event. | |
820 * | |
821 * @return {object} | |
822 * An object with `x` and `y` keys indicating the position. | |
823 */ | |
824 Drupal.tableDrag.prototype.pointerCoords = function (event) { | |
825 if (event.pageX || event.pageY) { | |
826 return { x: event.pageX, y: event.pageY }; | |
827 } | |
828 return { | |
829 x: event.clientX + document.body.scrollLeft - document.body.clientLeft, | |
830 y: event.clientY + document.body.scrollTop - document.body.clientTop, | |
831 }; | |
832 }; | |
833 | |
834 /** | |
835 * Get the event offset from the target element. | |
836 * | |
837 * Given a target element and a pointer event, get the event offset from that | |
838 * element. To do this we need the element's position and the target position. | |
839 * | |
840 * @param {HTMLElement} target | |
841 * The target HTML element. | |
842 * @param {jQuery.Event} event | |
843 * The pointer event. | |
844 * | |
845 * @return {object} | |
846 * An object with `x` and `y` keys indicating the position. | |
847 */ | |
848 Drupal.tableDrag.prototype.getPointerOffset = function (target, event) { | |
849 const docPos = $(target).offset(); | |
850 const pointerPos = this.pointerCoords(event); | |
851 return { x: pointerPos.x - docPos.left, y: pointerPos.y - docPos.top }; | |
852 }; | |
853 | |
854 /** | |
855 * Find the row the mouse is currently over. | |
856 * | |
857 * This row is then taken and swapped with the one being dragged. | |
858 * | |
859 * @param {number} x | |
860 * The x coordinate of the mouse on the page (not the screen). | |
861 * @param {number} y | |
862 * The y coordinate of the mouse on the page (not the screen). | |
863 * | |
864 * @return {*} | |
865 * The drop target row, if found. | |
866 */ | |
867 Drupal.tableDrag.prototype.findDropTargetRow = function (x, y) { | |
868 const rows = $(this.table.tBodies[0].rows).not(':hidden'); | |
869 for (let n = 0; n < rows.length; n++) { | |
870 let row = rows[n]; | |
871 let $row = $(row); | |
872 const rowY = $row.offset().top; | |
873 var rowHeight; | |
874 // Because Safari does not report offsetHeight on table rows, but does on | |
875 // table cells, grab the firstChild of the row and use that instead. | |
876 // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari. | |
877 if (row.offsetHeight === 0) { | |
878 rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2; | |
879 } | |
880 // Other browsers. | |
881 else { | |
882 rowHeight = parseInt(row.offsetHeight, 10) / 2; | |
883 } | |
884 | |
885 // Because we always insert before, we need to offset the height a bit. | |
886 if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) { | |
887 if (this.indentEnabled) { | |
888 // Check that this row is not a child of the row being dragged. | |
889 for (n in this.rowObject.group) { | |
890 if (this.rowObject.group[n] === row) { | |
891 return null; | |
892 } | |
893 } | |
894 } | |
895 else { | |
896 // Do not allow a row to be swapped with itself. | |
897 if (row === this.rowObject.element) { | |
898 return null; | |
899 } | |
900 } | |
901 | |
902 // Check that swapping with this row is allowed. | |
903 if (!this.rowObject.isValidSwap(row)) { | |
904 return null; | |
905 } | |
906 | |
907 // We may have found the row the mouse just passed over, but it doesn't | |
908 // take into account hidden rows. Skip backwards until we find a | |
909 // draggable row. | |
910 while ($row.is(':hidden') && $row.prev('tr').is(':hidden')) { | |
911 $row = $row.prev('tr:first-of-type'); | |
912 row = $row.get(0); | |
913 } | |
914 return row; | |
915 } | |
916 } | |
917 return null; | |
918 }; | |
919 | |
920 /** | |
921 * After the row is dropped, update the table fields. | |
922 * | |
923 * @param {HTMLElement} changedRow | |
924 * DOM object for the row that was just dropped. | |
925 */ | |
926 Drupal.tableDrag.prototype.updateFields = function (changedRow) { | |
927 for (const group in this.tableSettings) { | |
928 if (this.tableSettings.hasOwnProperty(group)) { | |
929 // Each group may have a different setting for relationship, so we find | |
930 // the source rows for each separately. | |
931 this.updateField(changedRow, group); | |
932 } | |
933 } | |
934 }; | |
935 | |
936 /** | |
937 * After the row is dropped, update a single table field. | |
938 * | |
939 * @param {HTMLElement} changedRow | |
940 * DOM object for the row that was just dropped. | |
941 * @param {string} group | |
942 * The settings group on which field updates will occur. | |
943 */ | |
944 Drupal.tableDrag.prototype.updateField = function (changedRow, group) { | |
945 let rowSettings = this.rowSettings(group, changedRow); | |
946 const $changedRow = $(changedRow); | |
947 let sourceRow; | |
948 let $previousRow; | |
949 let previousRow; | |
950 let useSibling; | |
951 // Set the row as its own target. | |
952 if (rowSettings.relationship === 'self' || rowSettings.relationship === 'group') { | |
953 sourceRow = changedRow; | |
954 } | |
955 // Siblings are easy, check previous and next rows. | |
956 else if (rowSettings.relationship === 'sibling') { | |
957 $previousRow = $changedRow.prev('tr:first-of-type'); | |
958 previousRow = $previousRow.get(0); | |
959 const $nextRow = $changedRow.next('tr:first-of-type'); | |
960 const nextRow = $nextRow.get(0); | |
961 sourceRow = changedRow; | |
962 if ($previousRow.is('.draggable') && $previousRow.find(`.${group}`).length) { | |
963 if (this.indentEnabled) { | |
964 if ($previousRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { | |
965 sourceRow = previousRow; | |
966 } | |
967 } | |
968 else { | |
969 sourceRow = previousRow; | |
970 } | |
971 } | |
972 else if ($nextRow.is('.draggable') && $nextRow.find(`.${group}`).length) { | |
973 if (this.indentEnabled) { | |
974 if ($nextRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { | |
975 sourceRow = nextRow; | |
976 } | |
977 } | |
978 else { | |
979 sourceRow = nextRow; | |
980 } | |
981 } | |
982 } | |
983 // Parents, look up the tree until we find a field not in this group. | |
984 // Go up as many parents as indentations in the changed row. | |
985 else if (rowSettings.relationship === 'parent') { | |
986 $previousRow = $changedRow.prev('tr'); | |
987 previousRow = $previousRow; | |
988 while ($previousRow.length && $previousRow.find('.js-indentation').length >= this.rowObject.indents) { | |
989 $previousRow = $previousRow.prev('tr'); | |
990 previousRow = $previousRow; | |
991 } | |
992 // If we found a row. | |
993 if ($previousRow.length) { | |
994 sourceRow = $previousRow.get(0); | |
995 } | |
996 // Otherwise we went all the way to the left of the table without finding | |
997 // a parent, meaning this item has been placed at the root level. | |
998 else { | |
999 // Use the first row in the table as source, because it's guaranteed to | |
1000 // be at the root level. Find the first item, then compare this row | |
1001 // against it as a sibling. | |
1002 sourceRow = $(this.table).find('tr.draggable:first-of-type').get(0); | |
1003 if (sourceRow === this.rowObject.element) { | |
1004 sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); | |
1005 } | |
1006 useSibling = true; | |
1007 } | |
1008 } | |
1009 | |
1010 // Because we may have moved the row from one category to another, | |
1011 // take a look at our sibling and borrow its sources and targets. | |
1012 this.copyDragClasses(sourceRow, changedRow, group); | |
1013 rowSettings = this.rowSettings(group, changedRow); | |
1014 | |
1015 // In the case that we're looking for a parent, but the row is at the top | |
1016 // of the tree, copy our sibling's values. | |
1017 if (useSibling) { | |
1018 rowSettings.relationship = 'sibling'; | |
1019 rowSettings.source = rowSettings.target; | |
1020 } | |
1021 | |
1022 const targetClass = `.${rowSettings.target}`; | |
1023 const targetElement = $changedRow.find(targetClass).get(0); | |
1024 | |
1025 // Check if a target element exists in this row. | |
1026 if (targetElement) { | |
1027 const sourceClass = `.${rowSettings.source}`; | |
1028 const sourceElement = $(sourceClass, sourceRow).get(0); | |
1029 switch (rowSettings.action) { | |
1030 case 'depth': | |
1031 // Get the depth of the target row. | |
1032 targetElement.value = $(sourceElement).closest('tr').find('.js-indentation').length; | |
1033 break; | |
1034 | |
1035 case 'match': | |
1036 // Update the value. | |
1037 targetElement.value = sourceElement.value; | |
1038 break; | |
1039 | |
1040 case 'order': | |
1041 var siblings = this.rowObject.findSiblings(rowSettings); | |
1042 if ($(targetElement).is('select')) { | |
1043 // Get a list of acceptable values. | |
1044 const values = []; | |
1045 $(targetElement).find('option').each(function () { | |
1046 values.push(this.value); | |
1047 }); | |
1048 const maxVal = values[values.length - 1]; | |
1049 // Populate the values in the siblings. | |
1050 $(siblings).find(targetClass).each(function () { | |
1051 // If there are more items than possible values, assign the | |
1052 // maximum value to the row. | |
1053 if (values.length > 0) { | |
1054 this.value = values.shift(); | |
1055 } | |
1056 else { | |
1057 this.value = maxVal; | |
1058 } | |
1059 }); | |
1060 } | |
1061 else { | |
1062 // Assume a numeric input field. | |
1063 let weight = parseInt($(siblings[0]).find(targetClass).val(), 10) || 0; | |
1064 $(siblings).find(targetClass).each(function () { | |
1065 this.value = weight; | |
1066 weight++; | |
1067 }); | |
1068 } | |
1069 break; | |
1070 } | |
1071 } | |
1072 }; | |
1073 | |
1074 /** | |
1075 * Copy all tableDrag related classes from one row to another. | |
1076 * | |
1077 * Copy all special tableDrag classes from one row's form elements to a | |
1078 * different one, removing any special classes that the destination row | |
1079 * may have had. | |
1080 * | |
1081 * @param {HTMLElement} sourceRow | |
1082 * The element for the source row. | |
1083 * @param {HTMLElement} targetRow | |
1084 * The element for the target row. | |
1085 * @param {string} group | |
1086 * The group selector. | |
1087 */ | |
1088 Drupal.tableDrag.prototype.copyDragClasses = function (sourceRow, targetRow, group) { | |
1089 const sourceElement = $(sourceRow).find(`.${group}`); | |
1090 const targetElement = $(targetRow).find(`.${group}`); | |
1091 if (sourceElement.length && targetElement.length) { | |
1092 targetElement[0].className = sourceElement[0].className; | |
1093 } | |
1094 }; | |
1095 | |
1096 /** | |
1097 * Check the suggested scroll of the table. | |
1098 * | |
1099 * @param {number} cursorY | |
1100 * The Y position of the cursor. | |
1101 * | |
1102 * @return {number} | |
1103 * The suggested scroll. | |
1104 */ | |
1105 Drupal.tableDrag.prototype.checkScroll = function (cursorY) { | |
1106 const de = document.documentElement; | |
1107 const b = document.body; | |
1108 | |
1109 const windowHeight = this.windowHeight = window.innerHeight || (de.clientHeight && de.clientWidth !== 0 ? de.clientHeight : b.offsetHeight); | |
1110 let scrollY; | |
1111 if (document.all) { | |
1112 scrollY = this.scrollY = !de.scrollTop ? b.scrollTop : de.scrollTop; | |
1113 } | |
1114 else { | |
1115 scrollY = this.scrollY = window.pageYOffset ? window.pageYOffset : window.scrollY; | |
1116 } | |
1117 const trigger = this.scrollSettings.trigger; | |
1118 let delta = 0; | |
1119 | |
1120 // Return a scroll speed relative to the edge of the screen. | |
1121 if (cursorY - scrollY > windowHeight - trigger) { | |
1122 delta = trigger / (windowHeight + scrollY - cursorY); | |
1123 delta = (delta > 0 && delta < trigger) ? delta : trigger; | |
1124 return delta * this.scrollSettings.amount; | |
1125 } | |
1126 else if (cursorY - scrollY < trigger) { | |
1127 delta = trigger / (cursorY - scrollY); | |
1128 delta = (delta > 0 && delta < trigger) ? delta : trigger; | |
1129 return -delta * this.scrollSettings.amount; | |
1130 } | |
1131 }; | |
1132 | |
1133 /** | |
1134 * Set the scroll for the table. | |
1135 * | |
1136 * @param {number} scrollAmount | |
1137 * The amount of scroll to apply to the window. | |
1138 */ | |
1139 Drupal.tableDrag.prototype.setScroll = function (scrollAmount) { | |
1140 const self = this; | |
1141 | |
1142 this.scrollInterval = setInterval(() => { | |
1143 // Update the scroll values stored in the object. | |
1144 self.checkScroll(self.currentPointerCoords.y); | |
1145 const aboveTable = self.scrollY > self.table.topY; | |
1146 const belowTable = self.scrollY + self.windowHeight < self.table.bottomY; | |
1147 if (scrollAmount > 0 && belowTable || scrollAmount < 0 && aboveTable) { | |
1148 window.scrollBy(0, scrollAmount); | |
1149 } | |
1150 }, this.scrollSettings.interval); | |
1151 }; | |
1152 | |
1153 /** | |
1154 * Command to restripe table properly. | |
1155 */ | |
1156 Drupal.tableDrag.prototype.restripeTable = function () { | |
1157 // :even and :odd are reversed because jQuery counts from 0 and | |
1158 // we count from 1, so we're out of sync. | |
1159 // Match immediate children of the parent element to allow nesting. | |
1160 $(this.table).find('> tbody > tr.draggable, > tr.draggable') | |
1161 .filter(':visible') | |
1162 .filter(':odd').removeClass('odd').addClass('even').end() | |
1163 .filter(':even').removeClass('even').addClass('odd'); | |
1164 }; | |
1165 | |
1166 /** | |
1167 * Stub function. Allows a custom handler when a row begins dragging. | |
1168 * | |
1169 * @return {null} | |
1170 * Returns null when the stub function is used. | |
1171 */ | |
1172 Drupal.tableDrag.prototype.onDrag = function () { | |
1173 return null; | |
1174 }; | |
1175 | |
1176 /** | |
1177 * Stub function. Allows a custom handler when a row is dropped. | |
1178 * | |
1179 * @return {null} | |
1180 * Returns null when the stub function is used. | |
1181 */ | |
1182 Drupal.tableDrag.prototype.onDrop = function () { | |
1183 return null; | |
1184 }; | |
1185 | |
1186 /** | |
1187 * Constructor to make a new object to manipulate a table row. | |
1188 * | |
1189 * @param {HTMLElement} tableRow | |
1190 * The DOM element for the table row we will be manipulating. | |
1191 * @param {string} method | |
1192 * The method in which this row is being moved. Either 'keyboard' or | |
1193 * 'mouse'. | |
1194 * @param {bool} indentEnabled | |
1195 * Whether the containing table uses indentations. Used for optimizations. | |
1196 * @param {number} maxDepth | |
1197 * The maximum amount of indentations this row may contain. | |
1198 * @param {bool} addClasses | |
1199 * Whether we want to add classes to this row to indicate child | |
1200 * relationships. | |
1201 */ | |
1202 Drupal.tableDrag.prototype.row = function (tableRow, method, indentEnabled, maxDepth, addClasses) { | |
1203 const $tableRow = $(tableRow); | |
1204 | |
1205 this.element = tableRow; | |
1206 this.method = method; | |
1207 this.group = [tableRow]; | |
1208 this.groupDepth = $tableRow.find('.js-indentation').length; | |
1209 this.changed = false; | |
1210 this.table = $tableRow.closest('table')[0]; | |
1211 this.indentEnabled = indentEnabled; | |
1212 this.maxDepth = maxDepth; | |
1213 // Direction the row is being moved. | |
1214 this.direction = ''; | |
1215 if (this.indentEnabled) { | |
1216 this.indents = $tableRow.find('.js-indentation').length; | |
1217 this.children = this.findChildren(addClasses); | |
1218 this.group = $.merge(this.group, this.children); | |
1219 // Find the depth of this entire group. | |
1220 for (let n = 0; n < this.group.length; n++) { | |
1221 this.groupDepth = Math.max($(this.group[n]).find('.js-indentation').length, this.groupDepth); | |
1222 } | |
1223 } | |
1224 }; | |
1225 | |
1226 /** | |
1227 * Find all children of rowObject by indentation. | |
1228 * | |
1229 * @param {bool} addClasses | |
1230 * Whether we want to add classes to this row to indicate child | |
1231 * relationships. | |
1232 * | |
1233 * @return {Array} | |
1234 * An array of children of the row. | |
1235 */ | |
1236 Drupal.tableDrag.prototype.row.prototype.findChildren = function (addClasses) { | |
1237 const parentIndentation = this.indents; | |
1238 let currentRow = $(this.element, this.table).next('tr.draggable'); | |
1239 const rows = []; | |
1240 let child = 0; | |
1241 | |
1242 function rowIndentation(indentNum, el) { | |
1243 const self = $(el); | |
1244 if (child === 1 && (indentNum === parentIndentation)) { | |
1245 self.addClass('tree-child-first'); | |
1246 } | |
1247 if (indentNum === parentIndentation) { | |
1248 self.addClass('tree-child'); | |
1249 } | |
1250 else if (indentNum > parentIndentation) { | |
1251 self.addClass('tree-child-horizontal'); | |
1252 } | |
1253 } | |
1254 | |
1255 while (currentRow.length) { | |
1256 // A greater indentation indicates this is a child. | |
1257 if (currentRow.find('.js-indentation').length > parentIndentation) { | |
1258 child++; | |
1259 rows.push(currentRow[0]); | |
1260 if (addClasses) { | |
1261 currentRow.find('.js-indentation').each(rowIndentation); | |
1262 } | |
1263 } | |
1264 else { | |
1265 break; | |
1266 } | |
1267 currentRow = currentRow.next('tr.draggable'); | |
1268 } | |
1269 if (addClasses && rows.length) { | |
1270 $(rows[rows.length - 1]).find(`.js-indentation:nth-child(${parentIndentation + 1})`).addClass('tree-child-last'); | |
1271 } | |
1272 return rows; | |
1273 }; | |
1274 | |
1275 /** | |
1276 * Ensure that two rows are allowed to be swapped. | |
1277 * | |
1278 * @param {HTMLElement} row | |
1279 * DOM object for the row being considered for swapping. | |
1280 * | |
1281 * @return {bool} | |
1282 * Whether the swap is a valid swap or not. | |
1283 */ | |
1284 Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) { | |
1285 const $row = $(row); | |
1286 if (this.indentEnabled) { | |
1287 let prevRow; | |
1288 let nextRow; | |
1289 if (this.direction === 'down') { | |
1290 prevRow = row; | |
1291 nextRow = $row.next('tr').get(0); | |
1292 } | |
1293 else { | |
1294 prevRow = $row.prev('tr').get(0); | |
1295 nextRow = row; | |
1296 } | |
1297 this.interval = this.validIndentInterval(prevRow, nextRow); | |
1298 | |
1299 // We have an invalid swap if the valid indentations interval is empty. | |
1300 if (this.interval.min > this.interval.max) { | |
1301 return false; | |
1302 } | |
1303 } | |
1304 | |
1305 // Do not let an un-draggable first row have anything put before it. | |
1306 if (this.table.tBodies[0].rows[0] === row && $row.is(':not(.draggable)')) { | |
1307 return false; | |
1308 } | |
1309 | |
1310 return true; | |
1311 }; | |
1312 | |
1313 /** | |
1314 * Perform the swap between two rows. | |
1315 * | |
1316 * @param {string} position | |
1317 * Whether the swap will occur 'before' or 'after' the given row. | |
1318 * @param {HTMLElement} row | |
1319 * DOM element what will be swapped with the row group. | |
1320 */ | |
1321 Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) { | |
1322 // Makes sure only DOM object are passed to Drupal.detachBehaviors(). | |
1323 this.group.forEach((row) => { | |
1324 Drupal.detachBehaviors(row, drupalSettings, 'move'); | |
1325 }); | |
1326 $(row)[position](this.group); | |
1327 // Makes sure only DOM object are passed to Drupal.attachBehaviors()s. | |
1328 this.group.forEach((row) => { | |
1329 Drupal.attachBehaviors(row, drupalSettings); | |
1330 }); | |
1331 this.changed = true; | |
1332 this.onSwap(row); | |
1333 }; | |
1334 | |
1335 /** | |
1336 * Determine the valid indentations interval for the row at a given position. | |
1337 * | |
1338 * @param {?HTMLElement} prevRow | |
1339 * DOM object for the row before the tested position | |
1340 * (or null for first position in the table). | |
1341 * @param {?HTMLElement} nextRow | |
1342 * DOM object for the row after the tested position | |
1343 * (or null for last position in the table). | |
1344 * | |
1345 * @return {object} | |
1346 * An object with the keys `min` and `max` to indicate the valid indent | |
1347 * interval. | |
1348 */ | |
1349 Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) { | |
1350 const $prevRow = $(prevRow); | |
1351 let minIndent; | |
1352 let maxIndent; | |
1353 | |
1354 // Minimum indentation: | |
1355 // Do not orphan the next row. | |
1356 minIndent = nextRow ? $(nextRow).find('.js-indentation').length : 0; | |
1357 | |
1358 // Maximum indentation: | |
1359 if (!prevRow || $prevRow.is(':not(.draggable)') || $(this.element).is('.tabledrag-root')) { | |
1360 // Do not indent: | |
1361 // - the first row in the table, | |
1362 // - rows dragged below a non-draggable row, | |
1363 // - 'root' rows. | |
1364 maxIndent = 0; | |
1365 } | |
1366 else { | |
1367 // Do not go deeper than as a child of the previous row. | |
1368 maxIndent = $prevRow.find('.js-indentation').length + ($prevRow.is('.tabledrag-leaf') ? 0 : 1); | |
1369 // Limit by the maximum allowed depth for the table. | |
1370 if (this.maxDepth) { | |
1371 maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents)); | |
1372 } | |
1373 } | |
1374 | |
1375 return { min: minIndent, max: maxIndent }; | |
1376 }; | |
1377 | |
1378 /** | |
1379 * Indent a row within the legal bounds of the table. | |
1380 * | |
1381 * @param {number} indentDiff | |
1382 * The number of additional indentations proposed for the row (can be | |
1383 * positive or negative). This number will be adjusted to nearest valid | |
1384 * indentation level for the row. | |
1385 * | |
1386 * @return {number} | |
1387 * The number of indentations applied. | |
1388 */ | |
1389 Drupal.tableDrag.prototype.row.prototype.indent = function (indentDiff) { | |
1390 const $group = $(this.group); | |
1391 // Determine the valid indentations interval if not available yet. | |
1392 if (!this.interval) { | |
1393 const prevRow = $(this.element).prev('tr').get(0); | |
1394 const nextRow = $group.eq(-1).next('tr').get(0); | |
1395 this.interval = this.validIndentInterval(prevRow, nextRow); | |
1396 } | |
1397 | |
1398 // Adjust to the nearest valid indentation. | |
1399 let indent = this.indents + indentDiff; | |
1400 indent = Math.max(indent, this.interval.min); | |
1401 indent = Math.min(indent, this.interval.max); | |
1402 indentDiff = indent - this.indents; | |
1403 | |
1404 for (let n = 1; n <= Math.abs(indentDiff); n++) { | |
1405 // Add or remove indentations. | |
1406 if (indentDiff < 0) { | |
1407 $group.find('.js-indentation:first-of-type').remove(); | |
1408 this.indents--; | |
1409 } | |
1410 else { | |
1411 $group.find('td:first-of-type').prepend(Drupal.theme('tableDragIndentation')); | |
1412 this.indents++; | |
1413 } | |
1414 } | |
1415 if (indentDiff) { | |
1416 // Update indentation for this row. | |
1417 this.changed = true; | |
1418 this.groupDepth += indentDiff; | |
1419 this.onIndent(); | |
1420 } | |
1421 | |
1422 return indentDiff; | |
1423 }; | |
1424 | |
1425 /** | |
1426 * Find all siblings for a row. | |
1427 * | |
1428 * According to its subgroup or indentation. Note that the passed-in row is | |
1429 * included in the list of siblings. | |
1430 * | |
1431 * @param {object} rowSettings | |
1432 * The field settings we're using to identify what constitutes a sibling. | |
1433 * | |
1434 * @return {Array} | |
1435 * An array of siblings. | |
1436 */ | |
1437 Drupal.tableDrag.prototype.row.prototype.findSiblings = function (rowSettings) { | |
1438 const siblings = []; | |
1439 const directions = ['prev', 'next']; | |
1440 const rowIndentation = this.indents; | |
1441 let checkRowIndentation; | |
1442 for (let d = 0; d < directions.length; d++) { | |
1443 let checkRow = $(this.element)[directions[d]](); | |
1444 while (checkRow.length) { | |
1445 // Check that the sibling contains a similar target field. | |
1446 if (checkRow.find(`.${rowSettings.target}`)) { | |
1447 // Either add immediately if this is a flat table, or check to ensure | |
1448 // that this row has the same level of indentation. | |
1449 if (this.indentEnabled) { | |
1450 checkRowIndentation = checkRow.find('.js-indentation').length; | |
1451 } | |
1452 | |
1453 if (!(this.indentEnabled) || (checkRowIndentation === rowIndentation)) { | |
1454 siblings.push(checkRow[0]); | |
1455 } | |
1456 else if (checkRowIndentation < rowIndentation) { | |
1457 // No need to keep looking for siblings when we get to a parent. | |
1458 break; | |
1459 } | |
1460 } | |
1461 else { | |
1462 break; | |
1463 } | |
1464 checkRow = checkRow[directions[d]](); | |
1465 } | |
1466 // Since siblings are added in reverse order for previous, reverse the | |
1467 // completed list of previous siblings. Add the current row and continue. | |
1468 if (directions[d] === 'prev') { | |
1469 siblings.reverse(); | |
1470 siblings.push(this.element); | |
1471 } | |
1472 } | |
1473 return siblings; | |
1474 }; | |
1475 | |
1476 /** | |
1477 * Remove indentation helper classes from the current row group. | |
1478 */ | |
1479 Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () { | |
1480 for (const n in this.children) { | |
1481 if (this.children.hasOwnProperty(n)) { | |
1482 $(this.children[n]).find('.js-indentation') | |
1483 .removeClass('tree-child') | |
1484 .removeClass('tree-child-first') | |
1485 .removeClass('tree-child-last') | |
1486 .removeClass('tree-child-horizontal'); | |
1487 } | |
1488 } | |
1489 }; | |
1490 | |
1491 /** | |
1492 * Add an asterisk or other marker to the changed row. | |
1493 */ | |
1494 Drupal.tableDrag.prototype.row.prototype.markChanged = function () { | |
1495 const marker = Drupal.theme('tableDragChangedMarker'); | |
1496 const cell = $(this.element).find('td:first-of-type'); | |
1497 if (cell.find('abbr.tabledrag-changed').length === 0) { | |
1498 cell.append(marker); | |
1499 } | |
1500 }; | |
1501 | |
1502 /** | |
1503 * Stub function. Allows a custom handler when a row is indented. | |
1504 * | |
1505 * @return {null} | |
1506 * Returns null when the stub function is used. | |
1507 */ | |
1508 Drupal.tableDrag.prototype.row.prototype.onIndent = function () { | |
1509 return null; | |
1510 }; | |
1511 | |
1512 /** | |
1513 * Stub function. Allows a custom handler when a row is swapped. | |
1514 * | |
1515 * @param {HTMLElement} swappedRow | |
1516 * The element for the swapped row. | |
1517 * | |
1518 * @return {null} | |
1519 * Returns null when the stub function is used. | |
1520 */ | |
1521 Drupal.tableDrag.prototype.row.prototype.onSwap = function (swappedRow) { | |
1522 return null; | |
1523 }; | |
1524 | |
1525 $.extend(Drupal.theme, /** @lends Drupal.theme */{ | |
1526 | |
1527 /** | |
1528 * @return {string} | |
1529 * Markup for the marker. | |
1530 */ | |
1531 tableDragChangedMarker() { | |
1532 return `<abbr class="warning tabledrag-changed" title="${Drupal.t('Changed')}">*</abbr>`; | |
1533 }, | |
1534 | |
1535 /** | |
1536 * @return {string} | |
1537 * Markup for the indentation. | |
1538 */ | |
1539 tableDragIndentation() { | |
1540 return '<div class="js-indentation indentation"> </div>'; | |
1541 }, | |
1542 | |
1543 /** | |
1544 * @return {string} | |
1545 * Markup for the warning. | |
1546 */ | |
1547 tableDragChangedWarning() { | |
1548 return `<div class="tabledrag-changed-warning messages messages--warning" role="alert">${Drupal.theme('tableDragChangedMarker')} ${Drupal.t('You have unsaved changes.')}</div>`; | |
1549 }, | |
1550 }); | |
1551 }(jQuery, Drupal, drupalSettings)); |