You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
921 lines
24 KiB
921 lines
24 KiB
1 year ago
|
/*
|
||
|
** Zabbix
|
||
|
** Copyright (C) 2001-2023 Zabbix SIA
|
||
|
**
|
||
|
** This program is free software; you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation; either version 2 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program is distributed in the hope that it will be useful,
|
||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
** GNU General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program; if not, write to the Free Software
|
||
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
**/
|
||
|
|
||
|
|
||
|
const ZBX_STYLE_SORTABLE = 'sortable';
|
||
|
const ZBX_STYLE_SORTABLE_LIST = 'sortable-list';
|
||
|
const ZBX_STYLE_SORTABLE_ITEM = 'sortable-item';
|
||
|
const ZBX_STYLE_SORTABLE_DRAG_HANDLE = 'sortable-drag-handle';
|
||
|
const ZBX_STYLE_SORTABLE_DRAGGING = 'sortable-dragging';
|
||
|
|
||
|
const SORTABLE_EVENT_DRAG_START = 'sortable-drag-start';
|
||
|
const SORTABLE_EVENT_DRAG_END = 'sortable-drag-end';
|
||
|
const SORTABLE_EVENT_SORT = 'sortable-sort';
|
||
|
|
||
|
class CSortable extends CBaseComponent {
|
||
|
|
||
|
/**
|
||
|
* Create CSortable instance.
|
||
|
*
|
||
|
* @param {HTMLElement} target
|
||
|
*
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
constructor(target, {
|
||
|
is_vertical,
|
||
|
is_sorting_enabled = true,
|
||
|
drag_scroll_delay_short = 150,
|
||
|
drag_scroll_delay_long = 400,
|
||
|
wheel_step = 100,
|
||
|
show_grabbing_cursor = true,
|
||
|
do_activate = true
|
||
|
}) {
|
||
|
super(target);
|
||
|
|
||
|
this._is_vertical = is_vertical;
|
||
|
this._is_sorting_enabled = is_sorting_enabled;
|
||
|
this._drag_scroll_delay_short = drag_scroll_delay_short;
|
||
|
this._drag_scroll_delay_long = drag_scroll_delay_long;
|
||
|
this._wheel_step = wheel_step;
|
||
|
this._show_grabbing_cursor = show_grabbing_cursor;
|
||
|
|
||
|
this._init();
|
||
|
this._registerEvents();
|
||
|
|
||
|
if (do_activate) {
|
||
|
this.activate();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Activate the interactive functionality.
|
||
|
*
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
activate() {
|
||
|
if (this._is_activated) {
|
||
|
throw Error('Instance already activated.');
|
||
|
}
|
||
|
|
||
|
this._fixListPos();
|
||
|
|
||
|
this._activateEvents();
|
||
|
|
||
|
this._is_activated = true;
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deactivate the interactive functionality.
|
||
|
*
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
deactivate() {
|
||
|
if (!this._is_activated) {
|
||
|
throw Error('Instance already deactivated.');
|
||
|
}
|
||
|
|
||
|
this._cancelDragging();
|
||
|
|
||
|
this._deactivateEvents();
|
||
|
|
||
|
this._is_activated = false;
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Enable or disable the sorting functionality.
|
||
|
*
|
||
|
* @param {boolean} enable
|
||
|
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
enableSorting(enable = true) {
|
||
|
if (this._is_sorting_enabled && !enable) {
|
||
|
this._cancelDragging();
|
||
|
}
|
||
|
|
||
|
this._is_sorting_enabled = enable;
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get list of items.
|
||
|
*
|
||
|
* @returns {HTMLCollection}
|
||
|
*/
|
||
|
getList() {
|
||
|
return this._list;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Is the list scrollable (not all items visible)?
|
||
|
*
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
isScrollable() {
|
||
|
return !this._isPosEqual(this._getListPosMax(), 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Insert item to the list before the reference item or at the end.
|
||
|
*
|
||
|
* @param {HTMLLIElement} item
|
||
|
* @param {HTMLLIElement|null} reference_item
|
||
|
*
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
insertItemBefore(item, reference_item = null) {
|
||
|
item.classList.add(ZBX_STYLE_SORTABLE_ITEM);
|
||
|
item.tabIndex = 0;
|
||
|
|
||
|
this._cancelDragging();
|
||
|
this._list.insertBefore(item, reference_item);
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove item from the list.
|
||
|
*
|
||
|
* @param {HTMLLIElement} item
|
||
|
*
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
removeItem(item) {
|
||
|
if (item.parentNode !== this._list) {
|
||
|
throw RangeError('Item does not belong to the list.');
|
||
|
}
|
||
|
|
||
|
this._cancelDragging();
|
||
|
this._list.removeChild(item);
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Scroll item into view.
|
||
|
*
|
||
|
* @param {HTMLLIElement} item
|
||
|
*
|
||
|
* @returns {CSortable}
|
||
|
*/
|
||
|
scrollItemIntoView(item) {
|
||
|
if (item.parentNode !== this._list) {
|
||
|
throw RangeError('Item does not belong to the list.');
|
||
|
}
|
||
|
|
||
|
const list_loc = this._getRectLoc(this._list.getBoundingClientRect());
|
||
|
const item_loc = this._getRectLoc(item.getBoundingClientRect());
|
||
|
|
||
|
this._scrollIntoView(item_loc.pos - list_loc.pos, item_loc.dim);
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initialize the instance.
|
||
|
*/
|
||
|
_init() {
|
||
|
this._target.classList.add(ZBX_STYLE_SORTABLE);
|
||
|
|
||
|
this._list = this._target.querySelector(`.${ZBX_STYLE_SORTABLE_LIST}`);
|
||
|
|
||
|
if (this._list === null) {
|
||
|
this._list = document.createElement('ul');
|
||
|
this._target.appendChild(this._list);
|
||
|
}
|
||
|
|
||
|
this._list.classList.add(ZBX_STYLE_SORTABLE_LIST);
|
||
|
|
||
|
this._list_pos = -parseFloat(getComputedStyle(this._list).getPropertyValue(
|
||
|
this._is_vertical ? 'top' : 'left'
|
||
|
));
|
||
|
|
||
|
this._drag_item = null;
|
||
|
this._drag_scroll_timeout = null;
|
||
|
|
||
|
this._is_activated = false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start dragging the item.
|
||
|
*
|
||
|
* @param {HTMLLIElement} drag_item Dragged item.
|
||
|
* @param {number} pos Starting axis position.
|
||
|
*/
|
||
|
_startDragging(drag_item, pos) {
|
||
|
this._drag_item_index_original = [...this._list.children].indexOf(drag_item);
|
||
|
|
||
|
this._drag_item_index = this._drag_item_index_original;
|
||
|
|
||
|
const target_rect = this._target.getBoundingClientRect();
|
||
|
const target_loc = this._getRectLoc(target_rect);
|
||
|
|
||
|
const list_rect = this._list.getBoundingClientRect();
|
||
|
const list_loc = this._getRectLoc(list_rect);
|
||
|
|
||
|
const drag_item_rect = drag_item.getBoundingClientRect();
|
||
|
const drag_item_loc = this._getRectLoc(drag_item_rect);
|
||
|
|
||
|
this._drag_item_loc = {
|
||
|
pos: drag_item_loc.pos - target_loc.pos,
|
||
|
dim: drag_item_loc.dim
|
||
|
};
|
||
|
|
||
|
this._drag_item_event_delta_pos = this._drag_item_loc.pos - pos;
|
||
|
|
||
|
this._item_loc = [];
|
||
|
|
||
|
for (const item of this._list.children) {
|
||
|
if (item === drag_item) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const item_rect = item.getBoundingClientRect();
|
||
|
const item_loc = this._getRectLoc(item_rect);
|
||
|
|
||
|
this._item_loc.push({
|
||
|
pos: item_loc.pos - list_loc.pos,
|
||
|
dim: item_loc.dim
|
||
|
});
|
||
|
|
||
|
item.style.left = `${item_rect.x - list_rect.x}px`;
|
||
|
item.style.top = `${item_rect.y - list_rect.y}px`;
|
||
|
item.style.width = `${item_rect.width}px`;
|
||
|
item.style.height = `${item_rect.height}px`;
|
||
|
}
|
||
|
|
||
|
this._target.classList.add(ZBX_STYLE_SORTABLE_DRAGGING);
|
||
|
this._list.style.width = `${list_rect.width}px`;
|
||
|
this._list.style.height = `${list_rect.height}px`;
|
||
|
|
||
|
// Clone the dragging item not to disturb the original order while dragging.
|
||
|
this._drag_item = drag_item;
|
||
|
this._drag_item.style.left = `${drag_item_rect.x - target_rect.x}px`;
|
||
|
this._drag_item.style.top = `${drag_item_rect.y - target_rect.y}px`;
|
||
|
this._drag_item.style.width = `${drag_item_rect.width}px`;
|
||
|
this._drag_item.style.height = `${drag_item_rect.height}px`;
|
||
|
|
||
|
this._target.appendChild(this._drag_item);
|
||
|
|
||
|
// Hide the actual dragging item.
|
||
|
drag_item.classList.add(ZBX_STYLE_SORTABLE_DRAGGING);
|
||
|
|
||
|
// Set mouse cursor to "grabbing".
|
||
|
if (this._show_grabbing_cursor) {
|
||
|
this._dragging_style = document.createElement('style');
|
||
|
document.head.appendChild(this._dragging_style);
|
||
|
this._dragging_style.sheet.insertRule('* { cursor: grabbing !important; }');
|
||
|
}
|
||
|
|
||
|
this.fire(SORTABLE_EVENT_DRAG_START, {item: drag_item});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Drag the currently dragged item to a new position.
|
||
|
*
|
||
|
* @param {number} pos New axis position.
|
||
|
*/
|
||
|
_drag(pos) {
|
||
|
const items = this._getNonDraggingItems();
|
||
|
|
||
|
const target_rect = this._target.getBoundingClientRect();
|
||
|
const target_loc = this._getRectLoc(target_rect);
|
||
|
|
||
|
const drag_item_rect = this._drag_item.getBoundingClientRect();
|
||
|
const drag_item_loc = this._getRectLoc(drag_item_rect);
|
||
|
|
||
|
const drag_item_max_pos = target_loc.dim - drag_item_loc.dim;
|
||
|
this._drag_item_loc.pos = Math.max(0, Math.min(drag_item_max_pos, pos + this._drag_item_event_delta_pos));
|
||
|
this._drag_item.style[this._is_vertical ? 'top' : 'left'] = `${this._drag_item_loc.pos}px`;
|
||
|
|
||
|
const center_pos = this._list_pos + this._drag_item_loc.pos + this._drag_item_loc.dim / 2;
|
||
|
|
||
|
for (let index = this._drag_item_index - 1; index >= 0; index--) {
|
||
|
if (center_pos >= this._item_loc[index].pos + (this._item_loc[index].dim + this._drag_item_loc.dim) / 2) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
this._drag_item_index--;
|
||
|
this._item_loc[index].pos += this._drag_item_loc.dim;
|
||
|
items[index].style[this._is_vertical ? 'top' : 'left'] = `${this._item_loc[index].pos}px`;
|
||
|
}
|
||
|
|
||
|
for (let index = this._drag_item_index; index < items.length; index++) {
|
||
|
if (center_pos <= this._item_loc[index].pos + (this._item_loc[index].dim - this._drag_item_loc.dim) / 2) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
this._drag_item_index++;
|
||
|
this._item_loc[index].pos -= this._drag_item_loc.dim;
|
||
|
items[index].style[this._is_vertical ? 'top' : 'left'] = `${this._item_loc[index].pos}px`;
|
||
|
}
|
||
|
|
||
|
if (this._drag_item_loc.pos === 0) {
|
||
|
this._startDragScrolling(-1);
|
||
|
}
|
||
|
else if (this._drag_item_loc.pos === drag_item_max_pos) {
|
||
|
this._startDragScrolling(1);
|
||
|
}
|
||
|
else {
|
||
|
this._endDragScrolling();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* End dragging the item.
|
||
|
*/
|
||
|
_endDragging() {
|
||
|
this._endDragScrolling();
|
||
|
|
||
|
const drag_item_pos = (this._drag_item_index > 0)
|
||
|
? this._item_loc[this._drag_item_index - 1].pos + this._item_loc[this._drag_item_index - 1].dim
|
||
|
: 0;
|
||
|
|
||
|
this._scrollIntoView(drag_item_pos, this._drag_item_loc.dim);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* End dragging the item after the positional transitions have ended.
|
||
|
*/
|
||
|
_endDraggingAfterTransitions() {
|
||
|
const items = this._getNonDraggingItems();
|
||
|
|
||
|
const drag_item = this._drag_item;
|
||
|
|
||
|
this._list.insertBefore(drag_item, this._drag_item_index < items.length ? items[this._drag_item_index] : null);
|
||
|
|
||
|
drag_item.classList.remove(ZBX_STYLE_SORTABLE_DRAGGING);
|
||
|
drag_item.style.left = '';
|
||
|
drag_item.style.top = '';
|
||
|
drag_item.style.width = '';
|
||
|
drag_item.style.height = '';
|
||
|
|
||
|
this._target.classList.remove(ZBX_STYLE_SORTABLE_DRAGGING);
|
||
|
this._list.style.width = '';
|
||
|
this._list.style.height = '';
|
||
|
|
||
|
for (const item of items) {
|
||
|
item.style.left = '';
|
||
|
item.style.top = '';
|
||
|
item.style.width = '';
|
||
|
item.style.height = '';
|
||
|
}
|
||
|
|
||
|
// Re-focus the dragged item.
|
||
|
drag_item.focus();
|
||
|
|
||
|
this._drag_item = null;
|
||
|
|
||
|
// Reset mouse cursor.
|
||
|
if (this._show_grabbing_cursor) {
|
||
|
this._dragging_style.remove();
|
||
|
}
|
||
|
|
||
|
this.fire(SORTABLE_EVENT_DRAG_END, {item: drag_item});
|
||
|
|
||
|
if (this._drag_item_index !== this._drag_item_index_original) {
|
||
|
this.fire(SORTABLE_EVENT_SORT);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start list scrolling iteratively when the item is dragged to the beginning or to the end of the list.
|
||
|
*
|
||
|
* @param {number} direction Either 1 or -1 for scrolling forward or backward respectively.
|
||
|
*/
|
||
|
_startDragScrolling(direction) {
|
||
|
if (this._drag_scroll_timeout === null) {
|
||
|
this._drag_scroll_tick = 0;
|
||
|
this._drag_scroll_timeout = setTimeout(() => {
|
||
|
this._dragScroll(direction);
|
||
|
}, this._getDragScrollDelay(0));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Scroll the list by one item when the item is dragged to the beginning or to the end of the list.
|
||
|
*
|
||
|
* @param {number} direction Either 1 or -1 for scrolling forward or backward respectively.
|
||
|
*/
|
||
|
_dragScroll(direction) {
|
||
|
const items = this._getNonDraggingItems();
|
||
|
|
||
|
const prev_item_pos = (this._drag_item_index > 0) ? this._item_loc[this._drag_item_index - 1].pos : 0;
|
||
|
const drag_item_pos = (this._drag_item_index > 0)
|
||
|
? prev_item_pos + this._item_loc[this._drag_item_index - 1].dim
|
||
|
: 0;
|
||
|
|
||
|
if (direction === -1) {
|
||
|
if (this._drag_item_index > 0) {
|
||
|
this._drag_item_index--;
|
||
|
|
||
|
this._setListPos(prev_item_pos);
|
||
|
|
||
|
this._item_loc[this._drag_item_index].pos += this._drag_item_loc.dim;
|
||
|
items[this._drag_item_index].style[this._is_vertical ? 'top' : 'left'] =
|
||
|
`${this._item_loc[this._drag_item_index].pos}px`;
|
||
|
}
|
||
|
else {
|
||
|
this._scrollIntoView(drag_item_pos, this._drag_item_loc.dim);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
const next_item_pos = (this._drag_item_index < items.length)
|
||
|
? this._item_loc[this._drag_item_index].pos
|
||
|
: drag_item_pos + this._drag_item_loc.dim;
|
||
|
|
||
|
const next_next_item_pos = (this._drag_item_index < items.length - 1)
|
||
|
? this._item_loc[this._drag_item_index + 1].pos
|
||
|
: next_item_pos + (
|
||
|
(this._drag_item_index < items.length) ? this._item_loc[this._drag_item_index].dim : 0
|
||
|
);
|
||
|
|
||
|
if (this._drag_item_index < items.length) {
|
||
|
const list_loc = this._getRectLoc(this._list.getBoundingClientRect());
|
||
|
|
||
|
this._setListPos(next_next_item_pos - list_loc.dim);
|
||
|
|
||
|
this._item_loc[this._drag_item_index].pos -= this._drag_item_loc.dim;
|
||
|
items[this._drag_item_index].style[this._is_vertical ? 'top' : 'left'] =
|
||
|
`${this._item_loc[this._drag_item_index].pos}px`;
|
||
|
|
||
|
this._drag_item_index++;
|
||
|
}
|
||
|
else {
|
||
|
this._scrollIntoView(drag_item_pos, this._drag_item_loc.dim);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this._drag_scroll_timeout = setTimeout(() => this._dragScroll(direction),
|
||
|
this._getDragScrollDelay(++this._drag_scroll_tick)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* End list scrolling iteratively when the item is dragged to the beginning or to the end of the list.
|
||
|
*/
|
||
|
_endDragScrolling() {
|
||
|
if (this._drag_scroll_timeout !== null) {
|
||
|
clearTimeout(this._drag_scroll_timeout);
|
||
|
this._drag_scroll_timeout = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the delay for a sequent list scrolling when the item is dragged to the beginning or to the end of the list.
|
||
|
*
|
||
|
* @param {number} iteration Zero-based list scrolling iteration.
|
||
|
*
|
||
|
* @returns {number}
|
||
|
*/
|
||
|
_getDragScrollDelay(iteration) {
|
||
|
return (iteration === 0 || iteration > 2) ? this._drag_scroll_delay_short : this._drag_scroll_delay_long;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cancel item dragging and return the item to its original position.
|
||
|
*/
|
||
|
_cancelDragging() {
|
||
|
if (this._drag_item !== null) {
|
||
|
// Simulate dropping the item at its original position.
|
||
|
|
||
|
this._drag_item_index = this._drag_item_index_original;
|
||
|
|
||
|
this.fire('_dragcancel');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Scroll the list by mouse wheel in the given direction.
|
||
|
*
|
||
|
* @param {number} direction Either 1 or -1 for scrolling forward or backward respectively.
|
||
|
* @param {number} pos Mouse axis position.
|
||
|
*/
|
||
|
_wheel(direction, pos) {
|
||
|
// Prevent using wheel while scrolling by dragging.
|
||
|
if (this._drag_scroll_timeout !== null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._setListPos(Math.max(0, Math.min(this._getListPosMax(), this._list_pos + this._wheel_step * direction)));
|
||
|
|
||
|
if (this._drag_item !== null) {
|
||
|
this._drag(pos);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Scroll the list as little as possible to fully contain the object with the given position and dimension.
|
||
|
*
|
||
|
* @param {number} pos Object position in decimal pixels.
|
||
|
* @param {number} dim Object dimension in decimal pixels.
|
||
|
*/
|
||
|
_scrollIntoView(pos, dim) {
|
||
|
if (pos < this._list_pos) {
|
||
|
this._setListPos(pos);
|
||
|
}
|
||
|
else {
|
||
|
const list_loc = this._getRectLoc(this._list.getBoundingClientRect());
|
||
|
|
||
|
if (pos + dim > this._list_pos + list_loc.dim) {
|
||
|
this._setListPos(pos + dim - list_loc.dim);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Scroll the list to the given position.
|
||
|
*
|
||
|
* @param {number} pos Position in decimal pixels.
|
||
|
*/
|
||
|
_setListPos(pos) {
|
||
|
if (this._isPosEqual(pos, this._list_pos)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._list_pos = pos;
|
||
|
this._list.style[this._is_vertical ? 'top' : 'left'] = `-${pos}px`;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Fix the list scroll position (on list resize).
|
||
|
*/
|
||
|
_fixListPos() {
|
||
|
const list_pos_max = this._getListPosMax();
|
||
|
|
||
|
if (this._list_pos > list_pos_max) {
|
||
|
this._setListPos(list_pos_max);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the maximum scroll position of the list.
|
||
|
*
|
||
|
* @returns {number} Position in decimal pixels.
|
||
|
*/
|
||
|
_getListPosMax() {
|
||
|
const items = this._getNonDraggingItems();
|
||
|
|
||
|
const list_loc = this._getRectLoc(this._list.getBoundingClientRect());
|
||
|
|
||
|
if (this._drag_item === null) {
|
||
|
if (items.length === 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
const last_item_loc = this._getRectLoc(items[items.length - 1].getBoundingClientRect());
|
||
|
|
||
|
return Math.max(0, last_item_loc.pos + last_item_loc.dim - list_loc.pos - list_loc.dim);
|
||
|
}
|
||
|
else {
|
||
|
if (items.length === 0) {
|
||
|
return Math.max(0, this._drag_item_loc.dim - list_loc.dim);
|
||
|
}
|
||
|
|
||
|
const scroll_dim = (this._drag_item_index < items.length)
|
||
|
? this._item_loc[items.length - 1].pos + this._item_loc[items.length - 1].dim
|
||
|
: this._item_loc[items.length - 1].pos + this._item_loc[items.length - 1].dim + this._drag_item_loc.dim;
|
||
|
|
||
|
return Math.max(0, scroll_dim - list_loc.dim);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all list items except the one being dragged.
|
||
|
*
|
||
|
* @returns {HTMLElement[]}
|
||
|
*/
|
||
|
_getNonDraggingItems() {
|
||
|
return [...this._list.children].filter((item) => !item.classList.contains(ZBX_STYLE_SORTABLE_DRAGGING));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the position and dimension of the DOMRect, based on the current instance orientation.
|
||
|
*
|
||
|
* @param {DOMRect} rect
|
||
|
*
|
||
|
* @returns {Object}
|
||
|
*/
|
||
|
_getRectLoc(rect) {
|
||
|
return (this._is_vertical
|
||
|
? {pos: rect.top, dim: rect.height}
|
||
|
: {pos: rect.left, dim: rect.width}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if decimal positions are equal by dismissing floating-point calculation errors.
|
||
|
*
|
||
|
* @param {number} pos_1 Decimal position.
|
||
|
* @param {number} pos_2 Decimal position.
|
||
|
*
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
_isPosEqual(pos_1, pos_2) {
|
||
|
return (Math.abs(pos_1 - pos_2) < 0.001);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register all DOM events.
|
||
|
*/
|
||
|
_registerEvents() {
|
||
|
let prevent_clicks;
|
||
|
let mouse_down_item;
|
||
|
let mouse_down_pos;
|
||
|
let mouse_move_request;
|
||
|
let mouse_move_pos;
|
||
|
let wheel_request;
|
||
|
let wheel_direction;
|
||
|
let wheel_pos;
|
||
|
let end_dragging_after_transitions;
|
||
|
let transitions_set;
|
||
|
let list_resize_observer;
|
||
|
|
||
|
this._events = {
|
||
|
targetClick: (e) => {
|
||
|
if (prevent_clicks) {
|
||
|
e.preventDefault();
|
||
|
e.stopImmediatePropagation();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
targetScroll: () => {
|
||
|
// Prevent browsers from automatically scrolling focusable elements into view.
|
||
|
this._target[this._is_vertical ? 'scrollTop' : 'scrollLeft'] = 0;
|
||
|
},
|
||
|
|
||
|
wheel: (e) => {
|
||
|
if (mouse_down_item !== null) {
|
||
|
this._startDragging(mouse_down_item, mouse_down_pos);
|
||
|
|
||
|
mouse_down_item = null;
|
||
|
|
||
|
// Prevent clicks after dragging has ended.
|
||
|
prevent_clicks = true;
|
||
|
}
|
||
|
|
||
|
if (this._drag_item !== null) {
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
|
||
|
wheel_direction = (e.deltaY !== 0) ? Math.sign(e.deltaY) : Math.sign(e.deltaX);
|
||
|
wheel_pos = this._is_vertical ? e.clientY : e.clientX;
|
||
|
|
||
|
if (wheel_request === null) {
|
||
|
wheel_request = requestAnimationFrame(() => {
|
||
|
this._wheel(wheel_direction, wheel_pos);
|
||
|
wheel_request = null;
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
listMouseDown: (e) => {
|
||
|
if (e.button !== 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!this._is_sorting_enabled) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Prevent clicks while transitions are running.
|
||
|
if (transitions_set.size > 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mouse_down_item = e.target.closest(`.${ZBX_STYLE_SORTABLE_ITEM}`);
|
||
|
|
||
|
// Interested in items and not the list itself.
|
||
|
if (mouse_down_item === null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Scroll item into view if it is partially visible.
|
||
|
this.scrollItemIntoView(mouse_down_item);
|
||
|
|
||
|
// Drag handle specified, but clicked elsewhere?
|
||
|
if (mouse_down_item.getElementsByClassName(ZBX_STYLE_SORTABLE_DRAG_HANDLE).length > 0
|
||
|
&& e.target.closest(`.${ZBX_STYLE_SORTABLE_DRAG_HANDLE}`) === null) {
|
||
|
mouse_down_item = null;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Prevent content selection while dragging the item.
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Re-focus the item.
|
||
|
mouse_down_item.focus();
|
||
|
|
||
|
// Save initial mouse position.
|
||
|
mouse_down_pos = this._is_vertical ? e.clientY : e.clientX;
|
||
|
|
||
|
this.off('wheel', this._events.wheel);
|
||
|
window.addEventListener('mousemove', this._events.windowMouseMove);
|
||
|
window.addEventListener('mouseup', this._events.windowMouseUp);
|
||
|
window.addEventListener('wheel', this._events.wheel, {passive: false});
|
||
|
},
|
||
|
|
||
|
windowMouseMove: (e) => {
|
||
|
if (mouse_down_item !== null) {
|
||
|
this._startDragging(mouse_down_item, mouse_down_pos);
|
||
|
|
||
|
mouse_down_item = null;
|
||
|
|
||
|
// Prevent clicks after dragging has ended.
|
||
|
prevent_clicks = true;
|
||
|
}
|
||
|
|
||
|
mouse_move_pos = this._is_vertical ? e.clientY : e.clientX;
|
||
|
|
||
|
if (mouse_move_request === null) {
|
||
|
mouse_move_request = requestAnimationFrame(() => {
|
||
|
this._drag(mouse_move_pos);
|
||
|
mouse_move_request = null;
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
windowMouseUp: () => {
|
||
|
// Was dragging in progress?
|
||
|
if (mouse_down_item === null) {
|
||
|
const prev_list_pos = this._list_pos;
|
||
|
|
||
|
// Will occasionally update this._list_pos and start the transition later.
|
||
|
this._endDragging();
|
||
|
|
||
|
end_dragging_after_transitions = (transitions_set.size > 0
|
||
|
|| !this._isPosEqual(this._list_pos, prev_list_pos));
|
||
|
|
||
|
if (!end_dragging_after_transitions) {
|
||
|
this._endDraggingAfterTransitions();
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
mouse_down_item = null;
|
||
|
}
|
||
|
|
||
|
prevent_clicks = false;
|
||
|
|
||
|
if (mouse_move_request !== null) {
|
||
|
cancelAnimationFrame(mouse_move_request);
|
||
|
mouse_move_request = null;
|
||
|
}
|
||
|
|
||
|
window.removeEventListener('mousemove', this._events.windowMouseMove);
|
||
|
window.removeEventListener('mouseup', this._events.windowMouseUp);
|
||
|
window.removeEventListener('wheel', this._events.wheel);
|
||
|
this.on('wheel', this._events.wheel, {passive: false});
|
||
|
},
|
||
|
|
||
|
listKeyDown: (e) => {
|
||
|
if (!this._is_sorting_enabled) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (e.target.parentNode !== this._list) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ((e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') || !e.ctrlKey) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (e.key === 'ArrowLeft' && e.target.previousElementSibling === null
|
||
|
|| e.key === 'ArrowRight' && e.target.nextElementSibling === null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.insertItemBefore(e.target, e.key === 'ArrowLeft'
|
||
|
? e.target.previousElementSibling
|
||
|
: e.target.nextElementSibling.nextElementSibling
|
||
|
);
|
||
|
|
||
|
e.preventDefault();
|
||
|
|
||
|
// Re-focus the moved item.
|
||
|
e.target.focus();
|
||
|
|
||
|
this.fire(SORTABLE_EVENT_SORT);
|
||
|
},
|
||
|
|
||
|
listFocusIn: (e) => {
|
||
|
const item = e.target.closest(`.${ZBX_STYLE_SORTABLE_ITEM}`);
|
||
|
|
||
|
if (item) {
|
||
|
this.scrollItemIntoView(item);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
listRunTransition: (e) => {
|
||
|
if (e.propertyName === (this._is_vertical ? 'top' : 'left')) {
|
||
|
transitions_set.add(e.target);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
listEndTransition: (e) => {
|
||
|
transitions_set.delete(e.target);
|
||
|
|
||
|
// Delete outdated targets.
|
||
|
for (const target of transitions_set) {
|
||
|
if (target === this._list) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const item = target.closest(`.${ZBX_STYLE_SORTABLE_ITEM}`);
|
||
|
|
||
|
if (item === null || item.parentNode !== this._list) {
|
||
|
transitions_set.delete(target);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (end_dragging_after_transitions && transitions_set.size === 0) {
|
||
|
this._endDraggingAfterTransitions();
|
||
|
end_dragging_after_transitions = false;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
listResize: () => {
|
||
|
this._fixListPos();
|
||
|
},
|
||
|
|
||
|
_cancelDragging: () => {
|
||
|
// Actually dragging an item?
|
||
|
if (prevent_clicks || mouse_down_item !== null) {
|
||
|
this._events.windowMouseUp();
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
|
||
|
this._activateEvents = () => {
|
||
|
prevent_clicks = false;
|
||
|
mouse_down_item = null;
|
||
|
mouse_move_request = null;
|
||
|
wheel_request = null;
|
||
|
end_dragging_after_transitions = false;
|
||
|
transitions_set = new Set();
|
||
|
|
||
|
this.on('click', this._events.targetClick);
|
||
|
this.on('scroll', this._events.targetScroll);
|
||
|
this.on('wheel', this._events.wheel, {passive: false});
|
||
|
this.on('_dragcancel', this._events._cancelDragging);
|
||
|
this._list.addEventListener('mousedown', this._events.listMouseDown);
|
||
|
this._list.addEventListener('keydown', this._events.listKeyDown);
|
||
|
this._list.addEventListener('focusin', this._events.listFocusIn);
|
||
|
this._list.addEventListener('transitionrun', this._events.listRunTransition);
|
||
|
this._list.addEventListener('transitionend', this._events.listEndTransition);
|
||
|
|
||
|
list_resize_observer = new ResizeObserver(this._events.listResize);
|
||
|
list_resize_observer.observe(this._list);
|
||
|
};
|
||
|
|
||
|
this._deactivateEvents = () => {
|
||
|
if (wheel_request !== null) {
|
||
|
cancelAnimationFrame(wheel_request);
|
||
|
}
|
||
|
|
||
|
if (end_dragging_after_transitions) {
|
||
|
this._endDraggingAfterTransitions();
|
||
|
}
|
||
|
|
||
|
this.off('click', this._events.targetClick);
|
||
|
this.off('scroll', this._events.targetScroll);
|
||
|
this.off('wheel', this._events.wheel);
|
||
|
this.off('_dragcancel', this._events._cancelDragging);
|
||
|
this._list.removeEventListener('mousedown', this._events.listMouseDown);
|
||
|
this._list.removeEventListener('keydown', this._events.listKeyDown);
|
||
|
this._list.removeEventListener('focusin', this._events.listFocusIn);
|
||
|
this._list.removeEventListener('transitionrun', this._events.listRunTransition);
|
||
|
this._list.removeEventListener('transitionend', this._events.listEndTransition);
|
||
|
|
||
|
// Added by mousedown event handler.
|
||
|
window.removeEventListener('mousemove', this._events.windowMouseMove);
|
||
|
window.removeEventListener('mouseup', this._events.windowMouseUp);
|
||
|
window.removeEventListener('wheel', this._events.wheel);
|
||
|
|
||
|
list_resize_observer.disconnect();
|
||
|
};
|
||
|
}
|
||
|
}
|