/** * @file * Extends methods from core/misc/tabledrag.js. */ (function ($) { // Save the original prototype. var prototype = Drupal.tableDrag.prototype; /** * 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 (table, tableSettings) { var self = this; var $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; /** * @type {?HTMLElement} */ this.$toggleWeightButton = null; /** * Check this table's settings to see if there are parent relationships in * this table. 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; for (var group in tableSettings) { if (tableSettings.hasOwnProperty(group)) { for (var n in tableSettings[group]) { if (tableSettings[group].hasOwnProperty(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. var indent = Drupal.theme('tableDragIndentation'); var testRow = $('').addClass('draggable').appendTo(table); var testCell = $('').appendTo(testRow).prepend(indent).prepend(indent); var $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 () { self.makeDraggable(this); }); // Add a link before the table for users to show or hide weight columns. self.$toggleWeightButton = $(Drupal.theme('btn-sm', { 'class': ['tabledrag-toggle-weight'], 'data-drupal-selector': ['tabledrag-toggle-weight'], title: Drupal.t('Re-order rows by numerical weight instead of dragging.'), 'data-toggle': 'tooltip' })); self.$toggleWeightButton .on('click', $.proxy(function (e) { e.preventDefault(); this.toggleColumns(); }, this)) .wrap('
') .parent(); $table.before(self.$toggleWeightButton); // 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', function (event) { return self.dragRow(event.originalEvent.touches[0], self); }); $(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); }); $(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); }); $(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); }); // React to localStorage event showing or hiding weight columns. $(window).on('storage', $.proxy(function (e) { // Only react to 'Drupal.tableDrag.showWeight' value change. if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') { // This was changed in another window, get the new value for this // window. showWeight = JSON.parse(e.originalEvent.newValue); this.displayColumns(showWeight); } }, this)); }; // Restore the original prototype. Drupal.tableDrag.prototype = prototype; /** * Take an item and add event handlers to make it become draggable. * * @param {HTMLElement} item */ Drupal.tableDrag.prototype.makeDraggable = function (item) { var self = this; var $item = $(item); // Add a class to the title link $item.find('td:first-of-type').find('a').addClass('menu-item__link'); // Create the handle. var handle = $(''); // Insert the handle after indentations (if any). var $indentationLast = $item.find('td:first-of-type').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 { $item.find('td').eq(0).prepend(handle); } // Add the glyphicon to the handle. handle .attr('title', Drupal.t('Drag to re-order')) .attr('data-toggle', 'tooltip') .append(Drupal.theme('bootstrapIcon', 'move')) ; handle.on('mousedown touchstart pointerdown', function (event) { event.preventDefault(); if (event.originalEvent.type === 'touchstart') { event = event.originalEvent.touches[0]; } self.dragStart(event, self, item); }); // Prevent the anchor tag from jumping us to the top of the page. handle.on('click', function (e) { e.preventDefault(); }); // Set blur cleanup when a handle is focused. handle.on('focus', function () { 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', function (event) { if (self.rowObject && self.safeBlur) { self.dropRow(event, self); } }); // Add arrow-key support to the handle. handle.on('keydown', function (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); } var keyChange = false; var groupHeight; 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: var $previousRow = $(self.rowObject.element).prev('tr:first-of-type'); var previousRow = $previousRow.get(0); while (previousRow && $previousRow.is(':hidden')) { $previousRow = $(previousRow).prev('tr:first-of-type'); 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:first-of-type'); 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: var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type'); var nextRow = $nextRow.get(0); while (nextRow && $nextRow.is(':hidden')) { $nextRow = $(nextRow).next('tr:first-of-type'); 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; var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false); if (nextGroup) { $(nextGroup.group).each(function () { groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight; }); var 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; } 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', function (event) { switch (event.keyCode) { // Left arrow. case 37: // Up arrow. case 38: // Right arrow. case 39: // Down arrow. case 40: return false; } }); }; /** * Add an asterisk or other marker to the changed row. */ Drupal.tableDrag.prototype.row.prototype.markChanged = function () { var $cell = $('td:first', this.element); // Find the first appropriate place to insert the marker. var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0)); if (!$cell.find('.tabledrag-changed').length) { $target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' '); } }; $.extend(Drupal.theme, /** @lends Drupal.theme */{ /** * @return {string} */ tableDragChangedMarker: function () { return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']}); }, /** * @return {string} */ tableDragChangedWarning: function () { return '
' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '
'; } }); })(jQuery);