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) {
|
constructor(wrap, handleSelector) {
|
||||||
this._handleSelector = handleSelector;
|
this._handleSelector = handleSelector;
|
||||||
this._mouseDown = this._mouseDown.bind(this);
|
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 { DomQuery } from '@tp/helpers/dom-query.js';
|
||||||
import { closest } from '@tp/helpers/closest.js';
|
import { closest } from '@tp/helpers/closest.js';
|
||||||
import { LitElement, html, css, svg } from 'lit';
|
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) {
|
export class TpTable extends DomQuery(LitElement) {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
@ -195,7 +196,7 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
|
|
||||||
renderTableHeader(columns) {
|
renderTableHeader(columns) {
|
||||||
return html`
|
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`
|
${this.selectable ? html`
|
||||||
<div class="select-col" part="chkAll"><tp-checkbox @toggled=${e => this._checkedChanged(e)}></tp-checkbox></div>
|
<div class="select-col" part="chkAll"><tp-checkbox @toggled=${e => this._checkedChanged(e)}></tp-checkbox></div>
|
||||||
` : null}
|
` : null}
|
||||||
@ -214,7 +215,7 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
const canSort = sortable !== false;
|
const canSort = sortable !== false;
|
||||||
|
|
||||||
return html`
|
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`
|
${canSort ? html`
|
||||||
<tp-icon part="sort-icon" .icon=${sortDirection === 'asc' ? TpTable.downIcon : TpTable.upIcon} ?hidden=${!isSortedBy}></tp-icon>
|
<tp-icon part="sort-icon" .icon=${sortDirection === 'asc' ? TpTable.downIcon : TpTable.upIcon} ?hidden=${!isSortedBy}></tp-icon>
|
||||||
` : null}
|
` : null}
|
||||||
@ -256,9 +257,10 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
sorting: { type: Object },
|
sorting: { type: Object },
|
||||||
selectable: { type: Boolean },
|
selectable: { type: Boolean },
|
||||||
columns: { type: Array },
|
columns: { type: Array },
|
||||||
|
items: { type: Array },
|
||||||
|
columnMoveHandle: { type: String },
|
||||||
_selItems: { type: Map },
|
_selItems: { type: Map },
|
||||||
_visibleColumns: { type: Array },
|
_visibleColumns: { type: Array },
|
||||||
items: { type: Array },
|
|
||||||
_advFilters: { type: Array },
|
_advFilters: { type: Array },
|
||||||
_advFilterActive: { type: Boolean },
|
_advFilterActive: { type: Boolean },
|
||||||
_advFilterFields: { type: Object },
|
_advFilterFields: { type: Object },
|
||||||
@ -318,7 +320,11 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.shadowRoot.querySelector('tp-scroll-threshold').target = this.shadowRoot.querySelector('lit-virtualizer');
|
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) {
|
_onScroll(e) {
|
||||||
@ -330,6 +336,7 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
if (changes.has('items')) {
|
if (changes.has('items')) {
|
||||||
this._updateSelEntries();
|
this._updateSelEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,9 +377,9 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
this.$.tableHeader.classList.remove('col-dragging');
|
this.$.tableHeader.classList.remove('col-dragging');
|
||||||
|
|
||||||
const colLink = closest(handle, 'a');
|
const colLink = closest(handle, 'a');
|
||||||
const { type, width } = colLink;
|
const { name, width } = colLink;
|
||||||
const newWidth = (parseInt(width, 10) + data.dx) + 'px';
|
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;
|
colDef.width = newWidth;
|
||||||
this.columns = [...this.columns];
|
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() {
|
clearTriggers() {
|
||||||
if (this.$.threshold) {
|
if (this.$.threshold) {
|
||||||
this.$.threshold.clearTriggers();
|
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() {
|
_selectAll() {
|
||||||
this._selItems = new Map();
|
this._selItems = new Map();
|
||||||
this.items.forEach(item => {
|
this.items.forEach(item => {
|
||||||
|
Loading…
Reference in New Issue
Block a user