package.src.vaadin-grid-column-reordering-mixin.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grid Show documentation
Show all versions of grid Show documentation
A free, flexible and high-quality Web Component for showing large amounts of tabular data
/**
* @license
* Copyright (c) 2016 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
import { addListener } from '@vaadin/component-base/src/gestures.js';
import { iterateChildren, updateColumnOrders } from './vaadin-grid-helpers.js';
/**
* @polymerMixin
*/
export const ColumnReorderingMixin = (superClass) =>
class ColumnReorderingMixin extends superClass {
static get properties() {
return {
/**
* Set to true to allow column reordering.
* @attr {boolean} column-reordering-allowed
* @type {boolean}
*/
columnReorderingAllowed: {
type: Boolean,
value: false,
},
/** @private */
_orderBaseScope: {
type: Number,
value: 10000000,
},
};
}
static get observers() {
return ['_updateOrders(_columnTree)'];
}
/** @protected */
ready() {
super.ready();
addListener(this, 'track', this._onTrackEvent);
this._reorderGhost = this.shadowRoot.querySelector('[part="reorder-ghost"]');
this.addEventListener('touchstart', this._onTouchStart.bind(this));
this.addEventListener('touchmove', this._onTouchMove.bind(this));
this.addEventListener('touchend', this._onTouchEnd.bind(this));
this.addEventListener('contextmenu', this._onContextMenu.bind(this));
}
/** @private */
_onContextMenu(e) {
if (this.hasAttribute('reordering')) {
e.preventDefault();
// A contextmenu event is fired on mobile Chrome on long-press
// (which should start reordering). Don't end the reorder on touch devices.
if (!isTouch) {
// Context menu cancels the track gesture on desktop without firing an end event.
// End the reorder manually.
this._onTrackEnd();
}
}
}
/** @private */
_onTouchStart(e) {
// Touch event, delay activation by 100ms
this._startTouchReorderTimeout = setTimeout(() => {
this._onTrackStart({
detail: {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
},
});
}, 100);
}
/** @private */
_onTouchMove(e) {
if (this._draggedColumn) {
e.preventDefault();
}
clearTimeout(this._startTouchReorderTimeout);
}
/** @private */
_onTouchEnd() {
clearTimeout(this._startTouchReorderTimeout);
this._onTrackEnd();
}
/** @private */
_onTrackEvent(e) {
if (e.detail.state === 'start') {
const path = e.composedPath();
const headerCell = path[path.indexOf(this.$.header) - 2];
if (!headerCell || !headerCell._content) {
// Not a header column
return;
}
if (headerCell._content.contains(this.getRootNode().activeElement)) {
// Something was focused inside the cell
return;
}
if (this.$.scroller.hasAttribute('column-resizing')) {
// Resizing is in progress
return;
}
if (!this._touchDevice) {
// Not a touch device
this._onTrackStart(e);
}
} else if (e.detail.state === 'track') {
this._onTrack(e);
} else if (e.detail.state === 'end') {
this._onTrackEnd(e);
}
}
/** @private */
_onTrackStart(e) {
if (!this.columnReorderingAllowed) {
return;
}
// Cancel reordering if there are draggable nodes on the event path
const path = e.composedPath && e.composedPath();
if (path && path.some((node) => node.hasAttribute && node.hasAttribute('draggable'))) {
return;
}
const headerCell = this._cellFromPoint(e.detail.x, e.detail.y);
if (!headerCell || !headerCell.getAttribute('part').includes('header-cell')) {
return;
}
this.toggleAttribute('reordering', true);
this._draggedColumn = headerCell._column;
while (this._draggedColumn.parentElement.childElementCount === 1) {
// This is the only column in the group, drag the whole group instead
this._draggedColumn = this._draggedColumn.parentElement;
}
this._setSiblingsReorderStatus(this._draggedColumn, 'allowed');
this._draggedColumn._reorderStatus = 'dragging';
this._updateGhost(headerCell);
this._reorderGhost.style.visibility = 'visible';
this._updateGhostPosition(e.detail.x, this._touchDevice ? e.detail.y - 50 : e.detail.y);
this._autoScroller();
}
/** @private */
_onTrack(e) {
if (!this._draggedColumn) {
// Reordering didn't start. Skip this event.
return;
}
const targetCell = this._cellFromPoint(e.detail.x, e.detail.y);
if (!targetCell) {
return;
}
const targetColumn = this._getTargetColumn(targetCell, this._draggedColumn);
if (
this._isSwapAllowed(this._draggedColumn, targetColumn) &&
this._isSwappableByPosition(targetColumn, e.detail.x)
) {
// Get the header level of the target column (and the dragged column)
const columnTreeLevel = this._columnTree.findIndex((level) => level.includes(targetColumn));
// Get the columns on that level in visual order
const levelColumnsInOrder = this._getColumnsInOrder(columnTreeLevel);
// Index of the column being dragged
const startIndex = levelColumnsInOrder.indexOf(this._draggedColumn);
// Index of the column being dragged over
const endIndex = levelColumnsInOrder.indexOf(targetColumn);
// Direction of iteration
const direction = startIndex < endIndex ? 1 : -1;
// Iteratively swap all the columns from the dragged column to the target column
for (let i = startIndex; i !== endIndex; i += direction) {
this._swapColumnOrders(this._draggedColumn, levelColumnsInOrder[i + direction]);
}
}
this._updateGhostPosition(e.detail.x, this._touchDevice ? e.detail.y - 50 : e.detail.y);
this._lastDragClientX = e.detail.x;
}
/** @private */
_onTrackEnd() {
if (!this._draggedColumn) {
// Reordering didn't start. Skip this event.
return;
}
this.toggleAttribute('reordering', false);
this._draggedColumn._reorderStatus = '';
this._setSiblingsReorderStatus(this._draggedColumn, '');
this._draggedColumn = null;
this._lastDragClientX = null;
this._reorderGhost.style.visibility = 'hidden';
this.dispatchEvent(
new CustomEvent('column-reorder', {
detail: {
columns: this._getColumnsInOrder(),
},
}),
);
}
/**
* Returns the columns (or column groups) on the specified header level in visual order.
* By default, the bottom level is used.
*
* @return {!Array}
* @protected
*/
_getColumnsInOrder(headerLevel = this._columnTree.length - 1) {
return this._columnTree[headerLevel].filter((c) => !c.hidden).sort((b, a) => b._order - a._order);
}
/**
* @param {number} x
* @param {number} y
* @return {HTMLElement | undefined}
* @protected
*/
_cellFromPoint(x = 0, y = 0) {
if (!this._draggedColumn) {
this.$.scroller.toggleAttribute('no-content-pointer-events', true);
}
const elementFromPoint = this.shadowRoot.elementFromPoint(x, y);
this.$.scroller.toggleAttribute('no-content-pointer-events', false);
return this._getCellFromElement(elementFromPoint);
}
/** @private */
_getCellFromElement(element) {
if (element) {
// Check if element is a cell
if (element._column) {
return element;
}
// Check if element is the cell of a focus button mode column
const { parentElement } = element;
if (parentElement && parentElement._focusButton === element) {
return parentElement;
}
}
return null;
}
/**
* @param {number} eventClientX
* @param {number} eventClientY
* @protected
*/
_updateGhostPosition(eventClientX, eventClientY) {
const ghostRect = this._reorderGhost.getBoundingClientRect();
// // This is where we want to position the ghost
const targetLeft = eventClientX - ghostRect.width / 2;
const targetTop = eventClientY - ghostRect.height / 2;
// Current position
const _left = parseInt(this._reorderGhost._left || 0);
const _top = parseInt(this._reorderGhost._top || 0);
// Reposition the ghost
this._reorderGhost._left = _left - (ghostRect.left - targetLeft);
this._reorderGhost._top = _top - (ghostRect.top - targetTop);
this._reorderGhost.style.transform = `translate(${this._reorderGhost._left}px, ${this._reorderGhost._top}px)`;
}
/**
* @param {!HTMLElement} cell
* @return {!HTMLElement}
* @protected
*/
_updateGhost(cell) {
const ghost = this._reorderGhost;
ghost.textContent = cell._content.innerText;
const style = window.getComputedStyle(cell);
[
'boxSizing',
'display',
'width',
'height',
'background',
'alignItems',
'padding',
'border',
'flex-direction',
'overflow',
].forEach((propertyName) => {
ghost.style[propertyName] = style[propertyName];
});
return ghost;
}
/** @private */
_updateOrders(columnTree) {
if (columnTree === undefined) {
return;
}
// Reset all column orders
columnTree[0].forEach((column) => {
column._order = 0;
});
// Set order numbers to top-level columns
updateColumnOrders(columnTree[0], this._orderBaseScope, 0);
}
/**
* @param {!GridColumn} column
* @param {string} status
* @protected
*/
_setSiblingsReorderStatus(column, status) {
iterateChildren(column.parentNode, (sibling) => {
if (/column/u.test(sibling.localName) && this._isSwapAllowed(sibling, column)) {
sibling._reorderStatus = status;
}
});
}
/** @protected */
_autoScroller() {
if (this._lastDragClientX) {
const rightDiff = this._lastDragClientX - this.getBoundingClientRect().right + 50;
const leftDiff = this.getBoundingClientRect().left - this._lastDragClientX + 50;
if (rightDiff > 0) {
this.$.table.scrollLeft += rightDiff / 10;
} else if (leftDiff > 0) {
this.$.table.scrollLeft -= leftDiff / 10;
}
}
if (this._draggedColumn) {
setTimeout(() => this._autoScroller(), 10);
}
}
/**
* @param {GridColumn | undefined} column1
* @param {GridColumn | undefined} column2
* @return {boolean | undefined}
* @protected
*/
_isSwapAllowed(column1, column2) {
if (column1 && column2) {
const differentColumns = column1 !== column2;
const sameParent = column1.parentElement === column2.parentElement;
const sameFrozen =
(column1.frozen && column2.frozen) || // Both columns are frozen
(column1.frozenToEnd && column2.frozenToEnd) || // Both columns are frozen to end
(!column1.frozen && !column1.frozenToEnd && !column2.frozen && !column2.frozenToEnd);
return differentColumns && sameParent && sameFrozen;
}
}
/**
* @param {!GridColumn} targetColumn
* @param {number} clientX
* @return {boolean}
* @protected
*/
_isSwappableByPosition(targetColumn, clientX) {
const targetCell = Array.from(this.$.header.querySelectorAll('tr:not([hidden]) [part~="cell"]')).find((cell) =>
targetColumn.contains(cell._column),
);
const sourceCellRect = this.$.header
.querySelector('tr:not([hidden]) [reorder-status=dragging]')
.getBoundingClientRect();
const targetRect = targetCell.getBoundingClientRect();
if (targetRect.left > sourceCellRect.left) {
return clientX > targetRect.right - sourceCellRect.width;
}
return clientX < targetRect.left + sourceCellRect.width;
}
/**
* @param {!GridColumn} column1
* @param {!GridColumn} column2
* @protected
*/
_swapColumnOrders(column1, column2) {
[column1._order, column2._order] = [column2._order, column1._order];
this._debounceUpdateFrozenColumn();
this._updateFirstAndLastColumn();
}
/**
* @param {HTMLElement | undefined} targetCell
* @param {GridColumn} draggedColumn
* @return {GridColumn | undefined}
* @protected
*/
_getTargetColumn(targetCell, draggedColumn) {
if (targetCell && draggedColumn) {
let candidate = targetCell._column;
while (candidate.parentElement !== draggedColumn.parentElement && candidate !== this) {
candidate = candidate.parentElement;
}
if (candidate.parentElement === draggedColumn.parentElement) {
return candidate;
}
return targetCell._column;
}
}
/**
* Fired when the columns in the grid are reordered.
*
* @event column-reorder
* @param {Object} detail
* @param {Object} detail.columns the columns in the new order
*/
};