3 * Extends methods from core/misc/tabledrag.js.
7 // Save the original prototype.
8 var prototype = Drupal.tableDrag.prototype;
11 * Provides table and field manipulation.
15 * @param {HTMLElement} table
16 * DOM object for the table to be made draggable.
17 * @param {object} tableSettings
18 * Settings for the table added via drupal_add_dragtable().
20 Drupal.tableDrag = function (table, tableSettings) {
22 var $table = $(table);
27 this.$table = $(table);
38 this.tableSettings = tableSettings;
41 * Used to hold information about a current drag operation.
43 * @type {?HTMLElement}
45 this.dragObject = null;
48 * Provides operations for row manipulation.
50 * @type {?HTMLElement}
52 this.rowObject = null;
55 * Remember the previous element.
57 * @type {?HTMLElement}
59 this.oldRowElement = null;
62 * Used to determine up or down direction from last mouse move.
69 * Whether anything in the entire table has changed.
76 * Maximum amount of allowed parenting.
83 * Direction of the table.
87 this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;
93 this.striping = $(this.table).data('striping') === 1;
96 * Configure the scroll settings.
100 * @prop {number} amount
101 * @prop {number} interval
102 * @prop {number} trigger
104 this.scrollSettings = {amount: 4, interval: 50, trigger: 70};
110 this.scrollInterval = null;
122 this.windowHeight = 0;
125 * Check this table's settings to see if there are parent relationships in
126 * this table. For efficiency, large sections of code can be skipped if we
127 * don't need to track horizontal movement and indentations.
131 this.indentEnabled = false;
132 for (var group in tableSettings) {
133 if (tableSettings.hasOwnProperty(group)) {
134 for (var n in tableSettings[group]) {
135 if (tableSettings[group].hasOwnProperty(n)) {
136 if (tableSettings[group][n].relationship === 'parent') {
137 this.indentEnabled = true;
139 if (tableSettings[group][n].limit > 0) {
140 this.maxDepth = tableSettings[group][n].limit;
146 if (this.indentEnabled) {
149 * Total width of indents, set in makeDraggable.
153 this.indentCount = 1;
154 // Find the width of indentations to measure mouse movements against.
155 // Because the table doesn't need to start with any indentations, we
156 // manually append 2 indentations in the first draggable row, measure
157 // the offset, then remove.
158 var indent = Drupal.theme('tableDragIndentation');
159 var testRow = $('<tr/>').addClass('draggable').appendTo(table);
160 var testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent);
161 var $indentation = testCell.find('.js-indentation');
167 this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
171 // Make each applicable row draggable.
172 // Match immediate children of the parent element to allow nesting.
173 $table.find('> tr.draggable, > tbody > tr.draggable').each(function () { self.makeDraggable(this); });
175 // Add a link before the table for users to show or hide weight columns.
176 var $button = $(Drupal.theme('btn-sm', {
177 'class': ['tabledrag-toggle-weight'],
178 title: Drupal.t('Re-order rows by numerical weight instead of dragging.'),
179 'data-toggle': 'tooltip'
183 .on('click', $.proxy(function (e) {
185 this.toggleColumns();
187 .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
190 $table.before($button);
192 // Initialize the specified columns (for example, weight or parent columns)
193 // to show or hide according to user preference. This aids accessibility
194 // so that, e.g., screen reader users can choose to enter weight values and
195 // manipulate form elements directly, rather than using drag-and-drop..
198 // Add event bindings to the document. The self variable is passed along
199 // as event handlers do not have direct access to the tableDrag object.
200 $(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
201 $(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
202 $(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); });
203 $(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); });
205 // React to localStorage event showing or hiding weight columns.
206 $(window).on('storage', $.proxy(function (e) {
207 // Only react to 'Drupal.tableDrag.showWeight' value change.
208 if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
209 // This was changed in another window, get the new value for this
211 showWeight = JSON.parse(e.originalEvent.newValue);
212 this.displayColumns(showWeight);
217 // Restore the original prototype.
218 Drupal.tableDrag.prototype = prototype;
221 * Take an item and add event handlers to make it become draggable.
223 * @param {HTMLElement} item
225 Drupal.tableDrag.prototype.makeDraggable = function (item) {
229 // Add a class to the title link
230 $item.find('td:first-of-type').find('a').addClass('menu-item__link');
232 // Create the handle.
233 var handle = $('<a href="#" class="tabledrag-handle"/>');
235 // Insert the handle after indentations (if any).
236 var $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1);
237 if ($indentationLast.length) {
238 $indentationLast.after(handle);
239 // Update the total width of indentation in this entire table.
240 self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount);
243 $item.find('td').eq(0).prepend(handle);
246 // Add the glyphicon to the handle.
248 .attr('title', Drupal.t('Drag to re-order'))
249 .attr('data-toggle', 'tooltip')
250 .append(Drupal.theme('bootstrapIcon', 'move'))
253 handle.on('mousedown touchstart pointerdown', function (event) {
254 event.preventDefault();
255 if (event.originalEvent.type === 'touchstart') {
256 event = event.originalEvent.touches[0];
258 self.dragStart(event, self, item);
261 // Prevent the anchor tag from jumping us to the top of the page.
262 handle.on('click', function (e) {
266 // Set blur cleanup when a handle is focused.
267 handle.on('focus', function () {
268 self.safeBlur = true;
271 // On blur, fire the same function as a touchend/mouseup. This is used to
272 // update values after a row has been moved through the keyboard support.
273 handle.on('blur', function (event) {
274 if (self.rowObject && self.safeBlur) {
275 self.dropRow(event, self);
279 // Add arrow-key support to the handle.
280 handle.on('keydown', function (event) {
281 // If a rowObject doesn't yet exist and this isn't the tab key.
282 if (event.keyCode !== 9 && !self.rowObject) {
283 self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
286 var keyChange = false;
288 switch (event.keyCode) {
291 // Safari left arrow.
294 self.rowObject.indent(-1 * self.rtl);
301 var $previousRow = $(self.rowObject.element).prev('tr:first-of-type');
302 var previousRow = $previousRow.get(0);
303 while (previousRow && $previousRow.is(':hidden')) {
304 $previousRow = $(previousRow).prev('tr:first-of-type');
305 previousRow = $previousRow.get(0);
308 // Do not allow the onBlur cleanup.
309 self.safeBlur = false;
310 self.rowObject.direction = 'up';
313 if ($(item).is('.tabledrag-root')) {
314 // Swap with the previous top-level row.
316 while (previousRow && $previousRow.find('.js-indentation').length) {
317 $previousRow = $(previousRow).prev('tr:first-of-type');
318 previousRow = $previousRow.get(0);
319 groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight;
322 self.rowObject.swap('before', previousRow);
323 // No need to check for indentation, 0 is the only valid one.
324 window.scrollBy(0, -groupHeight);
327 else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) {
328 // Swap with the previous row (unless previous row is the first
329 // one and undraggable).
330 self.rowObject.swap('before', previousRow);
331 self.rowObject.interval = null;
332 self.rowObject.indent(0);
333 window.scrollBy(0, -parseInt(item.offsetHeight, 10));
335 // Regain focus after the DOM manipulation.
336 handle.trigger('focus');
342 // Safari right arrow.
345 self.rowObject.indent(self.rtl);
350 // Safari down arrow.
352 var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type');
353 var nextRow = $nextRow.get(0);
354 while (nextRow && $nextRow.is(':hidden')) {
355 $nextRow = $(nextRow).next('tr:first-of-type');
356 nextRow = $nextRow.get(0);
359 // Do not allow the onBlur cleanup.
360 self.safeBlur = false;
361 self.rowObject.direction = 'down';
364 if ($(item).is('.tabledrag-root')) {
365 // Swap with the next group (necessarily a top-level one).
367 var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
369 $(nextGroup.group).each(function () {
370 groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
372 var nextGroupRow = $(nextGroup.group).eq(-1).get(0);
373 self.rowObject.swap('after', nextGroupRow);
374 // No need to check for indentation, 0 is the only valid one.
375 window.scrollBy(0, parseInt(groupHeight, 10));
379 // Swap with the next row.
380 self.rowObject.swap('after', nextRow);
381 self.rowObject.interval = null;
382 self.rowObject.indent(0);
383 window.scrollBy(0, parseInt(item.offsetHeight, 10));
385 // Regain focus after the DOM manipulation.
386 handle.trigger('focus');
391 if (self.rowObject && self.rowObject.changed === true) {
392 $(item).addClass('drag');
393 if (self.oldRowElement) {
394 $(self.oldRowElement).removeClass('drag-previous');
396 self.oldRowElement = item;
397 if (self.striping === true) {
398 self.restripeTable();
403 // Returning false if we have an arrow key to prevent scrolling.
409 // Compatibility addition, return false on keypress to prevent unwanted
410 // scrolling. IE and Safari will suppress scrolling on keydown, but all
411 // other browsers need to return false on keypress.
412 // http://www.quirksmode.org/js/keys.html
413 handle.on('keypress', function (event) {
414 switch (event.keyCode) {
429 * Add an asterisk or other marker to the changed row.
431 Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
432 var $cell = $('td:first', this.element);
433 // Find the first appropriate place to insert the marker.
434 var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0));
435 if (!$cell.find('.tabledrag-changed').length) {
436 $target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' ');
440 $.extend(Drupal.theme, /** @lends Drupal.theme */{
445 tableDragChangedMarker: function () {
446 return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']});
452 tableDragChangedWarning: function () {
453 return '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';