Add column mover

This commit is contained in:
trading_peter 2024-01-18 16:27:07 +01:00
parent d1e1c75949
commit 7b416bf910
3 changed files with 229 additions and 8 deletions

120
column-mover.js Normal file
View File

@ -0,0 +1,120 @@
import { TpTable } from "./tp-table";
export default class ColumnMover {
constructor(wrap, handleSelector) {
this._handleSelector = handleSelector;
this._mouseDown = this._mouseDown.bind(this);
this._dragging = this._dragging.bind(this);
this._draggingEnd = this._draggingEnd.bind(this);
this._trackingStarted = false;
// Amount of pixels the handle must be moved before its registered as a tracking gesture.
this.threshold = 5;
wrap.addEventListener('mousedown', this._mouseDown, true);
}
_mouseDown(e) {
const handle = e.composedPath().find(node => node.matches && node.matches(this._handleSelector));
if (!handle) return;
e.stopPropagation();
e.preventDefault();
this._cHandle = handle;
this._startX = e.pageX;
this._watchDrag();
}
_watchDrag() {
window.addEventListener('mousemove', this._dragging);
window.addEventListener('mouseup', this._draggingEnd);
}
_stopWatchDrag() {
window.removeEventListener('mousemove', this._dragging);
window.removeEventListener('mouseup', this._draggingEnd);
}
_dragging(e) {
if (this._trackingStarted === false && Math.abs(e.pageX - this._startX) >= this.threshold) {
this._trackingStarted = true;
this._showClone();
this._clearSelection();
}
if (this._trackingStarted === true) {
this._positionClone(e.pageX, e.pageY);
this._cHandle.dispatchEvent(new CustomEvent('track-move', { detail: { target: this._cHandle, state: 'track', x: e.pageX, y: e.pageX }, bubbles: true, composed: true }));
}
}
_draggingEnd(e) {
this._trackingStarted = false;
this._cHandle.dispatchEvent(new CustomEvent('track-move', { detail: { target: this._cHandle, state: 'end', x: e.pageX, y: e.pageX }, bubbles: true, composed: true }));
this._stopWatchDrag();
this._cHandle = null;
this._startX = 0;
this._clearSelection();
this._clone.remove();
this._clone = null;
if (this._indicator) {
this._indicator.remove();
this._indicator = null;
}
}
_clearSelection() {
const sel = window.getSelection ? window.getSelection() : document.selection;
if (sel) {
if (sel.removeAllRanges) {
sel.removeAllRanges();
} else if (sel.empty) {
sel.empty();
}
}
}
/**
* Show indicator to visualize where a column would be dropped after releasing the mouse.
* @param {number} x X-coordinate
* @param {number} y Y-coordinate
*/
showIndicator(x, y) {
if (!this._indicator) {
const icon = document.createElement('tp-icon');
icon.icon = TpTable.downIcon;
icon.style.position = 'fixed';
icon.style.left = '0px';
icon.style.right = '0px';
icon.style.zIndex = '100000';
document.body.appendChild(icon);
this._indicator = icon;
}
const rect = this._indicator.getBoundingClientRect();
this._indicator.style.transform = `translate(${x - ((rect.right - rect.left) / 2)}px, ${y - rect.height}px)`;
}
_showClone() {
const div = document.createElement('div');
div.style.background = 'rgba(255, 255, 255, 0.6)';
div.style.border = 'solid 1px rgba(255, 255, 255, 0.8)';
div.style.padding = '5px 10px';
div.style.borderRadius = '2px';
div.style.position = 'fixed';
div.style.zIndex = '99999';
div.style.left = '0px';
div.style.top = '0px';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.innerHTML = this._cHandle.column.label;
this._clone = div;
document.body.appendChild(div);
}
_positionClone(x, y) {
this._clone.style.transform = `translate(${x + 10}px, ${y}px)`;
}
}

View File

@ -1,4 +1,4 @@
export default class ColumResizer {
export default class ColumnResizer {
constructor(wrap, handleSelector) {
this._handleSelector = handleSelector;
this._mouseDown = this._mouseDown.bind(this);

View File

@ -12,7 +12,8 @@ import './tp-table-item.js';
import { DomQuery } from '@tp/helpers/dom-query.js';
import { closest } from '@tp/helpers/closest.js';
import { LitElement, html, css, svg } from 'lit';
import ColumResizer from './column-resizer.js';
import ColumnResizer from './column-resizer.js';
import ColumnMover from './column-mover.js';
export class TpTable extends DomQuery(LitElement) {
static get styles() {
@ -195,7 +196,7 @@ export class TpTable extends DomQuery(LitElement) {
renderTableHeader(columns) {
return html`
<div id="tableHeader" part="header" class="list-headline" @track=${this._colResizeTracked}>
<div id="tableHeader" part="header" class="list-headline" @track=${this._colResizeTracked} @track-move=${this._colMoveTracked}>
${this.selectable ? html`
<div class="select-col" part="chkAll"><tp-checkbox @toggled=${e => this._checkedChanged(e)}></tp-checkbox></div>
` : null}
@ -214,7 +215,7 @@ export class TpTable extends DomQuery(LitElement) {
const canSort = sortable !== false;
return html`
<a part="column-label" class="${canSort ? 'sort-link' : 'no-sort-link'}" @click=${() => canSort ? this._sort(column, sortDirection) : null} .type=${name} .width=${width}>
<a part="column-label" class="${canSort ? 'sort-link' : 'no-sort-link'}" @click=${() => canSort ? this._sort(column, sortDirection) : null} .name=${name} .width=${width}>
${canSort ? html`
<tp-icon part="sort-icon" .icon=${sortDirection === 'asc' ? TpTable.downIcon : TpTable.upIcon} ?hidden=${!isSortedBy}></tp-icon>
` : null}
@ -256,9 +257,10 @@ export class TpTable extends DomQuery(LitElement) {
sorting: { type: Object },
selectable: { type: Boolean },
columns: { type: Array },
items: { type: Array },
columnMoveHandle: { type: String },
_selItems: { type: Map },
_visibleColumns: { type: Array },
items: { type: Array },
_advFilters: { type: Array },
_advFilterActive: { type: Boolean },
_advFilterFields: { type: Object },
@ -318,7 +320,11 @@ export class TpTable extends DomQuery(LitElement) {
firstUpdated() {
this.shadowRoot.querySelector('tp-scroll-threshold').target = this.shadowRoot.querySelector('lit-virtualizer');
new ColumResizer(this.$.tableHeader, '.width-handle');
new ColumnResizer(this.$.tableHeader, '.width-handle');
if (this.columnMoveHandle) {
this.colMover = new ColumnMover(this.$.tableHeader, this.columnMoveHandle);
}
}
_onScroll(e) {
@ -330,6 +336,7 @@ export class TpTable extends DomQuery(LitElement) {
if (changes.has('items')) {
this._updateSelEntries();
}
return true;
}
@ -370,9 +377,9 @@ export class TpTable extends DomQuery(LitElement) {
this.$.tableHeader.classList.remove('col-dragging');
const colLink = closest(handle, 'a');
const { type, width } = colLink;
const { name, width } = colLink;
const newWidth = (parseInt(width, 10) + data.dx) + 'px';
const colDef = this.columns.find(col => col.name === type);
const colDef = this.columns.find(col => col.name === name);
colDef.width = newWidth;
this.columns = [...this.columns];
@ -385,6 +392,91 @@ export class TpTable extends DomQuery(LitElement) {
}
}
_colMoveTracked(e) {
const { target, state, x } = e.detail;
const headers = this.shadowRoot.querySelectorAll('[part="column-label"]');
let targetIdx = -1;
let moveTo = -1;
const getHeader = name => {
for (const header of headers) {
if (header.name === name) {
return header;
}
}
};
for (let idx = 0; idx < this.columns.length; idx++) {
const header = getHeader(this.columns[idx].name);
if (!header) continue;
if (header === target.parentNode) {
targetIdx = idx;
}
const coords = header.getBoundingClientRect();
const result = this._isOverlapping(coords, x);
if (Math.abs(x - this.offsetWidth + this.offsetLeft) < 50) {
this.$.virtualList.scrollBy({
left: 200,
behavior: 'smooth'
});
}
if (x > this.offsetLeft && Math.abs(x - this.offsetLeft) < 50) {
this.$.virtualList.scrollBy({
left: -200,
behavior: 'smooth'
});
}
if (result.overlap !== 0) {
if (state === 'track') {
this.colMover.showIndicator(result.x, coords.top);
}
moveTo = idx;
// User wants to move the column to the left side of the hovered column.
// This means we need to decrease the index by one.
if (result.overlap === -1) {
moveTo = idx - 1;
}
}
}
if (state === 'end') {
if (moveTo === targetIdx) return;
if (targetIdx > moveTo) {
moveTo++;
}
this.columns.splice(moveTo, 0, this.columns.splice(targetIdx, 1)[0])
this.columns = [ ...this.columns ];
this.dispatchEvent(new CustomEvent('column-moved', { detail: { from: targetIdx, to: moveTo, columns: this.columns }, bubbles: true, composed: true }));
}
}
/**
* Determines if the mouse is hovering over the left or right half of a column header element.
* This check only happens on the x-axis.
*
* @param {{left: number, right: number}} coords Coordinates of the column header element.
* @param {number} x X-coordinate of the mouse pointer.
* @returns {{overlap: number, x: number}} overlap is -1 if overlap on the left half, 1 if overlap on the right half, 0 if no overlap.
* X is the border coordinate of the element, so either the right or left bound. Depending on the overlap side.
*/
_isOverlapping(coords, x) {
const half = coords.left + ((coords.right - coords.left) / 2);
if (x > coords.left && x <= half) return { overlap: -1, x: coords.left };
if (x > half && x <= coords.right) return { overlap: 1, x: coords.right };
return { overlap: 0, x: 0 };
}
clearTriggers() {
if (this.$.threshold) {
this.$.threshold.clearTriggers();
@ -405,6 +497,15 @@ export class TpTable extends DomQuery(LitElement) {
}
}
setSelection(items) {
this._selItems = new Map();
items.forEach(item => {
this._selItems.set(this.getItemId(item), item);
});
this._selectionChanged();
}
_selectAll() {
this._selItems = new Map();
this.items.forEach(item => {