Source: themes/claro/js/tabledrag.es6.js

/**
 * @file
 * Overrides tabledrag.js that provides dragging capabilities.
 *
 * - New Drupal.theme.tableDragHandle() function for tabledrag handle markup
 *   (https://www.drupal.org/node/3077938).
 * - New Drupal.theme.tableDragToggle() function for tabledrag toggle markup
 *   (@todo: https://www.drupal.org/node/3084916).
 * - New Drupal.theme.tableDragToggleWrapper() function for the wrapper of the
 *   tabledrag toggle (@todo: https://www.drupal.org/node/3084916).
 * - Tabledrag functionality can be disabled
 *   (https://www.drupal.org/node/3083039).
 * - The initial content of the tabledrag-cell is wrapped into a new DOM element
 *   ".tabledrag-cell-content__item". This new element is moved into an another
 *   ".tabledrag-cell-content" division that contains the drag handle, the
 *   indentation elements and the tabledrag changed mark as well.
 *   This is needed to keep all of these element in a single line
 *   (https://www.drupal.org/node/3083044).
 *   Claro introduced two theme functions for these:
 *   - Drupal.theme.tableDragCellContentWrapper() provides the output of the
 *     original content of the first table cell.
 *   - Drupal.theme.tableDragCellItemsWrapper() provides the markup of the
 *     common wrapper for every tabledrag cell elements including the
 *     indentation(s), the drag-handle, the original content and the tabledrag
 *     changed marker.
 * - Fixes the RTL bug of the original tabledrag.js
 *   (https://www.drupal.org/node/197641).
 * - Tabledrag changed mark is added next to the drag-handle, and not after the
 *   last item. (@todo: https://www.drupal.org/node/3084910).
 *
 * The '_slicedToArray' shim added for handling destructured arrays breaks IE11,
 * that is why the 'prefer-destructuring' rule is disabled.
 * @see https://github.com/babel/babel/issues/7597.
 *
 * @todo Refactor after https://www.drupal.org/node/3077938,
 *   https://www.drupal.org/node/3083039, https://www.drupal.org/node/3083044
 *   and https://www.drupal.org/node/197641 are in.
 */

/**
 * Triggers when weights columns are toggled.
 *
 * @event columnschange
 */

/* eslint-disable default-case, new-cap, prefer-destructuring */
(($, Drupal, drupalSettings) => {
  /**
   * Store the state of weight columns display for all tables.
   *
   * Default value is to hide weight columns.
   */
  let showWeight = JSON.parse(
    localStorage.getItem('Drupal.tableDrag.showWeight'),
  );

  /**
   * Drag and drop table rows with field manipulation.
   *
   * Using the drupal_attach_tabledrag() function, any table with weights or
   * parent relationships may be made into draggable tables. Columns containing
   * a field may optionally be hidden, providing a better user experience.
   *
   * Created tableDrag instances may be modified with custom behaviors by
   * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods.
   * See blocks.js for an example of adding additional functionality to
   * tableDrag.
   *
   * @type {Drupal~behavior}
   */
  Drupal.behaviors.tableDrag = {
    attach(context, settings) {
      function initTableDrag(table, base) {
        if (table.length) {
          // Create the new tableDrag instance. Save in the Drupal variable
          // to allow other scripts access to the object.
          Drupal.tableDrag[base] = new Drupal.tableDrag(
            table[0],
            settings.tableDrag[base],
          );
        }
      }

      Object.keys(settings.tableDrag || {}).forEach((base) => {
        initTableDrag($(context).find(`#${base}`).once('tabledrag'), base);
      });
    },
  };

  /**
   * Provides table and field manipulation.
   *
   * @constructor
   *
   * @param {HTMLElement} table
   *   DOM object for the table to be made draggable.
   * @param {object} tableSettings
   *   Settings for the table added via drupal_add_dragtable().
   */
  Drupal.tableDrag = function init(table, tableSettings) {
    const self = this;
    const $table = $(table);

    /**
     * @type {jQuery}
     */
    this.$table = $(table);

    /**
     *
     * @type {HTMLElement}
     */
    this.table = table;

    /**
     * @type {object}
     */
    this.tableSettings = tableSettings;

    /**
     * Used to hold information about a current drag operation.
     *
     * @type {?HTMLElement}
     */
    this.dragObject = null;

    /**
     * Provides operations for row manipulation.
     *
     * @type {?HTMLElement}
     */
    this.rowObject = null;

    /**
     * Remember the previous element.
     *
     * @type {?HTMLElement}
     */
    this.oldRowElement = null;

    /**
     * Used to determine up or down direction from last mouse move.
     *
     * @type {?number}
     */
    this.oldY = null;

    /**
     * Whether anything in the entire table has changed.
     *
     * @type {bool}
     */
    this.changed = false;

    /**
     * Maximum amount of allowed parenting.
     *
     * @type {number}
     */
    this.maxDepth = 0;

    /**
     * Direction of the table.
     *
     * @type {number}
     */
    this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;

    /**
     *
     * @type {bool}
     */
    this.striping = $(this.table).data('striping') === 1;

    /**
     * Configure the scroll settings.
     *
     * @type {object}
     *
     * @prop {number} amount
     * @prop {number} interval
     * @prop {number} trigger
     */
    this.scrollSettings = { amount: 4, interval: 50, trigger: 70 };

    /**
     *
     * @type {?number}
     */
    this.scrollInterval = null;

    /**
     *
     * @type {number}
     */
    this.scrollY = 0;

    /**
     *
     * @type {number}
     */
    this.windowHeight = 0;

    /**
     * Check this table's settings for parent relationships.
     *
     * For efficiency, large sections of code can be skipped if we don't need to
     * track horizontal movement and indentations.
     *
     * @type {bool}
     */
    this.indentEnabled = false;
    Object.keys(tableSettings || {}).forEach((group) => {
      Object.keys(tableSettings[group] || {}).forEach((n) => {
        if (tableSettings[group][n].relationship === 'parent') {
          this.indentEnabled = true;
        }
        if (tableSettings[group][n].limit > 0) {
          this.maxDepth = tableSettings[group][n].limit;
        }
      });
    });
    if (this.indentEnabled) {
      /**
       * Total width of indents, set in makeDraggable.
       *
       * @type {number}
       */
      this.indentCount = 1;
      // Find the width of indentations to measure mouse movements against.
      // Because the table doesn't need to start with any indentations, we
      // manually append 2 indentations in the first draggable row, measure
      // the offset, then remove.
      const indent = Drupal.theme('tableDragIndentation');
      const testRow = $('<tr></tr>').addClass('draggable').appendTo(table);
      const testCell = $('<td></td>')
        .appendTo(testRow)
        .prepend(indent)
        .prepend(indent);
      const $indentation = testCell.find('.js-indentation');

      /**
       * @type {number}
       */
      this.indentAmount =
        $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
      testRow.remove();
    }

    // Make each applicable row draggable.
    // Match immediate children of the parent element to allow nesting.
    $table
      .find('> tr.draggable, > tbody > tr.draggable')
      .each(function initDraggable() {
        self.makeDraggable(this);
      });

    // Add the toggle link wrapper before the table that will contain the toggle
    // for users to show or hide weight columns.
    $table.before(
      $(Drupal.theme('tableDragToggleWrapper'))
        .addClass('js-tabledrag-toggle-weight-wrapper')
        .on(
          'click',
          '.js-tabledrag-toggle-weight',
          $.proxy(function toggleColumns(event) {
            event.preventDefault();
            this.toggleColumns();
          }, this),
        ),
    );

    // Initialize the specified columns (for example, weight or parent columns)
    // to show or hide according to user preference. This aids accessibility
    // so that, e.g., screen reader users can choose to enter weight values and
    // manipulate form elements directly, rather than using drag-and-drop..
    self.initColumns();

    // Add event bindings to the document. The self variable is passed along
    // as event handlers do not have direct access to the tableDrag object.
    $(document).on('touchmove', (event) =>
      self.dragRow(event.originalEvent.touches[0], self),
    );
    $(document).on('touchend', (event) =>
      self.dropRow(event.originalEvent.touches[0], self),
    );
    $(document).on('mousemove pointermove', (event) =>
      self.dragRow(event, self),
    );
    $(document).on('mouseup pointerup', (event) => self.dropRow(event, self));

    // React to localStorage event showing or hiding weight columns.
    $(window).on(
      'storage',
      $.proxy(function weightColumnDisplayChange(event) {
        // Only react to 'Drupal.tableDrag.showWeight' value change.
        if (event.originalEvent.key === 'Drupal.tableDrag.showWeight') {
          // This was changed in another window, get the new value for this
          // window.
          showWeight = JSON.parse(event.originalEvent.newValue);
          this.displayColumns(showWeight);
        }
      }, this),
    );
  };

  $.extend(
    Drupal.tableDrag.prototype,
    /** @lends Drupal.tableDrag# */ {
      /**
       * Initialize columns containing form elements to be hidden by default.
       *
       * Identify and mark each cell with a CSS class so we can easily toggle
       * show/hide it. Finally, hide columns if user does not have a
       * 'Drupal.tableDrag.showWeight' localStorage value.
       */
      initColumns() {
        const { $table } = this;
        let hidden;
        let cell;
        let columnIndex;
        Object.keys(this.tableSettings || {}).forEach((group) => {
          // Find the first field in this group.
          Object.keys(this.tableSettings[group]).some((tableSetting) => {
            const field = $table
              .find(`.${this.tableSettings[group][tableSetting].target}`)
              .eq(0);
            if (
              field.length &&
              this.tableSettings[group][tableSetting].hidden
            ) {
              hidden = this.tableSettings[group][tableSetting].hidden;
              cell = field.closest('td');
              return true;
            }
            return false;
          });

          // Mark the column containing this field so it can be hidden.
          if (hidden && cell[0]) {
            // Add 1 to our indexes. The nth-child selector is 1 based, not 0
            // based. Match immediate children of the parent element to allow
            // nesting.
            columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1;
            $table
              .find('> thead > tr, > tbody > tr, > tr')
              .each(this.addColspanClass(columnIndex));
          }
        });
        this.displayColumns(showWeight);
      },

      /**
       * Mark cells that have colspan.
       *
       * In order to adjust the colspan instead of hiding them altogether.
       *
       * @param {number} columnIndex
       *   The column index to add colspan class to.
       *
       * @return {function}
       *   Function to add colspan class.
       */
      addColspanClass(columnIndex) {
        return function addColspanClass() {
          // Get the columnIndex and adjust for any colspans in this row.
          const $row = $(this);
          let index = columnIndex;
          const cells = $row.children();
          let cell;
          cells.each(function checkColspan(n) {
            if (n < index && this.colSpan && this.colSpan > 1) {
              index -= this.colSpan - 1;
            }
          });
          if (index > 0) {
            cell = cells.filter(`:nth-child(${index})`);
            if (cell[0].colSpan && cell[0].colSpan > 1) {
              // If this cell has a colspan, mark it so we can reduce the colspan.
              cell.addClass('tabledrag-has-colspan');
            } else {
              // Mark this cell so we can hide it.
              cell.addClass('tabledrag-hide');
            }
          }
        };
      },

      /**
       * Hide or display weight columns. Triggers an event on change.
       *
       * @fires event:columnschange
       *
       * @param {bool} displayWeight
       *   'true' will show weight columns.
       */
      displayColumns(displayWeight) {
        if (displayWeight) {
          this.showColumns();
        }
        // Default action is to hide columns.
        else {
          this.hideColumns();
        }
        // Trigger an event to allow other scripts to react to this display change.
        // Force the extra parameter as a bool.
        $('table')
          .findOnce('tabledrag')
          .trigger('columnschange', !!displayWeight);
      },

      /**
       * Toggle the weight column depending on 'showWeight' value.
       *
       * Store only default override.
       */
      toggleColumns() {
        showWeight = !showWeight;
        this.displayColumns(showWeight);
        if (showWeight) {
          // Save default override.
          localStorage.setItem('Drupal.tableDrag.showWeight', showWeight);
        } else {
          // Reset the value to its default.
          localStorage.removeItem('Drupal.tableDrag.showWeight');
        }
      },

      /**
       * Hide the columns containing weight/parent form elements.
       *
       * Undo showColumns().
       */
      hideColumns() {
        const $tables = $('table').findOnce('tabledrag');
        // Hide weight/parent cells and headers.
        $tables.find('.tabledrag-hide').css('display', 'none');
        // Show TableDrag handles.
        $tables.find('.js-tabledrag-handle').css('display', '');
        // Reduce the colspan of any effected multi-span columns.
        $tables.find('.tabledrag-has-colspan').each(function decreaseColspan() {
          this.colSpan -= -1;
        });
        // Change link text.
        $('.js-tabledrag-toggle-weight-wrapper').each(
          function addShowWeightToggle() {
            const $wrapper = $(this);
            const toggleWasFocused = $wrapper.find(
              '.js-tabledrag-toggle-weight:focus',
            ).length;
            $wrapper
              .empty()
              .append(
                $(
                  Drupal.theme(
                    'tableDragToggle',
                    'show',
                    Drupal.t('Show row weights'),
                  ),
                ).addClass('js-tabledrag-toggle-weight'),
              );
            if (toggleWasFocused) {
              $wrapper.find('.js-tabledrag-toggle-weight').trigger('focus');
            }
          },
        );
      },

      /**
       * Show the columns containing weight/parent form elements.
       *
       * Undo hideColumns().
       */
      showColumns() {
        const $tables = $('table').findOnce('tabledrag');
        // Show weight/parent cells and headers.
        $tables.find('.tabledrag-hide').css('display', '');
        // Hide TableDrag handles.
        $tables.find('.js-tabledrag-handle').css('display', 'none');
        // Increase the colspan for any columns where it was previously reduced.
        $tables.find('.tabledrag-has-colspan').each(function increaseColspan() {
          this.colSpan += 1;
        });
        // Change link text.
        $('.js-tabledrag-toggle-weight-wrapper').each(
          function addHideWeightToggle() {
            const $wrapper = $(this);
            const toggleWasFocused = $wrapper.find(
              '.js-tabledrag-toggle-weight:focus',
            ).length;
            $wrapper
              .empty()
              .append(
                $(
                  Drupal.theme(
                    'tableDragToggle',
                    'hide',
                    Drupal.t('Hide row weights'),
                  ),
                ).addClass('js-tabledrag-toggle-weight'),
              );
            if (toggleWasFocused) {
              $wrapper.find('.js-tabledrag-toggle-weight').trigger('focus');
            }
          },
        );
      },

      /**
       * Find the target used within a particular row and group.
       *
       * @param {string} group
       *   Group selector.
       * @param {HTMLElement} row
       *   The row HTML element.
       *
       * @return {object}
       *   The table row settings.
       */
      rowSettings(group, row) {
        const field = $(row).find(`.${group}`);
        const tableSettingsGroup = this.tableSettings[group];
        return Object.keys(tableSettingsGroup)
          .map((delta) => {
            const targetClass = tableSettingsGroup[delta].target;
            let rowSettings;
            if (field.is(`.${targetClass}`)) {
              // Return a copy of the row settings.
              rowSettings = {};
              Object.keys(tableSettingsGroup[delta]).forEach((n) => {
                rowSettings[n] = tableSettingsGroup[delta][n];
              });
            }
            return rowSettings;
          })
          .filter((rowSetting) => rowSetting)[0];
      },

      /**
       * Take an item and add event handlers to make it become draggable.
       *
       * @param {HTMLElement} item
       *   The item to add event handlers to.
       */
      makeDraggable(item) {
        const self = this;
        const $item = $(item);
        const $firstCell = $item
          .find('td:first-of-type')
          .wrapInner(Drupal.theme.tableDragCellContentWrapper())
          .wrapInner(
            $(Drupal.theme('tableDragCellItemsWrapper')).addClass(
              'js-tabledrag-cell-content',
            ),
          );
        const $targetElem = $firstCell.find('.js-tabledrag-cell-content').length
          ? $firstCell.find('.js-tabledrag-cell-content')
          : $firstCell.addClass('js-tabledrag-cell-content');

        // Move indentations into the '.js-tabledrag-cell-content' target.
        $targetElem.find('.js-indentation').detach().prependTo($targetElem);

        // Add a class to the title link.
        $targetElem.find('a').addClass('menu-item__link');
        // Create the handle.
        const handle = $(Drupal.theme.tableDragHandle())
          .addClass('js-tabledrag-handle')
          .attr('title', Drupal.t('Drag to re-order'));
        // Insert the handle after indentations (if any).
        const $indentationLast = $targetElem.find('.js-indentation').eq(-1);
        if ($indentationLast.length) {
          $indentationLast.after(handle);
          // Update the total width of indentation in this entire table.
          self.indentCount = Math.max(
            $item.find('.js-indentation').length,
            self.indentCount,
          );
        } else {
          $targetElem.prepend(handle);
        }

        // Prevent the anchor tag from jumping us to the top of the page.
        handle.on('click', (event) => {
          event.preventDefault();
        });

        // Don't do anything if tabledrag is disabled.
        if (handle.closest('.js-tabledrag-disabled').length) {
          return;
        }

        handle.on('mousedown touchstart pointerdown', (event) => {
          event.preventDefault();
          if (event.originalEvent.type === 'touchstart') {
            event = event.originalEvent.touches[0];
          }
          self.dragStart(event, self, item);
        });

        // Set blur cleanup when a handle is focused.
        handle.on('focus', () => {
          self.safeBlur = true;
        });

        // On blur, fire the same function as a touchend/mouseup. This is used to
        // update values after a row has been moved through the keyboard support.
        handle.on('blur', (event) => {
          if (self.rowObject && self.safeBlur) {
            self.dropRow(event, self);
          }
        });

        // Add arrow-key support to the handle.
        handle.on('keydown', (event) => {
          // If a rowObject doesn't yet exist and this isn't the tab key.
          if (event.keyCode !== 9 && !self.rowObject) {
            self.rowObject = new self.row(
              item,
              'keyboard',
              self.indentEnabled,
              self.maxDepth,
              true,
            );
          }

          let keyChange = false;
          let groupHeight;

          /* eslint-disable no-fallthrough */

          switch (event.keyCode) {
            // Left arrow.
            case 37:
            // Safari left arrow.
            case 63234:
              keyChange = true;
              self.rowObject.indent(-1 * self.rtl);
              break;

            // Up arrow.
            case 38:
            // Safari up arrow.
            case 63232: {
              let $previousRow = $(self.rowObject.element).prev('tr').eq(0);
              let previousRow = $previousRow.get(0);
              while (previousRow && $previousRow.is(':hidden')) {
                $previousRow = $(previousRow).prev('tr').eq(0);
                previousRow = $previousRow.get(0);
              }
              if (previousRow) {
                // Do not allow the onBlur cleanup.
                self.safeBlur = false;
                self.rowObject.direction = 'up';
                keyChange = true;

                if ($(item).is('.tabledrag-root')) {
                  // Swap with the previous top-level row.
                  groupHeight = 0;
                  while (
                    previousRow &&
                    $previousRow.find('.js-indentation').length
                  ) {
                    $previousRow = $(previousRow).prev('tr').eq(0);
                    previousRow = $previousRow.get(0);
                    groupHeight += $previousRow.is(':hidden')
                      ? 0
                      : previousRow.offsetHeight;
                  }
                  if (previousRow) {
                    self.rowObject.swap('before', previousRow);
                    // No need to check for indentation, 0 is the only valid one.
                    window.scrollBy(0, -groupHeight);
                  }
                } else if (
                  self.table.tBodies[0].rows[0] !== previousRow ||
                  $previousRow.is('.draggable')
                ) {
                  // Swap with the previous row (unless previous row is the first
                  // one and undraggable).
                  self.rowObject.swap('before', previousRow);
                  self.rowObject.interval = null;
                  self.rowObject.indent(0);
                  window.scrollBy(0, -parseInt(item.offsetHeight, 10));
                }
                // Regain focus after the DOM manipulation.
                handle.trigger('focus');
              }
              break;
            }
            // Right arrow.
            case 39:
            // Safari right arrow.
            case 63235:
              keyChange = true;
              self.rowObject.indent(self.rtl);
              break;

            // Down arrow.
            case 40:
            // Safari down arrow.
            case 63233: {
              let $nextRow = $(self.rowObject.group).eq(-1).next('tr').eq(0);
              let nextRow = $nextRow.get(0);
              while (nextRow && $nextRow.is(':hidden')) {
                $nextRow = $(nextRow).next('tr').eq(0);
                nextRow = $nextRow.get(0);
              }
              if (nextRow) {
                // Do not allow the onBlur cleanup.
                self.safeBlur = false;
                self.rowObject.direction = 'down';
                keyChange = true;

                if ($(item).is('.tabledrag-root')) {
                  // Swap with the next group (necessarily a top-level one).
                  groupHeight = 0;
                  const nextGroup = new self.row(
                    nextRow,
                    'keyboard',
                    self.indentEnabled,
                    self.maxDepth,
                    false,
                  );
                  if (nextGroup) {
                    $(nextGroup.group).each(function groupIterator() {
                      groupHeight += $(this).is(':hidden')
                        ? 0
                        : this.offsetHeight;
                    });
                    const nextGroupRow = $(nextGroup.group).eq(-1).get(0);
                    self.rowObject.swap('after', nextGroupRow);
                    // No need to check for indentation, 0 is the only valid one.
                    window.scrollBy(0, parseInt(groupHeight, 10));
                  }
                } else {
                  // Swap with the next row.
                  self.rowObject.swap('after', nextRow);
                  self.rowObject.interval = null;
                  self.rowObject.indent(0);
                  window.scrollBy(0, parseInt(item.offsetHeight, 10));
                }
                // Regain focus after the DOM manipulation.
                handle.trigger('focus');
              }
              break;
            }
          }

          /* eslint-enable no-fallthrough */

          if (self.rowObject && self.rowObject.changed === true) {
            $(item).addClass('drag');
            if (self.oldRowElement) {
              $(self.oldRowElement).removeClass('drag-previous');
            }
            self.oldRowElement = item;
            if (self.striping === true) {
              self.restripeTable();
            }
            self.onDrag();
          }

          // Returning false if we have an arrow key to prevent scrolling.
          if (keyChange) {
            return false;
          }
        });

        // Compatibility addition, return false on keypress to prevent unwanted
        // scrolling. IE and Safari will suppress scrolling on keydown, but all
        // other browsers need to return false on keypress.
        // http://www.quirksmode.org/js/keys.html
        handle.on('keypress', (event) => {
          /* eslint-disable no-fallthrough */

          switch (event.keyCode) {
            // Left arrow.
            case 37:
            // Up arrow.
            case 38:
            // Right arrow.
            case 39:
            // Down arrow.
            case 40:
              return false;
          }

          /* eslint-enable no-fallthrough */
        });
      },

      /**
       * Pointer event initiator, creates drag object and information.
       *
       * @param {jQuery.Event} event
       *   The event object that trigger the drag.
       * @param {Drupal.tableDrag} self
       *   The drag handle.
       * @param {HTMLElement} item
       *   The item that is being dragged.
       */
      dragStart(event, self, item) {
        // Create a new dragObject recording the pointer information.
        self.dragObject = {};
        self.dragObject.initOffset = self.getPointerOffset(item, event);
        self.dragObject.initPointerCoords = self.pointerCoords(event);
        if (self.indentEnabled) {
          self.dragObject.indentPointerPos = self.dragObject.initPointerCoords;
        }

        // If there's a lingering row object from the keyboard, remove its focus.
        if (self.rowObject) {
          $(self.rowObject.element)
            .find('.js-tabledrag-handle')
            .trigger('blur');
        }

        // Create a new rowObject for manipulation of this row.
        self.rowObject = new self.row(
          item,
          'pointer',
          self.indentEnabled,
          self.maxDepth,
          true,
        );

        // Save the position of the table.
        self.table.topY = $(self.table).offset().top;
        self.table.bottomY = self.table.topY + self.table.offsetHeight;

        // Add classes to the handle and row.
        $(item).addClass('drag');

        // Set the document to use the move cursor during drag.
        $('body').addClass('drag');
        if (self.oldRowElement) {
          $(self.oldRowElement).removeClass('drag-previous');
        }

        // Set the initial y coordinate so the direction can be calculated in
        // dragRow().
        self.oldY = self.pointerCoords(event).y;
      },

      /**
       * Pointer movement handler, bound to document.
       *
       * @param {jQuery.Event} event
       *   The pointer event.
       * @param {Drupal.tableDrag} self
       *   The tableDrag instance.
       *
       * @return {bool|undefined}
       *   Undefined if no dragObject is defined, false otherwise.
       */
      dragRow(event, self) {
        if (self.dragObject) {
          self.currentPointerCoords = self.pointerCoords(event);
          const y = self.currentPointerCoords.y - self.dragObject.initOffset.y;
          const x = self.currentPointerCoords.x - self.dragObject.initOffset.x;

          // Check for row swapping and vertical scrolling.
          if (y !== self.oldY) {
            self.rowObject.direction = y > self.oldY ? 'down' : 'up';
            // Update the old value.
            self.oldY = y;
            // Check if the window should be scrolled (and how fast).
            const scrollAmount = self.checkScroll(self.currentPointerCoords.y);
            // Stop any current scrolling.
            clearInterval(self.scrollInterval);
            // Continue scrolling if the mouse has moved in the scroll direction.
            if (
              (scrollAmount > 0 && self.rowObject.direction === 'down') ||
              (scrollAmount < 0 && self.rowObject.direction === 'up')
            ) {
              self.setScroll(scrollAmount);
            }

            // If we have a valid target, perform the swap and restripe the table.
            const currentRow = self.findDropTargetRow(x, y);
            if (currentRow) {
              if (self.rowObject.direction === 'down') {
                self.rowObject.swap('after', currentRow, self);
              } else {
                self.rowObject.swap('before', currentRow, self);
              }
              if (self.striping === true) {
                self.restripeTable();
              }
            }
          }

          // Similar to row swapping, handle indentations.
          if (self.indentEnabled) {
            const xDiff =
              self.currentPointerCoords.x - self.dragObject.indentPointerPos.x;
            // Set the number of indentations the pointer has been moved left or
            // right.
            const indentDiff = Math.round(xDiff / self.indentAmount);
            // Indent the row with our estimated diff, which may be further
            // restricted according to the rows around this row.
            const indentChange = self.rowObject.indent(indentDiff);
            // Update table and pointer indentations.
            self.dragObject.indentPointerPos.x +=
              self.indentAmount * indentChange;
            self.indentCount = Math.max(
              self.indentCount,
              self.rowObject.indents,
            );
          }

          return false;
        }
      },

      /**
       * Pointerup behavior.
       *
       * @param {jQuery.Event} event
       *   The pointer event.
       * @param {Drupal.tableDrag} self
       *   The tableDrag instance.
       */
      dropRow(event, self) {
        let droppedRow;
        let $droppedRow;

        // Drop row functionality.
        if (self.rowObject !== null) {
          droppedRow = self.rowObject.element;
          $droppedRow = $(droppedRow);
          // The row is already in the right place so we just release it.
          if (self.rowObject.changed === true) {
            // Update the fields in the dropped row.
            self.updateFields(droppedRow);

            // If a setting exists for affecting the entire group, update all the
            // fields in the entire dragged group.
            Object.keys(self.tableSettings || {}).forEach((group) => {
              const rowSettings = self.rowSettings(group, droppedRow);
              if (rowSettings.relationship === 'group') {
                Object.keys(self.rowObject.children || {}).forEach((n) => {
                  self.updateField(self.rowObject.children[n], group);
                });
              }
            });

            self.rowObject.markChanged();
            if (self.changed === false) {
              const $messageTarget = $(self.table).prevAll(
                '.js-tabledrag-toggle-weight-wrapper',
              ).length
                ? $(self.table)
                    .prevAll('.js-tabledrag-toggle-weight-wrapper')
                    .last()
                : self.table;
              $(Drupal.theme('tableDragChangedWarning'))
                .insertBefore($messageTarget)
                .hide()
                .fadeIn('slow');
              self.changed = true;
            }
          }

          if (self.indentEnabled) {
            self.rowObject.removeIndentClasses();
          }
          if (self.oldRowElement) {
            $(self.oldRowElement).removeClass('drag-previous');
          }
          $droppedRow.removeClass('drag').addClass('drag-previous');
          self.oldRowElement = droppedRow;
          self.onDrop();
          self.rowObject = null;
        }

        // Functionality specific only to pointerup events.
        if (self.dragObject !== null) {
          self.dragObject = null;
          $('body').removeClass('drag');
          clearInterval(self.scrollInterval);
        }
      },

      /**
       * Get the coordinates from the event (allowing for browser differences).
       *
       * @param {jQuery.Event} event
       *   The pointer event.
       *
       * @return {object}
       *   An object with `x` and `y` keys indicating the position.
       */
      pointerCoords(event) {
        if (event.pageX || event.pageY) {
          return { x: event.pageX, y: event.pageY };
        }
        return {
          x:
            event.clientX +
            (document.body.scrollLeft - document.body.clientLeft),
          y:
            event.clientY + (document.body.scrollTop - document.body.clientTop),
        };
      },

      /**
       * Get the event offset from the target element.
       *
       * Given a target element and a pointer event, get the event offset from that
       * element. To do this we need the element's position and the target position.
       *
       * @param {HTMLElement} target
       *   The target HTML element.
       * @param {jQuery.Event} event
       *   The pointer event.
       *
       * @return {object}
       *   An object with `x` and `y` keys indicating the position.
       */
      getPointerOffset(target, event) {
        const docPos = $(target).offset();
        const pointerPos = this.pointerCoords(event);
        return { x: pointerPos.x - docPos.left, y: pointerPos.y - docPos.top };
      },

      /**
       * Find the row the mouse is currently over.
       *
       * This row is then taken and swapped with the one being dragged.
       *
       * @param {number} x
       *   The x coordinate of the mouse on the page (not the screen).
       * @param {number} y
       *   The y coordinate of the mouse on the page (not the screen).
       *
       * @return {*}
       *   The drop target row, if found.
       */
      findDropTargetRow(x, y) {
        const rows = $(this.table.tBodies[0].rows).not(':hidden');
        for (let n = 0; n < rows.length; n++) {
          let row = rows[n];
          let $row = $(row);
          const rowY = $row.offset().top;
          let rowHeight;
          // Because Safari does not report offsetHeight on table rows, but does on
          // table cells, grab the firstChild of the row and use that instead.
          // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari.
          if (row.offsetHeight === 0) {
            rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2;
          }
          // Other browsers.
          else {
            rowHeight = parseInt(row.offsetHeight, 10) / 2;
          }

          // Because we always insert before, we need to offset the height a bit.
          if (y > rowY - rowHeight && y < rowY + rowHeight) {
            if (this.indentEnabled) {
              // Check that this row is not a child of the row being dragged.
              if (
                Object.keys(this.rowObject.group).some(
                  (o) => this.rowObject.group[o] === row,
                )
              ) {
                return null;
              }
            }
            // Do not allow a row to be swapped with itself.
            else if (row === this.rowObject.element) {
              return null;
            }

            // Check that swapping with this row is allowed.
            if (!this.rowObject.isValidSwap(row)) {
              return null;
            }

            // We may have found the row the mouse just passed over, but it doesn't
            // take into account hidden rows. Skip backwards until we find a
            // draggable row.
            while ($row.is(':hidden') && $row.prev('tr').is(':hidden')) {
              $row = $row.prev('tr:first-of-type');
              row = $row.get(0);
            }
            return row;
          }
        }
        return null;
      },

      /**
       * After the row is dropped, update the table fields.
       *
       * @param {HTMLElement} changedRow
       *   DOM object for the row that was just dropped.
       */
      updateFields(changedRow) {
        Object.keys(this.tableSettings || {}).forEach((group) => {
          // Each group may have a different setting for relationship, so we find
          // the source rows for each separately.
          this.updateField(changedRow, group);
        });
      },

      /**
       * After the row is dropped, update a single table field.
       *
       * @param {HTMLElement} changedRow
       *   DOM object for the row that was just dropped.
       * @param {string} group
       *   The settings group on which field updates will occur.
       */
      updateField(changedRow, group) {
        let rowSettings = this.rowSettings(group, changedRow);
        const $changedRow = $(changedRow);
        let sourceRow;
        let $previousRow;
        let previousRow;
        let useSibling;
        // Set the row as its own target.
        if (
          rowSettings.relationship === 'self' ||
          rowSettings.relationship === 'group'
        ) {
          sourceRow = changedRow;
        }
        // Siblings are easy, check previous and next rows.
        else if (rowSettings.relationship === 'sibling') {
          $previousRow = $changedRow.prev('tr:first-of-type');
          previousRow = $previousRow.get(0);
          const $nextRow = $changedRow.next('tr:first-of-type');
          const nextRow = $nextRow.get(0);
          sourceRow = changedRow;
          if (
            $previousRow.is('.draggable') &&
            $previousRow.find(`.${group}`).length
          ) {
            if (this.indentEnabled) {
              if (
                $previousRow.find('.js-indentations').length ===
                $changedRow.find('.js-indentations').length
              ) {
                sourceRow = previousRow;
              }
            } else {
              sourceRow = previousRow;
            }
          } else if (
            $nextRow.is('.draggable') &&
            $nextRow.find(`.${group}`).length
          ) {
            if (this.indentEnabled) {
              if (
                $nextRow.find('.js-indentations').length ===
                $changedRow.find('.js-indentations').length
              ) {
                sourceRow = nextRow;
              }
            } else {
              sourceRow = nextRow;
            }
          }
        }
        // Parents, look up the tree until we find a field not in this group.
        // Go up as many parents as indentations in the changed row.
        else if (rowSettings.relationship === 'parent') {
          $previousRow = $changedRow.prev('tr');
          previousRow = $previousRow;
          while (
            $previousRow.length &&
            $previousRow.find('.js-indentation').length >=
              this.rowObject.indents
          ) {
            $previousRow = $previousRow.prev('tr');
            previousRow = $previousRow;
          }
          // If we found a row.
          if ($previousRow.length) {
            sourceRow = $previousRow.get(0);
          }
          // Otherwise we went all the way to the left of the table without finding
          // a parent, meaning this item has been placed at the root level.
          else {
            // Use the first row in the table as source, because it's guaranteed to
            // be at the root level. Find the first item, then compare this row
            // against it as a sibling.
            sourceRow = $(this.table).find('tr.draggable:first-of-type').get(0);
            if (sourceRow === this.rowObject.element) {
              sourceRow = $(
                this.rowObject.group[this.rowObject.group.length - 1],
              )
                .next('tr.draggable')
                .get(0);
            }
            useSibling = true;
          }
        }

        // Because we may have moved the row from one category to another,
        // take a look at our sibling and borrow its sources and targets.
        this.copyDragClasses(sourceRow, changedRow, group);
        rowSettings = this.rowSettings(group, changedRow);

        // In the case that we're looking for a parent, but the row is at the top
        // of the tree, copy our sibling's values.
        if (useSibling) {
          rowSettings.relationship = 'sibling';
          rowSettings.source = rowSettings.target;
        }

        const targetClass = `.${rowSettings.target}`;
        const targetElement = $changedRow.find(targetClass).get(0);

        // Check if a target element exists in this row.
        if (targetElement) {
          const sourceClass = `.${rowSettings.source}`;
          const sourceElement = $(sourceClass, sourceRow).get(0);
          switch (rowSettings.action) {
            case 'depth':
              // Get the depth of the target row.
              targetElement.value = $(sourceElement)
                .closest('tr')
                .find('.js-indentation').length;
              break;

            case 'match':
              // Update the value.
              targetElement.value = sourceElement.value;
              break;

            case 'order': {
              const siblings = this.rowObject.findSiblings(rowSettings);
              if ($(targetElement).is('select')) {
                // Get a list of acceptable values.
                const values = [];
                $(targetElement)
                  .find('option')
                  .each(function collectValues() {
                    values.push(this.value);
                  });
                const maxVal = values[values.length - 1];
                // Populate the values in the siblings.
                $(siblings)
                  .find(targetClass)
                  .each(function assignValues() {
                    // If there are more items than possible values, assign the
                    // maximum value to the row.
                    if (values.length > 0) {
                      this.value = values.shift();
                    } else {
                      this.value = maxVal;
                    }
                  });
              } else {
                // Assume a numeric input field.
                let weight =
                  parseInt($(siblings[0]).find(targetClass).val(), 10) || 0;
                $(siblings)
                  .find(targetClass)
                  .each(function assignWeight() {
                    this.value = weight;
                    weight += 1;
                  });
              }
              break;
            }
          }
        }
      },

      /**
       * Copy all tableDrag related classes from one row to another.
       *
       * Copy all special tableDrag classes from one row's form elements to a
       * different one, removing any special classes that the destination row
       * may have had.
       *
       * @param {HTMLElement} sourceRow
       *   The element for the source row.
       * @param {HTMLElement} targetRow
       *   The element for the target row.
       * @param {string} group
       *   The group selector.
       */
      copyDragClasses(sourceRow, targetRow, group) {
        const sourceElement = $(sourceRow).find(`.${group}`);
        const targetElement = $(targetRow).find(`.${group}`);
        if (sourceElement.length && targetElement.length) {
          targetElement[0].className = sourceElement[0].className;
        }
      },

      /**
       * Check the suggested scroll of the table.
       *
       * @param {number} cursorY
       *   The Y position of the cursor.
       *
       * @return {number}
       *   The suggested scroll.
       */
      checkScroll(cursorY) {
        const de = document.documentElement;
        const b = document.body;
        const windowHeight =
          window.innerHeight ||
          (de.clientHeight && de.clientWidth !== 0
            ? de.clientHeight
            : b.offsetHeight);
        this.windowHeight = windowHeight;
        let scrollY;
        if (document.all) {
          scrollY = !de.scrollTop ? b.scrollTop : de.scrollTop;
        } else {
          scrollY = window.pageYOffset ? window.pageYOffset : window.scrollY;
        }
        this.scrollY = scrollY;
        const { trigger } = this.scrollSettings;
        let delta = 0;

        // Return a scroll speed relative to the edge of the screen.
        if (cursorY - scrollY > windowHeight - trigger) {
          delta = trigger / (windowHeight + (scrollY - cursorY));
          delta = delta > 0 && delta < trigger ? delta : trigger;
          return delta * this.scrollSettings.amount;
        }
        if (cursorY - scrollY < trigger) {
          delta = trigger / (cursorY - scrollY);
          delta = delta > 0 && delta < trigger ? delta : trigger;
          return -delta * this.scrollSettings.amount;
        }
      },

      /**
       * Set the scroll for the table.
       *
       * @param {number} scrollAmount
       *   The amount of scroll to apply to the window.
       */
      setScroll(scrollAmount) {
        const self = this;

        this.scrollInterval = setInterval(() => {
          // Update the scroll values stored in the object.
          self.checkScroll(self.currentPointerCoords.y);
          const aboveTable = self.scrollY > self.table.topY;
          const belowTable =
            self.scrollY + self.windowHeight < self.table.bottomY;
          if (
            (scrollAmount > 0 && belowTable) ||
            (scrollAmount < 0 && aboveTable)
          ) {
            window.scrollBy(0, scrollAmount);
          }
        }, this.scrollSettings.interval);
      },

      /**
       * Command to restripe table properly.
       */
      restripeTable() {
        // :even and :odd are reversed because jQuery counts from 0 and
        // we count from 1, so we're out of sync.
        // Match immediate children of the parent element to allow nesting.
        $(this.table)
          .find('> tbody > tr.draggable, > tr.draggable')
          .filter(':visible')
          .filter(':odd')
          .removeClass('odd')
          .addClass('even')
          .end()
          .filter(':even')
          .removeClass('even')
          .addClass('odd');
      },

      /**
       * Stub function. Allows a custom handler when a row begins dragging.
       *
       * @return {null}
       *   Returns null when the stub function is used.
       */
      onDrag() {
        return null;
      },

      /**
       * Stub function. Allows a custom handler when a row is dropped.
       *
       * @return {null}
       *   Returns null when the stub function is used.
       */
      onDrop() {
        return null;
      },

      /**
       * Constructor to make a new object to manipulate a table row.
       *
       * @param {HTMLElement} tableRow
       *   The DOM element for the table row we will be manipulating.
       * @param {string} method
       *   The method in which this row is being moved. Either 'keyboard' or
       *   'mouse'.
       * @param {bool} indentEnabled
       *   Whether the containing table uses indentations. Used for optimizations.
       * @param {number} maxDepth
       *   The maximum amount of indentations this row may contain.
       * @param {bool} addClasses
       *   Whether we want to add classes to this row to indicate child
       *   relationships.
       */
      row(tableRow, method, indentEnabled, maxDepth, addClasses) {
        const $tableRow = $(tableRow);

        this.element = tableRow;
        this.method = method;
        this.group = [tableRow];
        this.groupDepth = $tableRow.find('.js-indentation').length;
        this.changed = false;
        this.table = $tableRow.closest('table')[0];
        this.indentEnabled = indentEnabled;
        this.maxDepth = maxDepth;
        // Direction the row is being moved.
        this.direction = '';
        if (this.indentEnabled) {
          this.indents = $tableRow.find('.js-indentation').length;
          this.children = this.findChildren(addClasses);
          this.group = $.merge(this.group, this.children);
          // Find the depth of this entire group.
          for (let n = 0; n < this.group.length; n++) {
            this.groupDepth = Math.max(
              $(this.group[n]).find('.js-indentation').length,
              this.groupDepth,
            );
          }
        }
      },
    },
  );

  $.extend(Drupal.tableDrag.prototype.row.prototype, /** @lends Drupal.tableDrag#.row# */{
    /**
     * Find all children of rowObject by indentation.
     *
     * @param {bool} addClasses
     *   Whether we want to add classes to this row to indicate child
     *   relationships.
     *
     * @return {Array}
     *   An array of children of the row.
     */
    findChildren(addClasses) {
      const parentIndentation = this.indents;
      let currentRow = $(this.element, this.table).next('tr.draggable');
      const rows = [];
      let child = 0;

      function rowIndentation(indentNum, el) {
        const self = $(el);
        if (child === 1 && indentNum === parentIndentation) {
          self.addClass('tree-child-first');
        }
        if (indentNum === parentIndentation) {
          self.addClass('tree-child');
        } else if (indentNum > parentIndentation) {
          self.addClass('tree-child-horizontal');
        }
      }

      while (currentRow.length) {
        // A greater indentation indicates this is a child.
        if (currentRow.find('.js-indentation').length > parentIndentation) {
          child += 1;
          rows.push(currentRow[0]);
          if (addClasses) {
            currentRow.find('.js-indentation').each(rowIndentation);
          }
        } else {
          break;
        }
        currentRow = currentRow.next('tr.draggable');
      }
      if (addClasses && rows.length) {
        $(rows[rows.length - 1])
          .find(`.js-indentation:nth-child(${parentIndentation + 1})`)
          .addClass('tree-child-last');
      }
      return rows;
    },

    /**
     * Ensure that two rows are allowed to be swapped.
     *
     * @param {HTMLElement} row
     *   DOM object for the row being considered for swapping.
     *
     * @return {bool}
     *   Whether the swap is a valid swap or not.
     */
    isValidSwap(row) {
      const $row = $(row);
      if (this.indentEnabled) {
        let prevRow;
        let nextRow;
        if (this.direction === 'down') {
          prevRow = row;
          nextRow = $row.next('tr').get(0);
        } else {
          prevRow = $row.prev('tr').get(0);
          nextRow = row;
        }
        this.interval = this.validIndentInterval(prevRow, nextRow);

        // We have an invalid swap if the valid indentations interval is empty.
        if (this.interval.min > this.interval.max) {
          return false;
        }
      }

      // Do not let an un-draggable first row have anything put before it.
      if (
        this.table.tBodies[0].rows[0] === row &&
        $row.is(':not(.draggable)')
      ) {
        return false;
      }

      return true;
    },

    /**
     * Perform the swap between two rows.
     *
     * @param {string} position
     *   Whether the swap will occur 'before' or 'after' the given row.
     * @param {HTMLElement} row
     *   DOM element what will be swapped with the row group.
     */
    swap(position, row) {
      // Makes sure only DOM object are passed to Drupal.detachBehaviors().
      this.group.forEach((detachedRow) => {
        Drupal.detachBehaviors(detachedRow, drupalSettings, 'move');
      });
      $(row)[position](this.group);
      // Makes sure only DOM object are passed to Drupal.attachBehaviors()s.
      this.group.forEach((attachedRow) => {
        Drupal.attachBehaviors(attachedRow, drupalSettings);
      });
      this.changed = true;
      this.onSwap(row);
    },

    /**
     * Determine the valid indentations interval for the row at a given position.
     *
     * @param {?HTMLElement} prevRow
     *   DOM object for the row before the tested position
     *   (or null for first position in the table).
     * @param {?HTMLElement} nextRow
     *   DOM object for the row after the tested position
     *   (or null for last position in the table).
     *
     * @return {object}
     *   An object with the keys `min` and `max` to indicate the valid indent
     *   interval.
     */
    validIndentInterval(prevRow, nextRow) {
      const $prevRow = $(prevRow);
      let maxIndent;

      // Minimum indentation:
      // Do not orphan the next row.
      const minIndent = nextRow ? $(nextRow).find('.js-indentation').length : 0;

      // Maximum indentation:
      if (
        !prevRow ||
        $prevRow.is(':not(.draggable)') ||
        $(this.element).is('.tabledrag-root')
      ) {
        // Do not indent:
        // - the first row in the table,
        // - rows dragged below a non-draggable row,
        // - 'root' rows.
        maxIndent = 0;
      } else {
        // Do not go deeper than as a child of the previous row.
        maxIndent =
          $prevRow.find('.js-indentation').length +
          ($prevRow.is('.tabledrag-leaf') ? 0 : 1);
        // Limit by the maximum allowed depth for the table.
        if (this.maxDepth) {
          maxIndent = Math.min(
            maxIndent,
            this.maxDepth - (this.groupDepth - this.indents),
          );
        }
      }

      return { min: minIndent, max: maxIndent };
    },

    /**
     * Indent a row within the legal bounds of the table.
     *
     * @param {number} indentDiff
     *   The number of additional indentations proposed for the row (can be
     *   positive or negative). This number will be adjusted to nearest valid
     *   indentation level for the row.
     *
     * @return {number}
     *   The number of indentations applied.
     */
    indent(indentDiff) {
      const $group = $(this.group);
      // Determine the valid indentations interval if not available yet.
      if (!this.interval) {
        const prevRow = $(this.element).prev('tr').get(0);
        const nextRow = $group.eq(-1).next('tr').get(0);
        this.interval = this.validIndentInterval(prevRow, nextRow);
      }

      // Adjust to the nearest valid indentation.
      let indent = this.indents + indentDiff;
      indent = Math.max(indent, this.interval.min);
      indent = Math.min(indent, this.interval.max);
      indentDiff = indent - this.indents;

      for (let n = 1; n <= Math.abs(indentDiff); n++) {
        // Add or remove indentations.
        if (indentDiff < 0) {
          $group.find('.js-indentation:first-of-type').remove();
          this.indents -= 1;
        } else {
          $group
            .find('.js-tabledrag-cell-content')
            .prepend(Drupal.theme('tableDragIndentation'));
          this.indents += 1;
        }
      }
      if (indentDiff) {
        // Update indentation for this row.
        this.changed = true;
        this.groupDepth += indentDiff;
        this.onIndent();
      }

      return indentDiff;
    },

    /**
     * Find all siblings for a row.
     *
     * According to its subgroup or indentation. Note that the passed-in row is
     * included in the list of siblings.
     *
     * @param {object} rowSettings
     *   The field settings we're using to identify what constitutes a sibling.
     *
     * @return {Array}
     *   An array of siblings.
     */
    findSiblings(rowSettings) {
      const siblings = [];
      const directions = ['prev', 'next'];
      const rowIndentation = this.indents;
      let checkRowIndentation;
      for (let d = 0; d < directions.length; d++) {
        let checkRow = $(this.element)[directions[d]]();
        while (checkRow.length) {
          // Check that the sibling contains a similar target field.
          if (checkRow.find(`.${rowSettings.target}`)) {
            // Either add immediately if this is a flat table, or check to
            // ensure that this row has the same level of indentation.
            if (this.indentEnabled) {
              checkRowIndentation = checkRow.find('.js-indentation').length;
            }

            if (!this.indentEnabled || checkRowIndentation === rowIndentation) {
              siblings.push(checkRow[0]);
            } else if (checkRowIndentation < rowIndentation) {
              // No need to keep looking for siblings when we get to a parent.
              break;
            }
          } else {
            break;
          }
          checkRow = checkRow[directions[d]]();
        }
        // Since siblings are added in reverse order for previous, reverse the
        // completed list of previous siblings. Add the current row and
        // continue.
        if (directions[d] === 'prev') {
          siblings.reverse();
          siblings.push(this.element);
        }
      }
      return siblings;
    },

    /**
     * Remove indentation helper classes from the current row group.
     */
    removeIndentClasses() {
      Object.keys(this.children || {}).forEach((n) => {
        $(this.children[n])
          .find('.js-indentation')
          .removeClass('tree-child')
          .removeClass('tree-child-first')
          .removeClass('tree-child-last')
          .removeClass('tree-child-horizontal');
      });
    },

    /**
     * Add an asterisk or other marker to the changed row.
     */
    markChanged() {
      const marker = $(Drupal.theme('tableDragChangedMarker')).addClass(
        'js-tabledrag-changed-marker',
      );
      const cell = $(this.element).find('td:first-of-type');
      if (cell.find('.js-tabledrag-changed-marker').length === 0) {
        cell.find('.js-tabledrag-handle').after(marker);
      }
    },

    /**
     * Stub function. Allows a custom handler when a row is indented.
     *
     * @return {null}
     *   Returns null when the stub function is used.
     */
    onIndent() {
      return null;
    },

    /**
     * Stub function. Allows a custom handler when a row is swapped.
     *
     * @param {HTMLElement} swappedRow
     *   The element for the swapped row.
     *
     * @return {null}
     *   Returns null when the stub function is used.
     */
    // eslint-disable-next-line no-unused-vars
    onSwap(swappedRow) {
      return null;
    },
  });

  $.extend(
    Drupal.theme,
    /** @lends Drupal.theme */ {
      /**
       * @return {string}
       *  Markup for the marker.
       */
      tableDragChangedMarker() {
        return `<abbr class="warning tabledrag-changed" title="${Drupal.t(
          'Changed',
        )}">*</abbr>`;
      },

      /**
       * @return {string}
       *   Markup for the indentation.
       */
      tableDragIndentation() {
        return '<div class="js-indentation indentation"><svg xmlns="http://www.w3.org/2000/svg" class="tree" width="25" height="25" viewBox="0 0 25 25"><path class="tree__item tree__item-child-ltr tree__item-child-last-ltr tree__item-horizontal tree__item-horizontal-right" d="M12,12.5 H25" stroke="#888"/><path class="tree__item tree__item-child-rtl tree__item-child-last-rtl tree__item-horizontal tree__horizontal-left" d="M0,12.5 H13" stroke="#888"/><path class="tree__item tree__item-child-ltr tree__item-child-rtl tree__item-child-last-ltr tree__item-child-last-rtl tree__vertical tree__vertical-top" d="M12.5,12 v-99" stroke="#888"/><path class="tree__item tree__item-child-ltr tree__item-child-rtl tree__vertical tree__vertical-bottom" d="M12.5,12 v99" stroke="#888"/></svg></div>';
      },

      /**
       * @return {string}
       *   Markup for the warning.
       */
      tableDragChangedWarning() {
        return `<div class="tabledrag-changed-warning messages messages--warning" role="alert">${Drupal.theme(
          'tableDragChangedMarker',
        )} ${Drupal.t('You have unsaved changes.')}</div>`;
      },

      /**
       * Constructs the table drag handle.
       *
       * @return {string}
       *   A string representing a DOM fragment.
       */
      tableDragHandle() {
        return '<a href="#" class="tabledrag-handle"></a>';
      },

      /**
       * Constructs the wrapper for the whole table drag cell.
       *
       * @return {string}
       *   A string representing a DOM fragment.
       */
      tableDragCellItemsWrapper() {
        return '<div class="tabledrag-cell-content"></div>';
      },

      /**
       * Constructs the wrapper for the initial content of the drag cell.
       *
       * @return {string}
       *   A string representing a DOM fragment.
       */
      tableDragCellContentWrapper() {
        return '<div class="tabledrag-cell-content__item"></div>';
      },

      /**
       * Constructs the weight column toggle.
       *
       * The 'tabledrag-toggle-weight' CSS class should be kept since it is used
       * elsewhere as well (e.g. in tests).
       *
       * @param {string} action
       *   The action the toggle will perform.
       * @param {string} text
       *   The text content of the toggle.
       *
       * @return {string}
       *   A string representing a DOM fragment.
       */
      tableDragToggle(action, text) {
        const classes = [
          'action-link',
          'action-link--extrasmall',
          'tabledrag-toggle-weight',
        ];
        switch (action) {
          case 'show':
            classes.push('action-link--icon-show');
            break;

          default:
            classes.push('action-link--icon-hide');
            break;
        }

        return `<a href="#" class="${classes.join(' ')}">${text}</a>`;
      },

      /**
       * Constructs the wrapper of the weight column toggle.
       *
       * The 'tabledrag-toggle-weight-wrapper' CSS class should be kept since it is used
       * by Views UI and inside off-canvas dialogs.
       *
       * @return {string}
       *   A string representing a DOM fragment.
       */
      tableDragToggleWrapper() {
        return '<div class="tabledrag-toggle-weight-wrapper"></div>';
      },
    },
  );
})(jQuery, Drupal, drupalSettings);