this._selectionChanged(e)}>
+ ${this.renderTableHeader(columns, showCheckboxColumn)}
+
this._selectionChanged(e)}
+ @click=${(e) => this._handleRowClick(e)}>
${!Array.isArray(items) || items.length === 0 ? html`
@@ -191,17 +201,21 @@ export class TpTable extends DomQuery(LitElement) {
`;
}
- renderTableHeader(columns) {
+ renderTableHeader(columns, showCheckboxColumn = this.selectable) {
return html`
`;
}
+ renderSelectionHeader() {
+ return html` this._checkedChanged(e)}>`;
+ }
+
renderColumnHeader(column, idx) {
if (column.visible !== true && column.required !== true) return null;
@@ -244,6 +258,7 @@ export class TpTable extends DomQuery(LitElement) {
.item=${item}
.selected=${selected}
.selectable=${this.selectable}
+ .selectionMode=${this.selectionMode}
.columns=${columns}>
`;
@@ -256,9 +271,18 @@ export class TpTable extends DomQuery(LitElement) {
sorting: { type: Object },
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 },
columnMoveHandle: { type: String },
- _selItems: { type: Map },
+ _selItems: { state: true },
+ _lastSelectedId: { state: true },
_visibleColumns: { type: Array },
_advFilters: { type: Array },
_advFilterActive: { type: Boolean },
@@ -271,10 +295,12 @@ export class TpTable extends DomQuery(LitElement) {
constructor() {
super();
this._selItems = new Map();
+ this._lastSelectedId = null;
+ this.selectionMode = 'checkbox';
}
updated(changes) {
- if (changes.has('columns')) {
+ if (changes.has('columns') || changes.has('selectionMode') || changes.has('selectable')) {
this._updateColumns();
}
}
@@ -365,7 +391,13 @@ export class TpTable extends DomQuery(LitElement) {
}
_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) {
@@ -558,15 +590,76 @@ export class TpTable extends DomQuery(LitElement) {
}
_updateSelEntries() {
- if (!this.items) {
- this._selItems = new Map();
+ if (!Array.isArray(this.items)) {
+ if (this._selItems.size > 0) {
+ this._selItems = new Map();
+ this._lastSelectedId = null;
+ }
+ return;
}
- for (const selItem of this._selItems.values()) {
- if (this.items.findIndex(item => this.getItemId(item) === this.getItemId(selItem)) > -1) {
+ const validIds = new Set(this.items.map(item => this.getItemId(item)));
+ 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 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();
}
}