Support for row selection without checkboxes.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tp/tp-table",
|
"name": "@tp/tp-table",
|
||||||
"version": "1.4.2",
|
"version": "1.5.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "tp-table.js",
|
"main": "tp-table.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+24
-4
@@ -71,10 +71,11 @@ class TpTableItem extends BaseElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { columns, item, selected } = this;
|
const { columns, item, selected } = this;
|
||||||
|
const showCheckbox = this.selectable && this.selectionMode !== 'row-click';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div id="grid" class="wrap" part="row">
|
<div id="grid" class="wrap" part="row">
|
||||||
${this.selectable ? html`
|
${showCheckbox ? html`
|
||||||
<div class="chk" part="chk">
|
<div class="chk" part="chk">
|
||||||
<tp-checkbox id="selectionChk" .checked=${selected} @click=${this._updateSelectionState}></tp-checkbox>
|
<tp-checkbox id="selectionChk" .checked=${selected} @click=${this._updateSelectionState}></tp-checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,12 +89,23 @@ class TpTableItem extends BaseElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changes) {
|
||||||
|
if (changes.has('selectionMode') || changes.has('selectable')) {
|
||||||
|
if (this.selectionMode && this.selectionMode !== 'checkbox') {
|
||||||
|
this.setAttribute('data-selection-mode', this.selectionMode);
|
||||||
|
} else {
|
||||||
|
this.removeAttribute('data-selection-mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
index: { type: Array },
|
index: { type: Array },
|
||||||
item: { type: Object, hasChanged: () => true },
|
item: { type: Object, hasChanged: () => true },
|
||||||
columns: { type: Array },
|
columns: { type: Array },
|
||||||
selectable: { type: Boolean },
|
selectable: { type: Boolean },
|
||||||
|
selectionMode: { type: String },
|
||||||
selected: { type: Boolean, reflect: true },
|
selected: { type: Boolean, reflect: true },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,10 +115,18 @@ class TpTableItem extends BaseElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updated(changes) {
|
async updated(changes) {
|
||||||
if (changes.has('columns') && Array.isArray(this.columns)) {
|
const needsWidthRecalc =
|
||||||
let colWidths = this.columns.filter(col => col.required || col.visible).map(col => col.width).join(' ');
|
(changes.has('columns') && Array.isArray(this.columns)) ||
|
||||||
|
changes.has('selectionMode') ||
|
||||||
|
changes.has('selectable');
|
||||||
|
|
||||||
if (this.selectable) {
|
if (needsWidthRecalc) {
|
||||||
|
let colWidths = (this.columns || [])
|
||||||
|
.filter(col => col.required || col.visible)
|
||||||
|
.map(col => col.width)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
if (this.selectable && this.selectionMode !== 'row-click') {
|
||||||
colWidths = '40px ' + colWidths;
|
colWidths = '40px ' + colWidths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+105
-12
@@ -166,6 +166,13 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
background: var(--table-row-hl);
|
background: var(--table-row-hl);
|
||||||
--list-columns-h-borders: var(--list-columns-h-borders-hover);
|
--list-columns-h-borders: var(--list-columns-h-borders-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Selected rows get a pointer cursor so the click-to-select affordance
|
||||||
|
is obvious. Visual styling is the consumer's responsibility
|
||||||
|
(see terminal/frontend/src/styles/controls.js for the default). */
|
||||||
|
[item][selected] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -173,11 +180,14 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
render() {
|
render() {
|
||||||
const columns = this.columns || [];
|
const columns = this.columns || [];
|
||||||
const items = this.items || [];
|
const items = this.items || [];
|
||||||
|
const showCheckboxColumn = this.selectable && this.selectionMode !== 'row-click';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
${this.renderTableHeader(columns)}
|
${this.renderTableHeader(columns, showCheckboxColumn)}
|
||||||
<div class="list" @row-selection-changed=${(e) => this._selectionChanged(e)}>
|
<div class="list"
|
||||||
|
@row-selection-changed=${(e) => this._selectionChanged(e)}
|
||||||
|
@click=${(e) => this._handleRowClick(e)}>
|
||||||
${!Array.isArray(items) || items.length === 0 ? html`
|
${!Array.isArray(items) || items.length === 0 ? html`
|
||||||
<div class="empty-message">
|
<div class="empty-message">
|
||||||
<slot name="empty-message"></slot>
|
<slot name="empty-message"></slot>
|
||||||
@@ -191,17 +201,21 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTableHeader(columns) {
|
renderTableHeader(columns, showCheckboxColumn = this.selectable) {
|
||||||
return html`
|
return html`
|
||||||
<div id="tableHeader" part="header" class="list-headline" @track=${this._colResizeTracked} @track-move=${this._colMoveTracked}>
|
<div id="tableHeader" part="header" class="list-headline" @track=${this._colResizeTracked} @track-move=${this._colMoveTracked}>
|
||||||
${this.selectable ? html`
|
${showCheckboxColumn ? html`
|
||||||
<div class="select-col" part="chkAll"><tp-checkbox @toggled=${e => this._checkedChanged(e)}></tp-checkbox></div>
|
<div class="select-col" part="chkAll">${this.renderSelectionHeader()}</div>
|
||||||
` : null}
|
` : null}
|
||||||
${columns.map((column, idx) => this.renderColumnHeader(column, idx))}
|
${columns.map((column, idx) => this.renderColumnHeader(column, idx))}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSelectionHeader() {
|
||||||
|
return html`<tp-checkbox @toggled=${e => this._checkedChanged(e)}></tp-checkbox>`;
|
||||||
|
}
|
||||||
|
|
||||||
renderColumnHeader(column, idx) {
|
renderColumnHeader(column, idx) {
|
||||||
if (column.visible !== true && column.required !== true) return null;
|
if (column.visible !== true && column.required !== true) return null;
|
||||||
|
|
||||||
@@ -244,6 +258,7 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
.item=${item}
|
.item=${item}
|
||||||
.selected=${selected}
|
.selected=${selected}
|
||||||
.selectable=${this.selectable}
|
.selectable=${this.selectable}
|
||||||
|
.selectionMode=${this.selectionMode}
|
||||||
.columns=${columns}>
|
.columns=${columns}>
|
||||||
</tp-table-item>
|
</tp-table-item>
|
||||||
`;
|
`;
|
||||||
@@ -256,9 +271,18 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
|
|
||||||
sorting: { type: Object },
|
sorting: { type: Object },
|
||||||
selectable: { type: Boolean },
|
selectable: { type: Boolean },
|
||||||
|
/**
|
||||||
|
* Controls how rows are selected when `selectable` is true.
|
||||||
|
* - "checkbox" (default): header + per-row checkboxes; multi-select.
|
||||||
|
* - "row-click": click a row to select. Single-select on plain click,
|
||||||
|
* ctrl/cmd+click toggles individual rows, shift+click selects a range
|
||||||
|
* from the last clicked row. No checkbox column is rendered.
|
||||||
|
*/
|
||||||
|
selectionMode: { type: String },
|
||||||
items: { type: Array },
|
items: { type: Array },
|
||||||
columnMoveHandle: { type: String },
|
columnMoveHandle: { type: String },
|
||||||
_selItems: { type: Map },
|
_selItems: { state: true },
|
||||||
|
_lastSelectedId: { state: true },
|
||||||
_visibleColumns: { type: Array },
|
_visibleColumns: { type: Array },
|
||||||
_advFilters: { type: Array },
|
_advFilters: { type: Array },
|
||||||
_advFilterActive: { type: Boolean },
|
_advFilterActive: { type: Boolean },
|
||||||
@@ -271,10 +295,12 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._selItems = new Map();
|
this._selItems = new Map();
|
||||||
|
this._lastSelectedId = null;
|
||||||
|
this.selectionMode = 'checkbox';
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changes) {
|
updated(changes) {
|
||||||
if (changes.has('columns')) {
|
if (changes.has('columns') || changes.has('selectionMode') || changes.has('selectable')) {
|
||||||
this._updateColumns();
|
this._updateColumns();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +391,13 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateColumns() {
|
_updateColumns() {
|
||||||
this.$.tableHeader.style.gridTemplateColumns = this._updateColumnWidths(this.selectable ? ['40px'] : []).join(' ');
|
this.$.tableHeader.style.gridTemplateColumns = this._updateColumnWidths(
|
||||||
|
this._hasCheckboxColumn ? ['40px'] : []
|
||||||
|
).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _hasCheckboxColumn() {
|
||||||
|
return this.selectable && this.selectionMode !== 'row-click';
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateColumnWidths(prepend) {
|
_updateColumnWidths(prepend) {
|
||||||
@@ -558,15 +590,76 @@ export class TpTable extends DomQuery(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateSelEntries() {
|
_updateSelEntries() {
|
||||||
if (!this.items) {
|
if (!Array.isArray(this.items)) {
|
||||||
this._selItems = new Map();
|
if (this._selItems.size > 0) {
|
||||||
|
this._selItems = new Map();
|
||||||
|
this._lastSelectedId = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const selItem of this._selItems.values()) {
|
const validIds = new Set(this.items.map(item => this.getItemId(item)));
|
||||||
if (this.items.findIndex(item => this.getItemId(item) === this.getItemId(selItem)) > -1) {
|
let changed = false;
|
||||||
|
|
||||||
|
for (const id of [...this._selItems.keys()]) {
|
||||||
|
if (!validIds.has(id)) {
|
||||||
|
this._selItems.delete(id);
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._lastSelectedId !== null && !validIds.has(this._lastSelectedId)) {
|
||||||
|
this._lastSelectedId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this._selectionChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRowClick(e) {
|
||||||
|
if (this.selectionMode !== 'row-click' || !this.selectable) return;
|
||||||
|
|
||||||
|
// Walk through composedPath to find the <tp-table-item> host, since
|
||||||
|
// lit-virtualizer puts rows in a shadow tree.
|
||||||
|
const itemEl = e.composedPath().find(
|
||||||
|
n => n?.hasAttribute && n.hasAttribute('item')
|
||||||
|
);
|
||||||
|
if (!itemEl || !itemEl.item) return;
|
||||||
|
|
||||||
|
const id = this.getItemId(itemEl.item);
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
if (e.shiftKey && this._lastSelectedId !== null && this._lastSelectedId !== id) {
|
||||||
|
// Range select: replace selection with the range between last and current.
|
||||||
|
const items = this.items || [];
|
||||||
|
const fromIdx = items.findIndex(i => this.getItemId(i) === this._lastSelectedId);
|
||||||
|
const toIdx = items.findIndex(i => this.getItemId(i) === id);
|
||||||
|
if (fromIdx !== -1 && toIdx !== -1) {
|
||||||
|
const [lo, hi] = fromIdx < toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
|
||||||
|
this._selItems = new Map();
|
||||||
|
for (let i = lo; i <= hi; i++) {
|
||||||
|
const it = items[i];
|
||||||
|
this._selItems.set(this.getItemId(it), it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
|
// Toggle individual row.
|
||||||
|
if (this._selItems.has(id)) {
|
||||||
|
this._selItems.delete(id);
|
||||||
|
} else {
|
||||||
|
this._selItems.set(id, itemEl.item);
|
||||||
|
}
|
||||||
|
this._lastSelectedId = id;
|
||||||
|
} else {
|
||||||
|
// Plain click: single-select (replace).
|
||||||
|
this._selItems = new Map();
|
||||||
|
this._selItems.set(id, itemEl.item);
|
||||||
|
this._lastSelectedId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestUpdate();
|
||||||
|
this._selectionChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user