claro-8.x-1.x-dev/js/tabledrag.es6.js
js/tabledrag.es6.js
/** * @file * Overrides tabledrag.js that provides dragging capabilities. * * - New Drupal.theme.tableDragHandle() function for tabledrag handle markup * - 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 * - 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 * identation elements and the tabledrag changed mark as well. * This is needed to keep all of these element in a single line * 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 * - 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. * * @todo Refactor after https://www.drupal.org/node/3077938, * 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 = 0; /** * 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/>" ) .addClass( "draggable" ) .appendTo(table); const testCell = $( "<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, { /** * 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 = 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 = 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. 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 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" ); } }, /** * 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. 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, { /** * 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>`; }, /** * Constucts 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" /> '; }, /** * 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" /> '; }, /** * 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); |