From 8b7facdc64fad69433c2a6f9287fb18ae10b6e38 Mon Sep 17 00:00:00 2001 From: trading_peter Date: Fri, 17 Jun 2022 16:13:55 +0200 Subject: [PATCH] Initial implementation. --- README.md | 2 +- column-resizer.js | 68 +++++ package-lock.json | 235 +++++++++++++++ package.json | 15 +- pagination.js | 226 +++++++++++++++ tp-table-item.js | 135 +++++++++ tp-table.js | 439 +++++++++++++++++++++++++++++ tp-element.js => tp-text-filter.js | 7 +- 8 files changed, 1118 insertions(+), 9 deletions(-) create mode 100644 column-resizer.js create mode 100644 package-lock.json create mode 100644 pagination.js create mode 100644 tp-table-item.js create mode 100644 tp-table.js rename tp-element.js => tp-text-filter.js (74%) diff --git a/README.md b/README.md index 1ab27b7..06a19e8 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# tp-element +# tp-table diff --git a/column-resizer.js b/column-resizer.js new file mode 100644 index 0000000..05eb9e6 --- /dev/null +++ b/column-resizer.js @@ -0,0 +1,68 @@ +export default class ColumResizer { + 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); + } + + _mouseDown(e) { + const handle = e.composedPath().find(node => node.matches && node.matches(this._handleSelector)); + if (!handle) return; + + 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) { + this._dx = e.pageX - this._startX; + + if (this._trackingStarted === false && Math.abs(this._dx) >= this.threshold) { + this._trackingStarted = true; + this._clearSelection(); + this._cHandle.dispatchEvent(new CustomEvent('track', { detail: { state: 'start', dx: this._dx }, bubbles: true, composed: true })); + } + + if (this._trackingStarted === true) { + this._cHandle.dispatchEvent(new CustomEvent('track', { detail: { state: 'track', dx: this._dx }, bubbles: true, composed: true })); + } + } + + _draggingEnd(e) { + this._dx = e.pageX - this._startX; + this._trackingStarted = false; + this._cHandle.dispatchEvent(new CustomEvent('track', { detail: { state: 'end', dx: this._dx }, bubbles: true, composed: true })); + this._stopWatchDrag(); + this._cHandle = null; + this._startX = 0; + this._clearSelection(); + } + + _clearSelection() { + const sel = window.getSelection ? window.getSelection() : document.selection; + if (sel) { + if (sel.removeAllRanges) { + sel.removeAllRanges(); + } else if (sel.empty) { + sel.empty(); + } + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c131fe4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,235 @@ +{ + "name": "@tp/tp-table", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@tp/tp-table", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@lit-labs/virtualizer": "^0.7.0", + "@tp/helpers": "^1.1.3", + "@tp/tp-checkbox": "^1.0.4", + "@tp/tp-icon": "^1.0.1", + "@tp/tp-scroll-threshold": "^1.0.0", + "lit": "^2.2.6" + } + }, + "node_modules/@lit-labs/virtualizer": { + "version": "0.7.0", + "resolved": "https://verdaccio.codeblob.work/@lit-labs%2fvirtualizer/-/virtualizer-0.7.0.tgz", + "integrity": "sha512-4h/TGmabGoo81EysUR9AV+fBeYHHvcgs0knmZDUQ5QRP49PM8k5mNnMfPBL7fxYErZrl0cUiNewg101q5zOitg==", + "license": "BSD-3-Clause", + "dependencies": { + "event-target-shim": "^5.0.1", + "lit": "^2.0.0", + "tslib": "^1.10.0" + } + }, + "node_modules/@lit/reactive-element": { + "version": "1.3.2", + "resolved": "https://verdaccio.codeblob.work/@lit%2freactive-element/-/reactive-element-1.3.2.tgz", + "integrity": "sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==", + "license": "BSD-3-Clause" + }, + "node_modules/@tp/helpers": { + "version": "1.1.3", + "resolved": "https://verdaccio.codeblob.work/@tp%2fhelpers/-/helpers-1.1.3.tgz", + "integrity": "sha512-WDj3meXgCjF9/4eyPQMk2YEderVDUD3IVJrjds0i4seABVI5yyaMWVNtxxH+w8O9eIDgwxiuyQqaI7bdyNaCoA==", + "license": "Apache-2.0" + }, + "node_modules/@tp/tp-checkbox": { + "version": "1.0.4", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-checkbox/-/tp-checkbox-1.0.4.tgz", + "integrity": "sha512-6pa7rS8sTi4b2EKgSIfeqgjMRXixt9PnehaPN6mOPW83BBivXuF1rtMwzjc38xNiqesyis0uhcJApTCyh3hA+A==", + "license": "Apache-2.0", + "dependencies": { + "@tp/helpers": "^1.1.3", + "@tp/tp-icon": "^1.0.1", + "lit": "^2.2.0" + } + }, + "node_modules/@tp/tp-icon": { + "version": "1.0.1", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-icon/-/tp-icon-1.0.1.tgz", + "integrity": "sha512-rBbQoXZ5t35F7yIbPAEGAlDscZhxLZ5/o229kyiBBrXvCrc+aVOsetSwF1jPeBSmb57h2PfinIvQhtMARwWHoA==", + "license": "Apache-2.0", + "dependencies": { + "@tp/tp-tooltip": "^1.0.0", + "lit": "^2.2.0" + } + }, + "node_modules/@tp/tp-scroll-threshold": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-scroll-threshold/-/tp-scroll-threshold-1.0.0.tgz", + "integrity": "sha512-8azYxjw9P1y5j9FLt6MVjzIBpG8vV8B6es0k2zeHf1CJJ7I/o+nRI+LOlfMoeU491bMZpgBdJHZqeNHLO5RyWw==", + "license": "Apache-2.0", + "dependencies": { + "lit": "^2.2.0" + } + }, + "node_modules/@tp/tp-tooltip": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-tooltip/-/tp-tooltip-1.0.0.tgz", + "integrity": "sha512-UtrIK5KWcEiC+HnHOVbgg90j4RjHn3e9ehOBYPZsm6zO+tT7pQJJYFOtJqBW+DDV7jVfH3AvGKCxtzNiJXYvDw==", + "license": "Apache-2.0", + "dependencies": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://verdaccio.codeblob.work/@types%2ftrusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "license": "MIT" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://verdaccio.codeblob.work/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lit": { + "version": "2.2.6", + "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.6.tgz", + "integrity": "sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-element": { + "version": "3.2.0", + "resolved": "https://verdaccio.codeblob.work/lit-element/-/lit-element-3.2.0.tgz", + "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-html": { + "version": "2.2.6", + "resolved": "https://verdaccio.codeblob.work/lit-html/-/lit-html-2.2.6.tgz", + "integrity": "sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://verdaccio.codeblob.work/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + } + }, + "dependencies": { + "@lit-labs/virtualizer": { + "version": "0.7.0", + "resolved": "https://verdaccio.codeblob.work/@lit-labs%2fvirtualizer/-/virtualizer-0.7.0.tgz", + "integrity": "sha512-4h/TGmabGoo81EysUR9AV+fBeYHHvcgs0knmZDUQ5QRP49PM8k5mNnMfPBL7fxYErZrl0cUiNewg101q5zOitg==", + "requires": { + "event-target-shim": "^5.0.1", + "lit": "^2.0.0", + "tslib": "^1.10.0" + } + }, + "@lit/reactive-element": { + "version": "1.3.2", + "resolved": "https://verdaccio.codeblob.work/@lit%2freactive-element/-/reactive-element-1.3.2.tgz", + "integrity": "sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==" + }, + "@tp/helpers": { + "version": "1.1.3", + "resolved": "https://verdaccio.codeblob.work/@tp%2fhelpers/-/helpers-1.1.3.tgz", + "integrity": "sha512-WDj3meXgCjF9/4eyPQMk2YEderVDUD3IVJrjds0i4seABVI5yyaMWVNtxxH+w8O9eIDgwxiuyQqaI7bdyNaCoA==" + }, + "@tp/tp-checkbox": { + "version": "1.0.4", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-checkbox/-/tp-checkbox-1.0.4.tgz", + "integrity": "sha512-6pa7rS8sTi4b2EKgSIfeqgjMRXixt9PnehaPN6mOPW83BBivXuF1rtMwzjc38xNiqesyis0uhcJApTCyh3hA+A==", + "requires": { + "@tp/helpers": "^1.1.3", + "@tp/tp-icon": "^1.0.1", + "lit": "^2.2.0" + } + }, + "@tp/tp-icon": { + "version": "1.0.1", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-icon/-/tp-icon-1.0.1.tgz", + "integrity": "sha512-rBbQoXZ5t35F7yIbPAEGAlDscZhxLZ5/o229kyiBBrXvCrc+aVOsetSwF1jPeBSmb57h2PfinIvQhtMARwWHoA==", + "requires": { + "@tp/tp-tooltip": "^1.0.0", + "lit": "^2.2.0" + } + }, + "@tp/tp-scroll-threshold": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-scroll-threshold/-/tp-scroll-threshold-1.0.0.tgz", + "integrity": "sha512-8azYxjw9P1y5j9FLt6MVjzIBpG8vV8B6es0k2zeHf1CJJ7I/o+nRI+LOlfMoeU491bMZpgBdJHZqeNHLO5RyWw==", + "requires": { + "lit": "^2.2.0" + } + }, + "@tp/tp-tooltip": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-tooltip/-/tp-tooltip-1.0.0.tgz", + "integrity": "sha512-UtrIK5KWcEiC+HnHOVbgg90j4RjHn3e9ehOBYPZsm6zO+tT7pQJJYFOtJqBW+DDV7jVfH3AvGKCxtzNiJXYvDw==", + "requires": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + } + }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://verdaccio.codeblob.work/@types%2ftrusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://verdaccio.codeblob.work/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "lit": { + "version": "2.2.6", + "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.6.tgz", + "integrity": "sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "lit-element": { + "version": "3.2.0", + "resolved": "https://verdaccio.codeblob.work/lit-element/-/lit-element-3.2.0.tgz", + "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "lit-html": { + "version": "2.2.6", + "resolved": "https://verdaccio.codeblob.work/lit-html/-/lit-html-2.2.6.tgz", + "integrity": "sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://verdaccio.codeblob.work/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } +} diff --git a/package.json b/package.json index c39fdff..6b1dba7 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,23 @@ { - "name": "@tp/tp-element", - "version": "0.0.1", + "name": "@tp/tp-table", + "version": "1.0.0", "description": "", - "main": "tp-element.js", + "main": "tp-table.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "https://gitea.codeblob.work/tp-elements/tp-element.git" + "url": "https://gitea.codeblob.work/tp-elements/tp-table.git" }, "author": "trading_peter", "license": "Apache-2.0", "dependencies": { - "lit": "^2.2.0" + "@lit-labs/virtualizer": "^0.7.0", + "@tp/helpers": "^1.1.3", + "@tp/tp-checkbox": "^1.0.4", + "@tp/tp-icon": "^1.0.1", + "@tp/tp-scroll-threshold": "^1.0.0", + "lit": "^2.2.6" } } diff --git a/pagination.js b/pagination.js new file mode 100644 index 0000000..c362fd3 --- /dev/null +++ b/pagination.js @@ -0,0 +1,226 @@ +/** +@license +Copyright (c) 2022 trading_peter +This program is available under Apache License Version 2.0 +*/ + +import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js'; +import { Helper } from '@era-core/era-mixins/era-helper-mixin.js'; +import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js'; +import { timeOut } from '@polymer/polymer/lib/utils/async.js'; + +/** + * # ListLoader + * + * Methods to load list contents page by page. + * + * @polymerBehavior ListLoader + */ +export const Pagination = dedupingMixin(function(superClass) { + return class extends Helper(superClass) { + static get properties() { + return { + _entries: { type: Array }, + _entriesSet: { type: Object }, + _page: { type: Number }, + _filter: { type: String }, + _filterPage: { type: Number }, + _limit: { type: Number } + }; + } + + constructor() { + super(); + + this._entries = []; + this._entriesSet = new Set(); + this._page = 1; + this._filterPage = 1; + this._limit = 350; + } + + _fetch(query, cb) { + // Override + } + + _isInFlight() { + // Override + } + + get _hasFilter() { + return typeof this._filter === 'string' && this._filter.length > 0; + } + + _reloadList() { + this._resetList(); + this.__currentPage = 0; + this._lastPage = false; + this._listStateChanged(); + } + + _listStateChanged() { + if (this._hasFilter) { + this._fetchFiltered(); + } else { + this._fetchPage(); + } + } + + _scrollThresholdTriggered(e) { + if (this.active && this._listOverflows()) { + this.__list = e.composedPath()[0].scrollTarget; + this.__listScrollOffset = this.__list.scrollTop; + + if (this._hasFilter) { + this._filterPage++; + this._fetchFiltered(); + } else { + this._page++; + this._fetchPage(); + } + } + } + + _resetList() { + if (typeof this.setProperties === 'function') { + this.setProperties({ _page: 1, _filterPage: 1, _entries: [] }); + } else { + this._page = 1; + this._filterPage = 1; + this._entries = []; + } + } + + _shouldFetch() { + if (this._page === undefined || this._limit === undefined) return false; + const pageChanged = this._page !== this.__currentPage; + + let optionsChanged = false; + optionsChanged = JSON.stringify(this._statusFilter) !== JSON.stringify(this.__currentStatusFilter) || optionsChanged; + optionsChanged = JSON.stringify(this._sorting) !== JSON.stringify(this.__currentSorting) || optionsChanged; + + this.__currentStatusFilter = this._statusFilter; + this.__currentSorting = this._sorting; + + if (optionsChanged === true) { + this._resetList(); + } + + if ((pageChanged === false || this._lastPage || this._isInFlight()) && optionsChanged === false) return false; + + return true; + } + + _fetchPage() { + this._filterDebouncer = Debouncer.debounce( + this._filterDebouncer, + timeOut.after(20), + () => { + if (this._shouldFetch() === false) return; + + this._fetch({ page: this._page, limit: this._limit, options: { statusFilter: this._statusFilter, sorting: this._sorting } }, (docs, pages) => { + if (this._page === 1) { + this._entriesSet = new Set(); + } + + docs = docs.filter(doc => this._entriesSet.has(doc._id) === false); + docs.forEach(doc => this._entriesSet.add(doc._id)); + + if (this._page === 1) { + this._entries = docs; + } else { + this._entries = this._entries.concat(docs); + } + + // Restore scrolling position in the list. + if (this.__list !== undefined) { + this.__list.scrollTop = this.__listScrollOffset || 0; + } + + this.__currentPage = this._page; + + // Check if we reached the last page. + this._lastPage = pages <= this._page; + + // Clear scroll threshold so the next page can be loaded. + this.clearTriggers(); + + this.__fillList(); + }); + } + ); + } + + _fetchFiltered() { + this._filterDebouncer = Debouncer.debounce( + this._filterDebouncer, + timeOut.after(300), + () => { + if (!this._hasFilter) { + this._lastPage = false; + this.__currentPage = false; + this._lastFilter = null; + this._page = 1; + this._filterPage = 1; + this.__listScrollOffset = 0; + this._fetchPage(); + return; + } + + if (this._shouldFetch() === false && this._lastPage === true && this._filter === this._lastFilter) return; + + if (this._filter !== this._lastFilter) { + this._filterPage = 1; + } + + this._lastFilter = this._filter; + + this._fetch({ filter: this._filter, page: this._filterPage, limit: this._limit, options: { + statusFilter: this._statusFilter, + sorting: this._sorting + } }, (result, resp) => { + if (resp && resp.statusCode === 200) { + if (this._filterPage === 1) { + this._entries = result.entries.map(entry => entry.doc); + } else { + this._entries = this._entries.concat(result.entries.map(entry => entry.doc)); + } + + // Restore scrolling position in the list. + if (this.__list !== undefined) { + this.__list.scrollTop = this.__listScrollOffset || 0; + } + + // Check if we reached the last page. + this._lastPage = result.pages <= this._filterPage || result.pages === 0; + + // Clear scroll threshold so the next page can be loaded. + this.clearTriggers(); + + this.__fillList(); + } + }); + } + ); + } + + // Load pages until the list fills the screen. + // We check the list height after next render to see if the list already fills the screen. + // If not, load the next page. + // This needs to be triggered async so that the current `_fetchPage` request + // is finished and no longer considered "in flight". + __fillList() { + if (!this._lastPage && !this.listOverflows()) { + setTimeout(() => { + if (this._hasFilter) { + this._filterPage++; + this._fetchFiltered(); + } else { + this._page++; + this._fetchPage(); + } + }); + } + } + }; +}); diff --git a/tp-table-item.js b/tp-table-item.js new file mode 100644 index 0000000..87adc7a --- /dev/null +++ b/tp-table-item.js @@ -0,0 +1,135 @@ +/** +@license +Copyright (c) 2021 EDV Wasmeier +*/ + +import '@tp/tp-checkbox/tp-checkbox.js'; +import { LitElement, html, css } from 'lit'; +import { DomQuery } from '@tp/helpers/dom-query.js'; + +/** +# ef-base-table-item + +## Example +```html + +``` + +*/ + +const mixins = [ + DomQuery +]; + +/* @litElement */ +const BaseElement = mixins.reduce((baseClass, mixin) => { + return mixin(baseClass); +}, LitElement); + +class TpTableItem extends BaseElement { + static get styles() { + return [ + css` + :host { + display: block; + padding: 0; + } + + .wrap { + display: grid; + grid-template-columns: 0.5fr 3fr 0.5fr 0.5fr 0.5fr; /* Overridden by javascript */ + align-items: center; + } + + [part="cell"] { + align-self: stretch; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 10px 20px; + border-right: solid 1px #c1c1c1; + } + + .wrap > div.chk { + border-right: none; + } + ` + ]; + } + + render() { + const { columns, item } = this; + + return html` +
+ ${this.selectable ? html` +
+ +
+ ` : null} + ${Array.isArray(columns) ? columns.map(column => { + if (column.visible !== true && column.required !== true) return; + return this.renderColumn(column, item) || null; + }) : null + } +
+ `; + } + + static get properties() { + return { + index: { type: Array }, + item: { type: Object }, + columns: { type: Array }, + selectable: { type: Boolean }, + }; + } + + renderColumn(column, item) { + return html`
${item[column.name]}
`; + } + + async updated(changes) { + if (changes.has('item') && this.selectable) { + this.$.selectionChk.checked = Boolean(this.item.__selected__); + } + + if (changes.has('columns') && Array.isArray(this.columns)) { + let colWidths = this.columns.filter(col => col.required || col.visible).map(col => col.width).join(' '); + + if (this.selectable) { + colWidths = '40px ' + colWidths; + } + + this.$grid.style.gridTemplateColumns = colWidths; + } + + if (changes.has('index')) { + if (this.index % 2 === 0) { + this.part.add('odd'); + } else { + this.part.remove('odd'); + } + } + } + + get $grid() { + if (this.__$grid) { + return this.__$grid; + } + this.__$grid = this.shadowRoot.getElementById('grid'); + return this.__$grid; + } + + _updateSelectionState(e) { + const target = e.target; + + setTimeout(() => { + this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: this.item, selected: target.checked }, bubbles: true, composed: true })); + }, 0); + } +} + +window.customElements.define('tp-table-item', TpTableItem); + +export default TpTableItem diff --git a/tp-table.js b/tp-table.js new file mode 100644 index 0000000..198628b --- /dev/null +++ b/tp-table.js @@ -0,0 +1,439 @@ +/** +@license +Copyright (c) 2022 trading_peter +This program is available under Apache License Version 2.0 +*/ + +import '@tp/tp-scroll-threshold/tp-scroll-threshold.js'; +import '@lit-labs/virtualizer'; +import '@tp/tp-icon/tp-icon.js'; +import '@tp/tp-checkbox/tp-checkbox.js'; +import './tp-table-item.js'; +import { DomQuery } from '@tp/helpers/dom-query.js'; +import { closest } from '@tp/helpers/closest.js'; +import ColumResizer from './column-resizer.js'; +import { LitElement, html, css, svg } from 'lit'; + +export class TpTable extends DomQuery(LitElement) { + static get styles() { + return [ + css` + :host { + display: block; + padding: 20px; + position: relative; + } + + [hidden] { + display: none; + } + + .wrap { + overflow-y: auto; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + } + + .list { + position: relative; + flex: 1; + } + + #tableHeader { + display: grid; + overflow-x: hidden; + } + + #virtualList, + .empty-message { + position: absolute !important; + left: 0; + right: 0; + top: 0; + bottom: 0; + } + + #virtualList { + height: auto; + } + + .empty-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + a.sort-link, + a.no-sort-link { + text-decoration: none; + font-weight: bold; + position: relative; + border-right: solid 1px #c1c1c1; + padding: 10px 20px 10px 20px; + display: flex; + flex-direction: row; + overflow: hidden; + cursor: pointer; + user-select: none; + } + + a.sort-link:hover, + a.no-sort-link:hover { + border-right: solid 1px #c1c1c1; + } + + a.sort-link tp-icon { + position: absolute; + left: 0; + --tp-icon-width: 18px; + --tp-icon-height: 18px; + } + + div.col-label { + flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + } + + .width-handle { + position: absolute; + z-index: 1; + top: 0; + right: -2px; + cursor: col-resize; + width: 6px; + height: 100%; + } + + .width-handle > div { + margin: auto; + width: 2px; + height: 100%; + background: transparent; + } + + #tableHeader:not(.col-dragging) a.sort-link .width-handle:hover > div { + background: var(--pc-blue); + } + + a.sort-link .width-handle.dragging { + position: fixed; + height: 300px; + z-index: 100; + border-style: none; + width: 3px; + background: linear-gradient(180deg, rgba(59, 164, 240, 1), rgba(59, 164, 240, 0)); + } + + .select-col { + padding: 0px 20px 10px; + } + + lit-virtualizer { + overflow-x: hidden; + } + + lit-virtualizer::-webkit-scrollbar { + width: 12px; + } + + lit-virtualizer::-webkit-scrollbar-track { + background: var(--scrollbar-track); + } + + lit-virtualizer::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + outline: none; + border-radius: var(--scrollbar-thumb-border-radius, 4px); + } + + [item]:not(.odd) { + background: var(--table-bg-1); + } + + [item].odd { + background: var(--table-bg-2); + } + + [item]:hover { + background: var(--table-row-hl); + --list-columns-h-borders: var(--list-columns-h-borders-hover); + } + ` + ]; + } + + render() { + const columns = this.columns || []; + const items = this.items || []; + + return html` +
+
+ ${this.selectable ? html` +
this._checkedChanged(e)}>
+ ` : null} + ${columns.map(column => this.renderColumnHeader(column))} +
+
+ ${this._emptyMessage} + this.renderItem(item, idx, columns)}> +
+ + +
+ `; + } + + renderColumnHeader(column) { + if (column.visible !== true && column.required !== true) return null; + + const { name, width, label, sortable } = column; + const sorting = this.sorting || {}; + const isSortedBy = sorting.column === name; + const sortDirection = isSortedBy ? sorting.direction : 'desc'; + const canSort = sortable !== false; + + return html` + canSort ? this._sort(column, sortDirection) : null} .type=${name} .width=${width}> + ${canSort ? html` + + ` : null} +
${label}
+
+ ${this.renderColumnHeaderAddons(column)} +
+ `; + } + + renderColumnHeaderAddons(column) { + return null; + } + + static get downIcon() { + return svg``; + } + + static get upIcon() { + return svg``; + } + + renderItem(item, idx, columns) { + return html` + this._selectionChanged(e)}> + + `; + } + + static get properties() { + return { + sorting: { type: Object }, + selectable: { type: Boolean }, + columns: { type: Array }, + _selItems: { type: Array }, + _visibleColumns: { type: Array }, + items: { type: Array }, + _advFilters: { type: Array }, + _advFilterActive: { type: Boolean }, + _advFilterFields: { type: Object }, + _filter: { type: String }, + _totalCount: { type: Number }, + }; + } + + constructor() { + super(); + this._selItems = []; + } + + updated(changes) { + if (changes.has('columns')) { + this._updateColumns(); + } + } + + get _emptyMessage() { + return null; + } + + get sortingPath() { + return `${this.dataKey}.sorting`; + } + + get statusFilterPath() { + return `${this.dataKey}.filtering.statusFilter`; + } + + set filter(val) { + this._advFilterActive = false; + this._filter = this._clone(val); + this.reloadPagedList(); + } + + set statusFilter(val) { + this._statusFilter = this._clone(val); + this.reloadPagedList(); + } + + set advancedFilter(val) { + this._advFilterActive = val ? true : false; + this._advFilters = this._clone(val); + this.reloadPagedList(); + } + + firstUpdated() { + this.shadowRoot.querySelector('tp-scroll-threshold').target = this.shadowRoot.querySelector('lit-virtualizer'); + new ColumResizer(this.$.tableHeader, '.width-handle'); + } + + _onScroll(e) { + this.$.tableHeader.style.paddingRight = (this.$.virtualList.offsetWidth - this.$.virtualList.clientWidth) + 'px'; + this.$.tableHeader.scrollLeft = this.$.virtualList.scrollLeft; + } + + shouldUpdate(changes) { + if (changes.has('items')) { + this._updateSelEntries(); + } + return true; + } + + _sort(column, direction) { + if (this._draggedColumn) return; + + this.sorting = { column: column.name, direction: direction === 'asc' ? 'desc' : 'asc' }; + this.dispatchEvent(new CustomEvent('sorting-changed', { detail: this.sorting, bubbles: true, composed: true })); + } + + _updateColumns() { + this.$.tableHeader.style.gridTemplateColumns = this._updateColumnWidths(this.selectable ? [ '40px' ] : []).join(' '); + } + + _updateColumnWidths(prepend) { + return [ ...(prepend || []), ...this.columns.filter(col => col.required || col.visible).map(col => col.width) ]; + } + + _colResizeTracked(e) { + const handle = closest(e.target, '.width-handle'); + const data = e.detail; + + if (data.state === 'start') { + this._draggedColumn = true; + const rect = handle.getBoundingClientRect(); + document.body.style.userSelect = 'none'; + handle.style.top = `${rect.top}px`; + handle.style.left = `${rect.left}px`; + handle.style.transform = `translateX(${data.dx}px)`; + handle.classList.add('dragging'); + this.$.tableHeader.classList.add('col-dragging'); + } else if (data.state === 'end') { + document.body.style.userSelect = ''; + handle.classList.remove('dragging'); + handle.style.transform = ''; + handle.style.top = ''; + handle.style.left = ''; + this.$.tableHeader.classList.remove('col-dragging'); + + const colLink = closest(handle, 'a'); + const { type, width } = colLink; + const newWidth = (parseInt(width, 10) + data.dx) + 'px'; + const colDef = this.columns.find(col => col.name === type); + colDef.width = newWidth; + this.columns = [...this.columns]; + + setTimeout(() => { + this._draggedColumn = false; + this.dispatchEvent(new CustomEvent('column-width-changed', { detail: this.columns, bubbles: true, composed: true })); + }); + } else if (data.state === 'track') { + handle.style.transform = `translateX(${data.dx}px)`; + } + } + + clearTriggers() { + if (this.$.threshold) { + this.$.threshold.clearTriggers(); + } + } + + _listOverflows() { + return this.$.virtualList.scrollHeight > this.$.virtualList.offsetHeight; + } + + _checkedChanged(e) { + if (Array.isArray(this.items) === false) return; + + if (e.detail.value) { + this._selectAll(); + } else { + this._selectNone(); + } + + this.items = [...this.times]; + } + + _selectAll() { + this.items.forEach((entry, idx) => { + this.items[idx].__selected__ = true; + }); + + this._selectionChanged(); + } + + _selectNone() { + this._items.forEach((entry, idx) => { + this._items[idx].__selected__ = false; + }); + + this._selectionChanged(); + } + + _selectionChanged(e) { + if (this.__restoringSelection) return; + + if (e !== undefined) { + const item = this._items.find(item => item._id === e.detail.item._id); + item.__selected__ = e.detail.selected; + } + + this._selItems = this._items.filter(entry => entry.__selected__ === true); + this.dispatchEvent(new CustomEvent('item-selection-changed', { detail: this._selItems, bubbles: true, composed: true })); + } + + _updateSelEntries() { + this._hiddenSelection = 0; + + if (!this._items) { + this._selItems = []; + } else { + this._selItems.forEach(sel => { + const idx = this._items.findIndex(entry => entry._id === sel._id); + if (idx > -1) { + // Suppress rebuild of the selected invoices list. + // When we restore the selection on a filtered list of invoices we would loose the selection of hidden onces + // when _selectionChanged is triggered. So we set a flag that the observer is skipped. + this.__restoringSelection = true; + this._items[idx].__selected__ = true; + this.__restoringSelection = false; + } else { + this._hiddenSelection++; + } + }); + } + } +} + +window.customElements.define('tp-table', TpTable); diff --git a/tp-element.js b/tp-text-filter.js similarity index 74% rename from tp-element.js rename to tp-text-filter.js index 6a92a2f..665d854 100644 --- a/tp-element.js +++ b/tp-text-filter.js @@ -6,7 +6,7 @@ This program is available under Apache License Version 2.0 import { LitElement, html, css } from 'lit'; -class TpElement extends LitElement { +class TpTextFilter extends LitElement { static get styles() { return [ css` @@ -21,7 +21,8 @@ class TpElement extends LitElement { const { } = this; return html` - +

Test

+

No

`; } @@ -32,4 +33,4 @@ class TpElement extends LitElement { } -window.customElements.define('tp-element', TpElement); +window.customElements.define('tp-text-filter', TpTextFilter); \ No newline at end of file