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