Add column mover
This commit is contained in:
parent
d1e1c75949
commit
7b416bf910
120
column-mover.js
Normal file
120
column-mover.js
Normal 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)`;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export default class ColumResizer {
|
||||
export default class ColumnResizer {
|
||||
constructor(wrap, handleSelector) {
|
||||
this._handleSelector = handleSelector;
|
||||
this._mouseDown = this._mouseDown.bind(this);
|
||||
|
115
tp-table.js
115
tp-table.js
@ -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 => {
|
||||
|
Loading…
Reference in New Issue
Block a user