@nyaruka/temba-components 0.167.0 → 0.168.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/locales/es.js +3 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +3 -1
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +3 -1
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +2842 -1715
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/Icons.ts +2 -0
- package/src/display/Button.ts +8 -6
- package/src/display/Chat.ts +541 -191
- package/src/display/Dropdown.ts +40 -17
- package/src/display/Label.ts +25 -4
- package/src/display/TembaDate.ts +11 -2
- package/src/display/Tip.ts +157 -18
- package/src/events/eventRenderers.ts +678 -268
- package/src/events.ts +16 -1
- package/src/flow/NodeEditor.ts +82 -9
- package/src/flow/actions/add_contact_urn.ts +1 -0
- package/src/flow/actions/play_audio.ts +1 -0
- package/src/flow/actions/send_broadcast.ts +17 -1
- package/src/flow/actions/send_msg.ts +12 -1
- package/src/flow/actions/set_run_result.ts +1 -0
- package/src/flow/actions/start_session.ts +10 -1
- package/src/flow/nodes/shared-rules.ts +2 -0
- package/src/flow/nodes/split_by_airtime.ts +6 -0
- package/src/flow/nodes/split_by_expression.ts +1 -0
- package/src/flow/nodes/split_by_llm.ts +2 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +3 -1
- package/src/flow/nodes/split_by_random.ts +2 -1
- package/src/flow/nodes/split_by_resthook.ts +4 -1
- package/src/flow/nodes/split_by_webhook.ts +9 -1
- package/src/flow/types.ts +3 -0
- package/src/flow/utils.ts +49 -0
- package/src/form/DatePicker.ts +2 -1
- package/src/layout/Card.ts +9 -0
- package/src/layout/CardLayout.ts +426 -21
- package/src/layout/CardStack.ts +4 -1
- package/src/list/ContactList.ts +24 -17
- package/src/list/ContentList.ts +640 -33
- package/src/list/MsgList.ts +7 -1
- package/src/list/SortableList.ts +5 -1
- package/src/list/TembaMenu.ts +67 -6
- package/src/live/ContactChat.ts +655 -336
- package/src/live/ContactDetails.ts +961 -88
- package/src/live/ContactFieldEditor.ts +46 -11
- package/src/live/ContactFields.ts +57 -16
- package/src/live/ContactStoreElement.ts +15 -8
- package/src/locales/es.ts +3 -2
- package/src/locales/fr.ts +3 -2
- package/src/locales/pt.ts +3 -2
- package/src/simulator/Simulator.ts +28 -0
- package/src/styles/pillVariants.ts +13 -1
- package/web-test-runner.config.mjs +2 -0
- package/xliff/es.xlf +6 -3
- package/xliff/fr.xlf +6 -3
- package/xliff/pt.xlf +6 -3
package/src/list/ContentList.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { property, state } from 'lit/decorators.js';
|
|
|
3
3
|
import { RapidElement } from '../RapidElement';
|
|
4
4
|
import { Icon } from '../Icons';
|
|
5
5
|
import { CustomEventType } from '../interfaces';
|
|
6
|
-
import { getUrl, postUrl } from '../utils';
|
|
6
|
+
import { getUrl, postJSON, postUrl } from '../utils';
|
|
7
7
|
import { designTokens } from '../styles/designTokens';
|
|
8
8
|
|
|
9
9
|
/** A single column in the list. Subclasses typically define a static
|
|
@@ -41,6 +41,12 @@ export interface ContentListColumn {
|
|
|
41
41
|
* columns must be contiguous from the first column; right-pinned
|
|
42
42
|
* columns contiguous to the last. */
|
|
43
43
|
pinned?: boolean | 'left' | 'right';
|
|
44
|
+
/** Whether the column can be resized from its trailing edge. Resizing is
|
|
45
|
+
* opt-in so list types can expose it only where the interaction is useful. */
|
|
46
|
+
resizable?: boolean;
|
|
47
|
+
/** Optional resize-only floor. Unlike minWidth, this does not affect the
|
|
48
|
+
* table's native layout before the user resizes the column. */
|
|
49
|
+
resizeMinWidth?: string;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/** A bulk action surfaced in the toolbar when one or more rows are
|
|
@@ -208,7 +214,9 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
208
214
|
the select-all checkbox — rather than replacing the page header.
|
|
209
215
|
Positioned in the table-frame (not the scrolling table) so it
|
|
210
216
|
stays put horizontally, with an opaque header-tint background to
|
|
211
|
-
hide the labels underneath, above
|
|
217
|
+
hide the labels underneath, above every header layer — pinned
|
|
218
|
+
header cells reach z 5 (see tr.header th.pinned), so the bar
|
|
219
|
+
sits at 6 to cover them too. */
|
|
212
220
|
.bulk-bar {
|
|
213
221
|
position: absolute;
|
|
214
222
|
top: 0;
|
|
@@ -219,7 +227,7 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
219
227
|
left: var(--cl-firstcol-left, 44px);
|
|
220
228
|
right: var(--cl-scrollbar-w, 0px);
|
|
221
229
|
height: var(--cl-header-height, 36px);
|
|
222
|
-
z-index:
|
|
230
|
+
z-index: 6;
|
|
223
231
|
display: flex;
|
|
224
232
|
align-items: center;
|
|
225
233
|
gap: 4px;
|
|
@@ -561,8 +569,10 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
561
569
|
|
|
562
570
|
/* The header row sticks to the top of the scroll frame so the
|
|
563
571
|
column labels stay put while the rows scroll under them.
|
|
564
|
-
z-index 2 lifts it above the body; a pinned header cell
|
|
565
|
-
|
|
572
|
+
z-index 2 lifts it above the body; a pinned header cell needs
|
|
573
|
+
more to also clear the body's pinned column (z-index 1) and the
|
|
574
|
+
resize-handle layers (z 4) — but must stay under the bulk-action
|
|
575
|
+
bar (z 6), which overlays the whole header row on selection. */
|
|
566
576
|
tr.header th {
|
|
567
577
|
height: 36px;
|
|
568
578
|
vertical-align: middle;
|
|
@@ -585,7 +595,7 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
585
595
|
z-index: 2;
|
|
586
596
|
}
|
|
587
597
|
tr.header th.pinned {
|
|
588
|
-
z-index:
|
|
598
|
+
z-index: 5;
|
|
589
599
|
}
|
|
590
600
|
|
|
591
601
|
tr.row td {
|
|
@@ -699,6 +709,64 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
699
709
|
justify-content: center;
|
|
700
710
|
}
|
|
701
711
|
|
|
712
|
+
/* Column resize affordance. The hit target straddles the column
|
|
713
|
+
boundary from the leading edge of the following header, which
|
|
714
|
+
keeps both halves clickable above that cell's stacking context.
|
|
715
|
+
Its rule remains visible so column boundaries are
|
|
716
|
+
discoverable without hovering; interaction strengthens it.
|
|
717
|
+
A real element (rather than a pseudo-element) gives keyboard and
|
|
718
|
+
screen-reader users the same control. */
|
|
719
|
+
.resize-handle {
|
|
720
|
+
position: absolute;
|
|
721
|
+
top: 0;
|
|
722
|
+
left: -5px;
|
|
723
|
+
bottom: 0;
|
|
724
|
+
z-index: 4;
|
|
725
|
+
width: 10px;
|
|
726
|
+
cursor: col-resize;
|
|
727
|
+
touch-action: none;
|
|
728
|
+
outline: none;
|
|
729
|
+
}
|
|
730
|
+
.resize-handle::after {
|
|
731
|
+
content: '';
|
|
732
|
+
position: absolute;
|
|
733
|
+
top: 7px;
|
|
734
|
+
left: 50%;
|
|
735
|
+
bottom: 7px;
|
|
736
|
+
width: 1px;
|
|
737
|
+
border-radius: 1px;
|
|
738
|
+
background: var(--border);
|
|
739
|
+
transform: translateX(-50%);
|
|
740
|
+
transition:
|
|
741
|
+
width 0.12s ease,
|
|
742
|
+
background 0.12s ease;
|
|
743
|
+
}
|
|
744
|
+
:host(:not([column-resizing])) .resize-handle:hover::after,
|
|
745
|
+
:host(:not([column-resizing])) .resize-handle:focus-visible::after,
|
|
746
|
+
.resize-handle.resizing::after {
|
|
747
|
+
width: 3px;
|
|
748
|
+
background: var(--border-strong);
|
|
749
|
+
}
|
|
750
|
+
/* A header hosting the preceding column's handle must paint above
|
|
751
|
+
its neighbour so the target stays centered across the boundary. */
|
|
752
|
+
tr.header th:has(> .resize-handle.leading) {
|
|
753
|
+
z-index: 4;
|
|
754
|
+
}
|
|
755
|
+
/* Keep the final column's target inside the table so it doesn't
|
|
756
|
+
create a few pixels of horizontal overflow. Its rule stays in
|
|
757
|
+
the target's center, just inboard of the table edge. */
|
|
758
|
+
.resize-handle.trailing {
|
|
759
|
+
left: auto;
|
|
760
|
+
right: -5px;
|
|
761
|
+
}
|
|
762
|
+
.resize-handle.trailing.outer {
|
|
763
|
+
right: 0;
|
|
764
|
+
}
|
|
765
|
+
:host([column-resizing]),
|
|
766
|
+
:host([column-resizing]) * {
|
|
767
|
+
cursor: col-resize !important;
|
|
768
|
+
}
|
|
769
|
+
|
|
702
770
|
/* Pinned columns stay fixed against their edge while the rest
|
|
703
771
|
of the table scrolls under them. The frozen-region look —
|
|
704
772
|
the tint and the divider — only kicks in once the table
|
|
@@ -725,31 +793,17 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
725
793
|
.table-frame.overflowing tr.row.selected td.pinned {
|
|
726
794
|
background: var(--cl-selected);
|
|
727
795
|
}
|
|
728
|
-
/*
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
.table-frame.overflowing th.pin-last,
|
|
796
|
+
/* Body cells retain a subtle vertical rule where the pinned section
|
|
797
|
+
ends. Header boundaries are already represented by resize
|
|
798
|
+
separators, so they only keep the standard bottom rule. */
|
|
732
799
|
.table-frame.overflowing td.pin-last {
|
|
733
800
|
box-shadow: inset -1px 0 0 0 var(--border);
|
|
734
801
|
}
|
|
735
802
|
/* Mirror of the rule for the right-pinned group — the divider
|
|
736
803
|
sits on the inboard (left) edge of its first cell. */
|
|
737
|
-
.table-frame.overflowing th.pin-first,
|
|
738
804
|
.table-frame.overflowing td.pin-first {
|
|
739
805
|
box-shadow: inset 1px 0 0 0 var(--border);
|
|
740
806
|
}
|
|
741
|
-
/* A pinned header cell keeps the header's bottom-border shadow
|
|
742
|
-
alongside the pin-edge rule. */
|
|
743
|
-
.table-frame.overflowing tr.header th.pin-last {
|
|
744
|
-
box-shadow:
|
|
745
|
-
inset -1px 0 0 0 var(--border),
|
|
746
|
-
inset 0 -1px 0 0 var(--border);
|
|
747
|
-
}
|
|
748
|
-
.table-frame.overflowing tr.header th.pin-first {
|
|
749
|
-
box-shadow:
|
|
750
|
-
inset 1px 0 0 0 var(--border),
|
|
751
|
-
inset 0 -1px 0 0 var(--border);
|
|
752
|
-
}
|
|
753
807
|
/* Once scrolled, the frozen edge casts the same soft scroll
|
|
754
808
|
shadow as the sticky header — a gradient (matching th::after
|
|
755
809
|
and .scroll-shadow) drawn just outside the pinned edge, over
|
|
@@ -1261,6 +1315,22 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
1261
1315
|
@property({ type: String, attribute: 'history-state-key' })
|
|
1262
1316
|
historyStateKey = '';
|
|
1263
1317
|
|
|
1318
|
+
/** Saved widths for every list, keyed first by the list's
|
|
1319
|
+
* {@link historyStateKey}, then by column key. The host can seed this
|
|
1320
|
+
* from user settings so each list remembers its own layout. This is
|
|
1321
|
+
* the same `list_columns` value that {@link settingsEndpoint} saves —
|
|
1322
|
+
* the attribute is the read side of that round trip. */
|
|
1323
|
+
@property({ type: Object, attribute: 'column-width-settings' })
|
|
1324
|
+
columnWidthSettings: Record<string, Record<string, number>> = {};
|
|
1325
|
+
|
|
1326
|
+
/** Endpoint that accepts user-setting updates. Width persistence is
|
|
1327
|
+
* disabled when this or {@link historyStateKey} is unset. */
|
|
1328
|
+
@property({ type: String, attribute: 'settings-endpoint' })
|
|
1329
|
+
settingsEndpoint = '';
|
|
1330
|
+
|
|
1331
|
+
/** Debounce window for settings saves. Public so tests can shorten it. */
|
|
1332
|
+
saveDelay = 500;
|
|
1333
|
+
|
|
1264
1334
|
/** Placeholder for the search input. */
|
|
1265
1335
|
@property({ type: String })
|
|
1266
1336
|
searchPlaceholder = 'Search';
|
|
@@ -1403,6 +1473,26 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
1403
1473
|
private popstateHandler: () => void;
|
|
1404
1474
|
private resizeHandler: () => void;
|
|
1405
1475
|
|
|
1476
|
+
/** Active pointer-driven column resize. Window listeners keep the drag
|
|
1477
|
+
* alive when the pointer outruns the narrow header handle. */
|
|
1478
|
+
private columnResize:
|
|
1479
|
+
| {
|
|
1480
|
+
key: string;
|
|
1481
|
+
startX: number;
|
|
1482
|
+
startWidth: number;
|
|
1483
|
+
minWidth: number;
|
|
1484
|
+
nativeFloor: boolean;
|
|
1485
|
+
initialSavedWidth: number | undefined;
|
|
1486
|
+
changed: boolean;
|
|
1487
|
+
handle: HTMLElement;
|
|
1488
|
+
}
|
|
1489
|
+
| undefined;
|
|
1490
|
+
private previousBodyUserSelect = '';
|
|
1491
|
+
private columnWidths: Record<string, number> = {};
|
|
1492
|
+
private columnWidthSaveTimeout: ReturnType<typeof setTimeout> = null;
|
|
1493
|
+
private suppressHeaderClick = false;
|
|
1494
|
+
private headerClickSuppressionTimeout: ReturnType<typeof setTimeout> = null;
|
|
1495
|
+
|
|
1406
1496
|
/** Pin index assigned to each left-pinned column / leading cell,
|
|
1407
1497
|
* used to resolve its sticky `left`. Recomputed each render. */
|
|
1408
1498
|
private pinIndexByColumn = new Map<ContentListColumn, number>();
|
|
@@ -1427,6 +1517,42 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
1427
1517
|
super();
|
|
1428
1518
|
}
|
|
1429
1519
|
|
|
1520
|
+
protected willUpdate(changes: PropertyValues): void {
|
|
1521
|
+
super.willUpdate(changes);
|
|
1522
|
+
if (
|
|
1523
|
+
changes.has('columnWidthSettings') ||
|
|
1524
|
+
changes.has('historyStateKey') ||
|
|
1525
|
+
changes.has('columns')
|
|
1526
|
+
) {
|
|
1527
|
+
const saved = this.columnWidthSettings?.[this.historyStateKey];
|
|
1528
|
+
const widths: Record<string, number> = {};
|
|
1529
|
+
if (saved && typeof saved === 'object') {
|
|
1530
|
+
Object.entries(saved).forEach(([key, value]) => {
|
|
1531
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1532
|
+
const column = this.columns.find(
|
|
1533
|
+
(candidate) => candidate.key === key
|
|
1534
|
+
);
|
|
1535
|
+
widths[key] = this.clampColumnWidth(
|
|
1536
|
+
value,
|
|
1537
|
+
column
|
|
1538
|
+
? this.minimumColumnWidth(column)
|
|
1539
|
+
: ContentList.MIN_COLUMN_WIDTH
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
// A columns rebuild (custom fields arriving, anon flipping) must
|
|
1545
|
+
// not discard widths dragged this session that haven't round-tripped
|
|
1546
|
+
// through the settings attribute — only a new settings payload or a
|
|
1547
|
+
// different list replaces them outright.
|
|
1548
|
+
const preserved =
|
|
1549
|
+
changes.has('columnWidthSettings') || changes.has('historyStateKey')
|
|
1550
|
+
? {}
|
|
1551
|
+
: this.columnWidths;
|
|
1552
|
+
this.columnWidths = { ...widths, ...preserved };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1430
1556
|
public connectedCallback(): void {
|
|
1431
1557
|
super.connectedCallback();
|
|
1432
1558
|
if (this.urlState) {
|
|
@@ -1483,6 +1609,16 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
1483
1609
|
if (this.resizeHandler) {
|
|
1484
1610
|
window.removeEventListener('resize', this.resizeHandler);
|
|
1485
1611
|
}
|
|
1612
|
+
this.stopColumnResize();
|
|
1613
|
+
if (this.columnWidthSaveTimeout) {
|
|
1614
|
+
clearTimeout(this.columnWidthSaveTimeout);
|
|
1615
|
+
this.columnWidthSaveTimeout = null;
|
|
1616
|
+
this.saveColumnWidths();
|
|
1617
|
+
}
|
|
1618
|
+
if (this.headerClickSuppressionTimeout) {
|
|
1619
|
+
clearTimeout(this.headerClickSuppressionTimeout);
|
|
1620
|
+
this.headerClickSuppressionTimeout = null;
|
|
1621
|
+
}
|
|
1486
1622
|
if (this.bulkCollapseFrame) {
|
|
1487
1623
|
cancelAnimationFrame(this.bulkCollapseFrame);
|
|
1488
1624
|
this.bulkCollapseFrame = 0;
|
|
@@ -1947,6 +2083,23 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
1947
2083
|
this.writeUrlState();
|
|
1948
2084
|
}
|
|
1949
2085
|
|
|
2086
|
+
private handleColumnHeaderClick(
|
|
2087
|
+
event: MouseEvent,
|
|
2088
|
+
column: ContentListColumn
|
|
2089
|
+
): void {
|
|
2090
|
+
// Releasing a resize over the header label can synthesize a click on
|
|
2091
|
+
// the shared <th>. Consume that click so resizing never changes sort.
|
|
2092
|
+
if (this.suppressHeaderClick) {
|
|
2093
|
+
event.preventDefault();
|
|
2094
|
+
event.stopPropagation();
|
|
2095
|
+
this.suppressHeaderClick = false;
|
|
2096
|
+
clearTimeout(this.headerClickSuppressionTimeout);
|
|
2097
|
+
this.headerClickSuppressionTimeout = null;
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
this.handleSortClick(column);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1950
2103
|
private handleRowClick(item: T, event: MouseEvent): void {
|
|
1951
2104
|
// Ignore clicks originating from the checkbox cell so toggling
|
|
1952
2105
|
// selection doesn't double as navigation.
|
|
@@ -2577,10 +2730,12 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
2577
2730
|
* the column simply sizes to its content (header label or widest
|
|
2578
2731
|
* value) via the table's auto layout. */
|
|
2579
2732
|
private cellWidthStyle(column: ContentListColumn): string {
|
|
2733
|
+
const savedWidth = this.savedColumnWidth(column);
|
|
2580
2734
|
// Under fixed layout the column widths are set on the cells
|
|
2581
2735
|
// themselves (see {@link renderHeaderCell}); the inner wrapper
|
|
2582
2736
|
// just fills its cell and ellipsis-truncates against it.
|
|
2583
2737
|
if (this.fixedLayout) return '';
|
|
2738
|
+
if (savedWidth != null) return `width: ${savedWidth}px;`;
|
|
2584
2739
|
if (column.width) return `width: ${column.width};`;
|
|
2585
2740
|
const parts: string[] = [];
|
|
2586
2741
|
if (column.minWidth) parts.push(`min-width: ${column.minWidth};`);
|
|
@@ -2590,6 +2745,391 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
2590
2745
|
return parts.join(' ');
|
|
2591
2746
|
}
|
|
2592
2747
|
|
|
2748
|
+
/** Numeric pixel value for a CSS width used by the list's column
|
|
2749
|
+
* contracts. Resizable columns use pixel widths because pointer movement
|
|
2750
|
+
* is measured in pixels; non-pixel bounds fall back to the defaults. */
|
|
2751
|
+
private columnWidthPixels(
|
|
2752
|
+
value: string | undefined,
|
|
2753
|
+
fallback: number
|
|
2754
|
+
): number {
|
|
2755
|
+
if (!value || !value.trim().endsWith('px')) return fallback;
|
|
2756
|
+
const parsed = Number.parseFloat(value);
|
|
2757
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
private static readonly MIN_COLUMN_WIDTH = 80;
|
|
2761
|
+
private static readonly MAX_COLUMN_WIDTH = 600;
|
|
2762
|
+
|
|
2763
|
+
private clampColumnWidth(
|
|
2764
|
+
width: number,
|
|
2765
|
+
minWidth = ContentList.MIN_COLUMN_WIDTH
|
|
2766
|
+
): number {
|
|
2767
|
+
const floor = Math.min(minWidth, ContentList.MAX_COLUMN_WIDTH);
|
|
2768
|
+
return Math.round(
|
|
2769
|
+
Math.min(Math.max(width, floor), ContentList.MAX_COLUMN_WIDTH)
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
private isColumnResizable(column: ContentListColumn): boolean {
|
|
2774
|
+
return column.resizable === true && !column.grow;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
private savedColumnWidth(column: ContentListColumn): number | undefined {
|
|
2778
|
+
return this.isColumnResizable(column)
|
|
2779
|
+
? this.columnWidths[column.key]
|
|
2780
|
+
: undefined;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
private effectiveColumnWidth(column: ContentListColumn): number {
|
|
2784
|
+
return (
|
|
2785
|
+
this.savedColumnWidth(column) ??
|
|
2786
|
+
this.columnWidthPixels(column.width, ContentList.MIN_COLUMN_WIDTH)
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
private minimumColumnWidth(column: ContentListColumn): number {
|
|
2791
|
+
return this.columnWidthPixels(
|
|
2792
|
+
column.resizeMinWidth ?? column.minWidth,
|
|
2793
|
+
ContentList.MIN_COLUMN_WIDTH
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
private maximumColumnWidth(column: ContentListColumn): number {
|
|
2798
|
+
return this.columnWidthPixels(
|
|
2799
|
+
column.maxWidth,
|
|
2800
|
+
ContentList.MAX_COLUMN_WIDTH
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
private hasPrescribedColumnWidth(column: ContentListColumn): boolean {
|
|
2805
|
+
return this.savedColumnWidth(column) != null || column.width != null;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
private hasColumnWidthSlackSink(): boolean {
|
|
2809
|
+
return (
|
|
2810
|
+
this.columns.some((column) => column.grow) || this.spacerAfterIndex >= 0
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
private usesNativeWidthFloor(
|
|
2815
|
+
column: ContentListColumn,
|
|
2816
|
+
renderedWidth: number,
|
|
2817
|
+
prescribedWidth: number
|
|
2818
|
+
): boolean {
|
|
2819
|
+
return (
|
|
2820
|
+
!this.hasColumnWidthSlackSink() &&
|
|
2821
|
+
this.hasPrescribedColumnWidth(column) &&
|
|
2822
|
+
renderedWidth > prescribedWidth + 1
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/** Store a user's width separately from the static column definition.
|
|
2827
|
+
* This keeps overrides intact when a list rebuilds dynamic columns. */
|
|
2828
|
+
private setColumnWidth(
|
|
2829
|
+
key: string,
|
|
2830
|
+
width: number,
|
|
2831
|
+
minWidth = ContentList.MIN_COLUMN_WIDTH
|
|
2832
|
+
): number {
|
|
2833
|
+
const nextWidth = this.clampColumnWidth(width, minWidth);
|
|
2834
|
+
if (this.columnWidths[key] === nextWidth) return nextWidth;
|
|
2835
|
+
this.columnWidths = { ...this.columnWidths, [key]: nextWidth };
|
|
2836
|
+
this.requestUpdate();
|
|
2837
|
+
return nextWidth;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
private restoreColumnWidth(key: string, width: number | undefined): void {
|
|
2841
|
+
if (this.columnWidths[key] === width) return;
|
|
2842
|
+
const widths = { ...this.columnWidths };
|
|
2843
|
+
if (width == null) delete widths[key];
|
|
2844
|
+
else widths[key] = width;
|
|
2845
|
+
this.columnWidths = widths;
|
|
2846
|
+
this.requestUpdate();
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/** The width contract is carried by the inner wrapper, while the resize
|
|
2850
|
+
* handle sits on the outer table-cell boundary. Auto table layout can
|
|
2851
|
+
* distribute spare room to that cell, so derive the matching content
|
|
2852
|
+
* width from the rendered cell rather than trusting its prescribed width. */
|
|
2853
|
+
private renderedColumnWidth(
|
|
2854
|
+
header: HTMLElement | null,
|
|
2855
|
+
column: ContentListColumn
|
|
2856
|
+
): number {
|
|
2857
|
+
if (!header) return this.effectiveColumnWidth(column);
|
|
2858
|
+
const style = getComputedStyle(header);
|
|
2859
|
+
const padding =
|
|
2860
|
+
Number.parseFloat(style.paddingLeft) +
|
|
2861
|
+
Number.parseFloat(style.paddingRight);
|
|
2862
|
+
const width = header.getBoundingClientRect().width - padding;
|
|
2863
|
+
return Number.isFinite(width) && width > 0
|
|
2864
|
+
? width
|
|
2865
|
+
: this.effectiveColumnWidth(column);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
private scheduleColumnWidthSave(): void {
|
|
2869
|
+
if (!this.settingsEndpoint || !this.historyStateKey) return;
|
|
2870
|
+
clearTimeout(this.columnWidthSaveTimeout);
|
|
2871
|
+
this.columnWidthSaveTimeout = setTimeout(() => {
|
|
2872
|
+
this.columnWidthSaveTimeout = null;
|
|
2873
|
+
this.saveColumnWidths();
|
|
2874
|
+
}, this.saveDelay);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
private saveColumnWidths(): void {
|
|
2878
|
+
if (!this.settingsEndpoint || !this.historyStateKey) return;
|
|
2879
|
+
// This posts only the current list's widths, nested under the same
|
|
2880
|
+
// historyStateKey used to read them back out of columnWidthSettings.
|
|
2881
|
+
// The endpoint must merge list_columns at both the view and column
|
|
2882
|
+
// level (as temba's user-settings endpoint does) — replacing it
|
|
2883
|
+
// wholesale would wipe the widths saved for every other list.
|
|
2884
|
+
postJSON(this.settingsEndpoint, {
|
|
2885
|
+
list_columns: {
|
|
2886
|
+
[this.historyStateKey]: this.columnWidths
|
|
2887
|
+
}
|
|
2888
|
+
}).catch(() => {
|
|
2889
|
+
// Width persistence is a convenience; a failed save should not
|
|
2890
|
+
// interrupt list interaction. A later resize will retry it.
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
private startColumnResize(
|
|
2895
|
+
event: PointerEvent,
|
|
2896
|
+
column: ContentListColumn
|
|
2897
|
+
): void {
|
|
2898
|
+
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
2899
|
+
// A second pointer should replace, not overlap, an active drag and
|
|
2900
|
+
// must preserve the body's original selection style for cleanup.
|
|
2901
|
+
this.stopColumnResize();
|
|
2902
|
+
event.preventDefault();
|
|
2903
|
+
event.stopPropagation();
|
|
2904
|
+
const handle = event.currentTarget as HTMLElement;
|
|
2905
|
+
// Capture the pointer so the drag survives excursions outside the
|
|
2906
|
+
// window and its release retargets to the handle — the synthesized
|
|
2907
|
+
// click then lands on the handle (which swallows clicks) instead of
|
|
2908
|
+
// whichever header happens to be under the pointer.
|
|
2909
|
+
try {
|
|
2910
|
+
handle.setPointerCapture(event.pointerId);
|
|
2911
|
+
} catch {
|
|
2912
|
+
// synthetic pointers (tests) have no active pointer to capture
|
|
2913
|
+
}
|
|
2914
|
+
const header = this.resizeHeaderForHandle(handle);
|
|
2915
|
+
const prescribedWidth = this.effectiveColumnWidth(column);
|
|
2916
|
+
const startWidth = this.renderedColumnWidth(header, column);
|
|
2917
|
+
const resizeFloor = this.minimumRenderedColumnWidth(header, column);
|
|
2918
|
+
const nativeFloor = this.usesNativeWidthFloor(
|
|
2919
|
+
column,
|
|
2920
|
+
startWidth,
|
|
2921
|
+
prescribedWidth
|
|
2922
|
+
);
|
|
2923
|
+
this.columnResize = {
|
|
2924
|
+
key: column.key,
|
|
2925
|
+
startX: event.clientX,
|
|
2926
|
+
startWidth,
|
|
2927
|
+
minWidth: nativeFloor ? startWidth : resizeFloor,
|
|
2928
|
+
nativeFloor,
|
|
2929
|
+
initialSavedWidth: this.columnWidths[column.key],
|
|
2930
|
+
changed: false,
|
|
2931
|
+
handle
|
|
2932
|
+
};
|
|
2933
|
+
handle.setAttribute(
|
|
2934
|
+
'aria-valuemin',
|
|
2935
|
+
`${Math.round(nativeFloor ? startWidth : resizeFloor)}`
|
|
2936
|
+
);
|
|
2937
|
+
handle.classList.add('resizing');
|
|
2938
|
+
this.previousBodyUserSelect = document.body.style.userSelect;
|
|
2939
|
+
document.body.style.userSelect = 'none';
|
|
2940
|
+
this.toggleAttribute('column-resizing', true);
|
|
2941
|
+
window.addEventListener('pointermove', this.handleColumnResizeMove);
|
|
2942
|
+
window.addEventListener('pointerup', this.stopColumnResize);
|
|
2943
|
+
window.addEventListener('pointercancel', this.stopColumnResize);
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
private handleColumnResizeMove = (event: PointerEvent): void => {
|
|
2947
|
+
if (!this.columnResize) return;
|
|
2948
|
+
event.preventDefault();
|
|
2949
|
+
const resize = this.columnResize;
|
|
2950
|
+
const requestedWidth = resize.startWidth + event.clientX - resize.startX;
|
|
2951
|
+
if (
|
|
2952
|
+
resize.nativeFloor &&
|
|
2953
|
+
(requestedWidth <= resize.minWidth ||
|
|
2954
|
+
resize.minWidth >= ContentList.MAX_COLUMN_WIDTH)
|
|
2955
|
+
) {
|
|
2956
|
+
// The table is already rendering wider than the CSS contract. Treat
|
|
2957
|
+
// that native allocation as the floor without writing it back: doing
|
|
2958
|
+
// so would feed the spare width into the next auto-layout pass and
|
|
2959
|
+
// move the boundary even though the user dragged left.
|
|
2960
|
+
this.restoreColumnWidth(resize.key, resize.initialSavedWidth);
|
|
2961
|
+
} else {
|
|
2962
|
+
this.setColumnWidth(resize.key, requestedWidth, resize.minWidth);
|
|
2963
|
+
}
|
|
2964
|
+
resize.changed = this.columnWidths[resize.key] !== resize.initialSavedWidth;
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
private stopColumnResize = (event?: Event): void => {
|
|
2968
|
+
if (!this.columnResize) return;
|
|
2969
|
+
const { changed, handle } = this.columnResize;
|
|
2970
|
+
handle.classList.remove('resizing');
|
|
2971
|
+
this.columnResize = undefined;
|
|
2972
|
+
document.body.style.userSelect = this.previousBodyUserSelect;
|
|
2973
|
+
this.toggleAttribute('column-resizing', false);
|
|
2974
|
+
window.removeEventListener('pointermove', this.handleColumnResizeMove);
|
|
2975
|
+
window.removeEventListener('pointerup', this.stopColumnResize);
|
|
2976
|
+
window.removeEventListener('pointercancel', this.stopColumnResize);
|
|
2977
|
+
if (event?.type === 'pointerup') {
|
|
2978
|
+
this.suppressHeaderClick = true;
|
|
2979
|
+
clearTimeout(this.headerClickSuppressionTimeout);
|
|
2980
|
+
this.headerClickSuppressionTimeout = setTimeout(() => {
|
|
2981
|
+
this.suppressHeaderClick = false;
|
|
2982
|
+
this.headerClickSuppressionTimeout = null;
|
|
2983
|
+
}, 0);
|
|
2984
|
+
}
|
|
2985
|
+
if (changed) this.scheduleColumnWidthSave();
|
|
2986
|
+
};
|
|
2987
|
+
|
|
2988
|
+
/** Arrow keys resize in 10px steps (25px with Shift), providing an
|
|
2989
|
+
* accessible equivalent to dragging the separator. */
|
|
2990
|
+
private handleColumnResizeKeydown(
|
|
2991
|
+
event: KeyboardEvent,
|
|
2992
|
+
column: ContentListColumn
|
|
2993
|
+
): void {
|
|
2994
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
|
2995
|
+
event.preventDefault();
|
|
2996
|
+
event.stopPropagation();
|
|
2997
|
+
const handle = event.currentTarget as HTMLElement;
|
|
2998
|
+
const header = this.resizeHeaderForHandle(handle);
|
|
2999
|
+
const prescribedWidth = this.effectiveColumnWidth(column);
|
|
3000
|
+
const currentWidth = this.renderedColumnWidth(header, column);
|
|
3001
|
+
const resizeFloor = this.minimumRenderedColumnWidth(header, column);
|
|
3002
|
+
const nativeFloor = this.usesNativeWidthFloor(
|
|
3003
|
+
column,
|
|
3004
|
+
currentWidth,
|
|
3005
|
+
prescribedWidth
|
|
3006
|
+
);
|
|
3007
|
+
const direction = event.key === 'ArrowRight' ? 1 : -1;
|
|
3008
|
+
if (
|
|
3009
|
+
nativeFloor &&
|
|
3010
|
+
(direction < 0 || currentWidth >= ContentList.MAX_COLUMN_WIDTH)
|
|
3011
|
+
) {
|
|
3012
|
+
handle.setAttribute('aria-valuenow', `${Math.round(currentWidth)}`);
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
const width = this.setColumnWidth(
|
|
3016
|
+
column.key,
|
|
3017
|
+
currentWidth + direction * (event.shiftKey ? 25 : 10),
|
|
3018
|
+
nativeFloor ? currentWidth : resizeFloor
|
|
3019
|
+
);
|
|
3020
|
+
handle.setAttribute(
|
|
3021
|
+
'aria-valuemin',
|
|
3022
|
+
`${Math.round(nativeFloor ? currentWidth : resizeFloor)}`
|
|
3023
|
+
);
|
|
3024
|
+
handle.setAttribute('aria-valuenow', `${width}`);
|
|
3025
|
+
this.scheduleColumnWidthSave();
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
/** An auto-sized column has no saved or prescribed width at render
|
|
3029
|
+
* time, so the template's aria-valuenow is only a floor. Measure the
|
|
3030
|
+
* real rendered width when the separator gains focus so screen
|
|
3031
|
+
* readers announce the width the user actually sees. */
|
|
3032
|
+
private handleColumnResizeFocus(
|
|
3033
|
+
event: FocusEvent,
|
|
3034
|
+
column: ContentListColumn
|
|
3035
|
+
): void {
|
|
3036
|
+
const handle = event.currentTarget as HTMLElement;
|
|
3037
|
+
const header = this.resizeHeaderForHandle(handle);
|
|
3038
|
+
handle.setAttribute(
|
|
3039
|
+
'aria-valuenow',
|
|
3040
|
+
`${Math.round(this.renderedColumnWidth(header, column))}`
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
/** Fit the column to the widest rendered header/body value. Cell
|
|
3045
|
+
* contents are measured at max-content width without painting a layout
|
|
3046
|
+
* change; the configured column bounds keep outliers manageable. */
|
|
3047
|
+
private intrinsicWidth(element: HTMLElement): number {
|
|
3048
|
+
const previousStyle = element.getAttribute('style');
|
|
3049
|
+
element.style.width = 'max-content';
|
|
3050
|
+
element.style.minWidth = '0';
|
|
3051
|
+
element.style.maxWidth = 'none';
|
|
3052
|
+
element.style.position = 'absolute';
|
|
3053
|
+
const width = element.getBoundingClientRect().width;
|
|
3054
|
+
if (previousStyle == null) element.removeAttribute('style');
|
|
3055
|
+
else element.setAttribute('style', previousStyle);
|
|
3056
|
+
return width;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
private minimumRenderedColumnWidth(
|
|
3060
|
+
header: HTMLElement | null,
|
|
3061
|
+
column: ContentListColumn
|
|
3062
|
+
): number {
|
|
3063
|
+
const configuredFloor = this.minimumColumnWidth(column);
|
|
3064
|
+
const headerInner = header?.querySelector<HTMLElement>('.head-inner');
|
|
3065
|
+
return headerInner
|
|
3066
|
+
? Math.max(configuredFloor, this.headerContentWidth(headerInner))
|
|
3067
|
+
: configuredFloor;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
private headerContentWidth(headerInner: HTMLElement): number {
|
|
3071
|
+
return Math.ceil(
|
|
3072
|
+
Array.from(headerInner.children).reduce(
|
|
3073
|
+
(width, child) =>
|
|
3074
|
+
width + (child as HTMLElement).getBoundingClientRect().width,
|
|
3075
|
+
0
|
|
3076
|
+
)
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
private handleColumnAutoFit(
|
|
3081
|
+
event: MouseEvent,
|
|
3082
|
+
column: ContentListColumn
|
|
3083
|
+
): void {
|
|
3084
|
+
event.preventDefault();
|
|
3085
|
+
event.stopPropagation();
|
|
3086
|
+
const handle = event.currentTarget as HTMLElement;
|
|
3087
|
+
const header = this.resizeHeaderForHandle(handle);
|
|
3088
|
+
if (!header) return;
|
|
3089
|
+
|
|
3090
|
+
const widths: number[] = [];
|
|
3091
|
+
const headerInner = header.querySelector<HTMLElement>('.head-inner');
|
|
3092
|
+
if (headerInner) widths.push(this.headerContentWidth(headerInner));
|
|
3093
|
+
|
|
3094
|
+
const columnIndex = (header as HTMLTableCellElement).cellIndex;
|
|
3095
|
+
this.shadowRoot
|
|
3096
|
+
?.querySelectorAll<HTMLTableRowElement>('tr.row')
|
|
3097
|
+
.forEach((row) => {
|
|
3098
|
+
const cell = row.cells.item(columnIndex);
|
|
3099
|
+
const inner = cell?.querySelector<HTMLElement>('.cell-inner');
|
|
3100
|
+
if (!inner) return;
|
|
3101
|
+
let contentWidth = this.intrinsicWidth(inner);
|
|
3102
|
+
const icon = cell.querySelector<HTMLElement>('.lead-icon');
|
|
3103
|
+
if (icon) {
|
|
3104
|
+
const iconStyle = getComputedStyle(icon);
|
|
3105
|
+
contentWidth +=
|
|
3106
|
+
icon.offsetWidth + Number.parseFloat(iconStyle.marginRight || '0');
|
|
3107
|
+
}
|
|
3108
|
+
widths.push(contentWidth);
|
|
3109
|
+
});
|
|
3110
|
+
|
|
3111
|
+
const minWidth = this.minimumRenderedColumnWidth(header, column);
|
|
3112
|
+
const maxWidth = Math.max(this.maximumColumnWidth(column), minWidth);
|
|
3113
|
+
const contentWidth = widths.length ? Math.max(...widths) : minWidth;
|
|
3114
|
+
const width = this.setColumnWidth(
|
|
3115
|
+
column.key,
|
|
3116
|
+
Math.min(Math.max(contentWidth, minWidth), maxWidth),
|
|
3117
|
+
minWidth
|
|
3118
|
+
);
|
|
3119
|
+
handle.setAttribute('aria-valuenow', `${width}`);
|
|
3120
|
+
this.scheduleColumnWidthSave();
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/** Leading handles live in the next header so that header owns both
|
|
3124
|
+
* clickable halves of the boundary. Pinned and outer-edge handles stay
|
|
3125
|
+
* in their own header so they follow that sticky/table boundary. */
|
|
3126
|
+
private resizeHeaderForHandle(handle: HTMLElement): HTMLElement | null {
|
|
3127
|
+
const host = handle.parentElement;
|
|
3128
|
+
return handle.classList.contains('leading')
|
|
3129
|
+
? (host?.previousElementSibling as HTMLElement | null)
|
|
3130
|
+
: host;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
2593
3133
|
/** Measure the header's pinned cells and publish a cumulative
|
|
2594
3134
|
* `left` offset per pin index as a CSS var on the host. Pinned
|
|
2595
3135
|
* cells (header + body) read these via {@link pinStyle}. Pinned
|
|
@@ -2725,18 +3265,73 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
2725
3265
|
</th>
|
|
2726
3266
|
`
|
|
2727
3267
|
: null}
|
|
2728
|
-
${this.columns.map((
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3268
|
+
${this.columns.map((column, index) => {
|
|
3269
|
+
const previousColumn = this.columns[index - 1];
|
|
3270
|
+
const leadingResizeColumn =
|
|
3271
|
+
previousColumn &&
|
|
3272
|
+
!this.isLeftPinned(previousColumn) &&
|
|
3273
|
+
!this.isRightPinned(previousColumn) &&
|
|
3274
|
+
this.spacerAfterIndex !== index - 1
|
|
3275
|
+
? previousColumn
|
|
3276
|
+
: undefined;
|
|
3277
|
+
const trailingResize =
|
|
3278
|
+
index === this.columns.length - 1 ||
|
|
3279
|
+
this.isLeftPinned(column) ||
|
|
3280
|
+
this.isRightPinned(column);
|
|
3281
|
+
const outerResize = index === this.columns.length - 1;
|
|
3282
|
+
return html`${this.renderHeaderCell(
|
|
3283
|
+
column,
|
|
3284
|
+
leadingResizeColumn,
|
|
3285
|
+
trailingResize,
|
|
3286
|
+
outerResize
|
|
3287
|
+
)}${index === this.spacerAfterIndex
|
|
3288
|
+
? html`<th class="spacer">
|
|
3289
|
+
${!this.isLeftPinned(column) && !this.isRightPinned(column)
|
|
3290
|
+
? this.renderResizeHandle(column, true)
|
|
3291
|
+
: null}
|
|
3292
|
+
</th>`
|
|
3293
|
+
: null}`;
|
|
3294
|
+
})}
|
|
2734
3295
|
</tr>
|
|
2735
3296
|
</thead>
|
|
2736
3297
|
`;
|
|
2737
3298
|
}
|
|
2738
3299
|
|
|
2739
|
-
private
|
|
3300
|
+
private renderResizeHandle(
|
|
3301
|
+
column: ContentListColumn | undefined,
|
|
3302
|
+
leading: boolean,
|
|
3303
|
+
outer = false
|
|
3304
|
+
): TemplateResult | null {
|
|
3305
|
+
if (!column || !this.isColumnResizable(column)) return null;
|
|
3306
|
+
return html`<span
|
|
3307
|
+
class="resize-handle ${leading ? 'leading' : 'trailing'} ${outer
|
|
3308
|
+
? 'outer'
|
|
3309
|
+
: ''} ${this.columnResize?.key === column.key ? 'resizing' : ''}"
|
|
3310
|
+
role="separator"
|
|
3311
|
+
aria-orientation="vertical"
|
|
3312
|
+
aria-label="Resize ${column.label ?? column.key} column"
|
|
3313
|
+
aria-valuemin=${this.minimumColumnWidth(column)}
|
|
3314
|
+
aria-valuemax=${ContentList.MAX_COLUMN_WIDTH}
|
|
3315
|
+
aria-valuenow=${this.effectiveColumnWidth(column)}
|
|
3316
|
+
tabindex="0"
|
|
3317
|
+
@pointerdown=${(event: PointerEvent) =>
|
|
3318
|
+
this.startColumnResize(event, column)}
|
|
3319
|
+
@keydown=${(event: KeyboardEvent) =>
|
|
3320
|
+
this.handleColumnResizeKeydown(event, column)}
|
|
3321
|
+
@focus=${(event: FocusEvent) =>
|
|
3322
|
+
this.handleColumnResizeFocus(event, column)}
|
|
3323
|
+
@click=${(event: MouseEvent) => event.stopPropagation()}
|
|
3324
|
+
@dblclick=${(event: MouseEvent) =>
|
|
3325
|
+
this.handleColumnAutoFit(event, column)}
|
|
3326
|
+
></span>`;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
private renderHeaderCell(
|
|
3330
|
+
column: ContentListColumn,
|
|
3331
|
+
leadingResizeColumn?: ContentListColumn,
|
|
3332
|
+
trailingResize = false,
|
|
3333
|
+
outerResize = false
|
|
3334
|
+
): TemplateResult {
|
|
2740
3335
|
const active = this.sort === column.key || this.sort === '-' + column.key;
|
|
2741
3336
|
const desc = this.sort === '-' + column.key;
|
|
2742
3337
|
const cls = `head-cell ${column.align || ''} ${
|
|
@@ -2765,19 +3360,31 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
2765
3360
|
// Under fixed layout the header row drives the column widths, so
|
|
2766
3361
|
// each `width`-set column carries its width on the cell itself;
|
|
2767
3362
|
// the grow column is left unsized to claim the remainder.
|
|
2768
|
-
const
|
|
2769
|
-
|
|
3363
|
+
const savedWidth = this.savedColumnWidth(column);
|
|
3364
|
+
const widthStyle = this.fixedLayout
|
|
3365
|
+
? savedWidth != null
|
|
3366
|
+
? `width: ${savedWidth}px;`
|
|
3367
|
+
: column.width
|
|
3368
|
+
? `width: ${column.width};`
|
|
3369
|
+
: ''
|
|
3370
|
+
: '';
|
|
2770
3371
|
return html`
|
|
2771
3372
|
<th
|
|
2772
3373
|
class=${cls}
|
|
2773
3374
|
style="${this.columnPinStyle(column)} ${widthStyle}"
|
|
2774
|
-
@click=${column.sortable
|
|
3375
|
+
@click=${column.sortable
|
|
3376
|
+
? (event: MouseEvent) => this.handleColumnHeaderClick(event, column)
|
|
3377
|
+
: null}
|
|
2775
3378
|
>
|
|
3379
|
+
${this.renderResizeHandle(leadingResizeColumn, true)}
|
|
2776
3380
|
<div class="head-inner" style=${this.cellWidthStyle(column)}>
|
|
2777
3381
|
${column.align === 'right'
|
|
2778
3382
|
? html`${slot}${label}`
|
|
2779
3383
|
: html`${label}${slot}`}
|
|
2780
3384
|
</div>
|
|
3385
|
+
${trailingResize
|
|
3386
|
+
? this.renderResizeHandle(column, false, outerResize)
|
|
3387
|
+
: null}
|
|
2781
3388
|
</th>
|
|
2782
3389
|
`;
|
|
2783
3390
|
}
|