Security update to Drupal 8.4.6
[yaffs-website] / web / core / misc / tabledrag.es6.js
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">&nbsp;</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">&nbsp;</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));