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