464 lines
14 KiB
JavaScript
464 lines
14 KiB
JavaScript
/**
|
|
* @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 = $('<tr/>').addClass('draggable').appendTo(table);
|
|
var testCell = $('<td/>').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('<div class="tabledrag-toggle-weight-wrapper"></div>')
|
|
.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 = $('<a href="#" class="tabledrag-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 '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|