@cl-light-ds/ui 0.2.0 → 0.4.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/README.md +45 -45
- package/package.json +2 -2
- package/src/components/accordion/Accordion.astro +248 -248
- package/src/components/accordion/AccordionContent.astro +26 -26
- package/src/components/accordion/AccordionItem.astro +36 -36
- package/src/components/accordion/AccordionTrigger.astro +118 -118
- package/src/components/accordion/CHANGELOG.md +41 -41
- package/src/components/accordion/index.ts +4 -4
- package/src/components/alert/Alert.astro +167 -167
- package/src/components/alert/AlertDescription.astro +24 -24
- package/src/components/alert/AlertTitle.astro +24 -24
- package/src/components/alert/CHANGELOG.md +5 -5
- package/src/components/alert/index.ts +4 -4
- package/src/components/avatar/Avatar.astro +197 -197
- package/src/components/avatar/AvatarBadge.astro +44 -44
- package/src/components/avatar/AvatarFallback.astro +26 -26
- package/src/components/avatar/AvatarGroup.astro +26 -26
- package/src/components/avatar/AvatarGroupCount.astro +30 -30
- package/src/components/avatar/AvatarImage.astro +46 -46
- package/src/components/avatar/CHANGELOG.md +44 -44
- package/src/components/avatar/index.ts +6 -6
- package/src/components/badge/Badge.astro +167 -167
- package/src/components/badge/CHANGELOG.md +49 -49
- package/src/components/badge/index.ts +2 -2
- package/src/components/breadcrumb/Breadcrumb.astro +76 -76
- package/src/components/breadcrumb/BreadcrumbItem.astro +32 -32
- package/src/components/breadcrumb/BreadcrumbList.astro +26 -26
- package/src/components/breadcrumb/BreadcrumbSeparator.astro +26 -26
- package/src/components/breadcrumb/CHANGELOG.md +24 -24
- package/src/components/breadcrumb/index.ts +5 -5
- package/src/components/button/Button.astro +233 -233
- package/src/components/button/CHANGELOG.md +41 -41
- package/src/components/button/index.ts +2 -2
- package/src/components/button-group/ButtonGroup.astro +130 -130
- package/src/components/button-group/ButtonGroupSeparator.astro +30 -30
- package/src/components/button-group/ButtonGroupText.astro +21 -21
- package/src/components/button-group/CHANGELOG.md +34 -34
- package/src/components/button-group/index.ts +3 -3
- package/src/components/callout/CHANGELOG.md +32 -0
- package/src/components/callout/Callout.astro +147 -0
- package/src/components/callout/CalloutDescription.astro +16 -0
- package/src/components/callout/CalloutTitle.astro +16 -0
- package/src/components/callout/index.ts +3 -0
- package/src/components/card/CHANGELOG.md +162 -119
- package/src/components/card/Card.astro +197 -182
- package/src/components/card/CardAction.astro +26 -26
- package/src/components/card/CardContent.astro +28 -28
- package/src/components/card/CardDescription.astro +28 -28
- package/src/components/card/CardFooter.astro +24 -24
- package/src/components/card/CardHeader.astro +30 -30
- package/src/components/card/CardMedia.astro +27 -27
- package/src/components/card/CardMediaActions.astro +21 -0
- package/src/components/card/CardMediaSelect.astro +21 -0
- package/src/components/card/CardOverlay.astro +32 -32
- package/src/components/card/CardTitle.astro +21 -21
- package/src/components/card/index.ts +12 -10
- package/src/components/carousel/CHANGELOG.md +5 -5
- package/src/components/carousel/Carousel.astro +106 -106
- package/src/components/carousel/CarouselSlide.astro +22 -22
- package/src/components/carousel/index.ts +3 -3
- package/src/components/checkbox/CHANGELOG.md +58 -58
- package/src/components/checkbox/Checkbox.astro +159 -159
- package/src/components/checkbox/index.ts +1 -1
- package/src/components/collapsible/CHANGELOG.md +5 -5
- package/src/components/collapsible/Collapsible.astro +202 -202
- package/src/components/collapsible/CollapsibleContent.astro +36 -36
- package/src/components/collapsible/CollapsibleTrigger.astro +34 -34
- package/src/components/collapsible/index.ts +4 -4
- package/src/components/combobox/CHANGELOG.md +27 -27
- package/src/components/combobox/Combobox.astro +365 -365
- package/src/components/combobox/ComboboxContent.astro +37 -37
- package/src/components/combobox/ComboboxEmpty.astro +18 -18
- package/src/components/combobox/ComboboxGroup.astro +19 -19
- package/src/components/combobox/ComboboxInput.astro +74 -74
- package/src/components/combobox/ComboboxItem.astro +39 -39
- package/src/components/combobox/ComboboxLabel.astro +18 -18
- package/src/components/combobox/ComboboxList.astro +21 -21
- package/src/components/combobox/ComboboxSeparator.astro +17 -17
- package/src/components/combobox/ComboboxTrigger.astro +30 -30
- package/src/components/combobox/ComboboxValue.astro +22 -22
- package/src/components/combobox/index.ts +11 -11
- package/src/components/date/CHANGELOG.md +18 -18
- package/src/components/date/Date.astro +137 -137
- package/src/components/date/index.ts +2 -2
- package/src/components/dialog/CHANGELOG.md +39 -39
- package/src/components/dialog/Dialog.astro +196 -196
- package/src/components/dialog/DialogClose.astro +23 -23
- package/src/components/dialog/DialogContent.astro +42 -42
- package/src/components/dialog/DialogDescription.astro +18 -18
- package/src/components/dialog/DialogFooter.astro +18 -18
- package/src/components/dialog/DialogHeader.astro +18 -18
- package/src/components/dialog/DialogOverlay.astro +19 -19
- package/src/components/dialog/DialogTitle.astro +18 -18
- package/src/components/dialog/DialogTrigger.astro +37 -37
- package/src/components/dialog/index.ts +9 -9
- package/src/components/dropdown-menu/CHANGELOG.md +194 -194
- package/src/components/dropdown-menu/DropdownMenu.astro +226 -226
- package/src/components/dropdown-menu/DropdownMenuCheckboxItem.astro +114 -114
- package/src/components/dropdown-menu/DropdownMenuContent.astro +42 -42
- package/src/components/dropdown-menu/DropdownMenuGroup.astro +19 -19
- package/src/components/dropdown-menu/DropdownMenuItem.astro +220 -220
- package/src/components/dropdown-menu/DropdownMenuItemContent.astro +79 -79
- package/src/components/dropdown-menu/DropdownMenuLabel.astro +20 -20
- package/src/components/dropdown-menu/DropdownMenuPortal.astro +13 -13
- package/src/components/dropdown-menu/DropdownMenuRadioGroup.astro +13 -13
- package/src/components/dropdown-menu/DropdownMenuRadioItem.astro +101 -101
- package/src/components/dropdown-menu/DropdownMenuSeparator.astro +18 -18
- package/src/components/dropdown-menu/DropdownMenuShortcut.astro +18 -18
- package/src/components/dropdown-menu/DropdownMenuTrigger.astro +49 -49
- package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +24 -24
- package/src/components/dropdown-menu/index.ts +12 -12
- package/src/components/field/CHANGELOG.md +33 -33
- package/src/components/field/Field.astro +664 -664
- package/src/components/field/FieldContent.astro +20 -20
- package/src/components/field/FieldDescription.astro +22 -22
- package/src/components/field/FieldError.astro +51 -51
- package/src/components/field/FieldGroup.astro +20 -20
- package/src/components/field/FieldLabel.astro +22 -22
- package/src/components/field/FieldLegend.astro +23 -23
- package/src/components/field/FieldSeparator.astro +30 -30
- package/src/components/field/FieldSet.astro +20 -20
- package/src/components/field/FieldTitle.astro +20 -20
- package/src/components/field/index.ts +10 -10
- package/src/components/hover-card/CHANGELOG.md +20 -20
- package/src/components/hover-card/HoverCard.astro +235 -235
- package/src/components/hover-card/HoverCardContent.astro +54 -54
- package/src/components/hover-card/HoverCardTrigger.astro +38 -38
- package/src/components/hover-card/index.ts +3 -3
- package/src/components/input/CHANGELOG.md +44 -29
- package/src/components/input/Input.astro +130 -130
- package/src/components/input/InputOTP.astro +211 -0
- package/src/components/input/index.ts +2 -2
- package/src/components/input-group/CHANGELOG.md +49 -49
- package/src/components/input-group/InputGroup.astro +135 -135
- package/src/components/input-group/InputGroupAddon.astro +49 -49
- package/src/components/input-group/InputGroupButton.astro +58 -58
- package/src/components/input-group/InputGroupInput.astro +18 -18
- package/src/components/input-group/InputGroupText.astro +21 -21
- package/src/components/input-group/InputGroupTextarea.astro +17 -17
- package/src/components/input-group/controlClasses.ts +2 -2
- package/src/components/input-group/index.ts +6 -6
- package/src/components/item/CHANGELOG.md +27 -27
- package/src/components/item/Item.astro +122 -122
- package/src/components/item/ItemActions.astro +21 -21
- package/src/components/item/ItemContent.astro +25 -25
- package/src/components/item/ItemDescription.astro +25 -25
- package/src/components/item/ItemFooter.astro +24 -24
- package/src/components/item/ItemGroup.astro +24 -24
- package/src/components/item/ItemHeader.astro +24 -24
- package/src/components/item/ItemMedia.astro +47 -47
- package/src/components/item/ItemSeparator.astro +21 -21
- package/src/components/item/ItemTitle.astro +25 -25
- package/src/components/item/index.ts +10 -10
- package/src/components/kbd/CHANGELOG.md +5 -5
- package/src/components/kbd/Kbd.astro +145 -145
- package/src/components/kbd/KbdGroup.astro +55 -55
- package/src/components/kbd/index.ts +2 -2
- package/src/components/label/CHANGELOG.md +28 -28
- package/src/components/label/Label.astro +97 -97
- package/src/components/label/index.ts +2 -2
- package/src/components/link-group/CHANGELOG.md +75 -75
- package/src/components/link-group/LinkGroup.astro +67 -67
- package/src/components/link-group/LinkGroupItem.astro +32 -32
- package/src/components/link-group/LinkGroupList.astro +25 -25
- package/src/components/link-group/LinkGroupTitle.astro +22 -22
- package/src/components/link-group/index.ts +5 -5
- package/src/components/link-group/link-group-list-variants.ts +15 -15
- package/src/components/logo/CHANGELOG.md +17 -0
- package/src/components/logo/Logo.astro +90 -0
- package/src/components/logo/index.ts +3 -0
- package/src/components/logo/logo-paths.ts +57 -0
- package/src/components/marquee/CHANGELOG.md +5 -5
- package/src/components/marquee/Marquee.astro +119 -119
- package/src/components/marquee/index.ts +2 -2
- package/src/components/native-select/CHANGELOG.md +5 -5
- package/src/components/native-select/NativeSelect.astro +125 -125
- package/src/components/native-select/NativeSelectOptGroup.astro +14 -14
- package/src/components/native-select/NativeSelectOption.astro +9 -9
- package/src/components/native-select/index.ts +3 -3
- package/src/components/navigation-menu/CHANGELOG.md +63 -63
- package/src/components/navigation-menu/NavigationMenu.astro +414 -414
- package/src/components/navigation-menu/NavigationMenuContent.astro +37 -37
- package/src/components/navigation-menu/NavigationMenuIndicator.astro +24 -24
- package/src/components/navigation-menu/NavigationMenuItem.astro +22 -22
- package/src/components/navigation-menu/NavigationMenuLink.astro +18 -18
- package/src/components/navigation-menu/NavigationMenuList.astro +21 -21
- package/src/components/navigation-menu/NavigationMenuPositioner.astro +44 -44
- package/src/components/navigation-menu/NavigationMenuTrigger.astro +30 -30
- package/src/components/navigation-menu/NavigationMenuViewport.astro +19 -19
- package/src/components/navigation-menu/index.ts +10 -10
- package/src/components/organisms/lightning-sidebar-nav/CHANGELOG.md +50 -0
- package/src/components/organisms/lightning-sidebar-nav/LightningSidebarNav.astro +176 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNav.astro +50 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavGroup.astro +74 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavItem.astro +94 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavProBadge.astro +26 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavSection.astro +71 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavSkeleton.astro +67 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavSubItem.astro +69 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavUsage.astro +78 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavWorkspaceAvatar.astro +46 -0
- package/src/components/organisms/lightning-sidebar-nav/SidebarNavWorkspaceSwitcher.astro +130 -0
- package/src/components/organisms/lightning-sidebar-nav/icons.ts +46 -0
- package/src/components/organisms/lightning-sidebar-nav/index.ts +18 -0
- package/src/components/organisms/lightning-sidebar-nav/types.ts +77 -0
- package/src/components/popover/CHANGELOG.md +20 -20
- package/src/components/popover/Popover.astro +211 -211
- package/src/components/popover/PopoverClose.astro +18 -18
- package/src/components/popover/PopoverContent.astro +50 -50
- package/src/components/popover/PopoverDescription.astro +18 -18
- package/src/components/popover/PopoverHeader.astro +18 -18
- package/src/components/popover/PopoverTitle.astro +18 -18
- package/src/components/popover/PopoverTrigger.astro +35 -35
- package/src/components/popover/index.ts +7 -7
- package/src/components/radio-group/CHANGELOG.md +31 -31
- package/src/components/radio-group/RadioGroup.astro +81 -81
- package/src/components/radio-group/RadioGroupItem.astro +51 -51
- package/src/components/radio-group/index.ts +3 -3
- package/src/components/select/CHANGELOG.md +39 -39
- package/src/components/select/Select.astro +373 -373
- package/src/components/select/SelectContent.astro +91 -91
- package/src/components/select/SelectGroup.astro +19 -19
- package/src/components/select/SelectItem.astro +48 -48
- package/src/components/select/SelectLabel.astro +18 -18
- package/src/components/select/SelectSeparator.astro +17 -17
- package/src/components/select/SelectTrigger.astro +34 -34
- package/src/components/select/SelectValue.astro +22 -22
- package/src/components/select/index.ts +8 -8
- package/src/components/separator/CHANGELOG.md +5 -5
- package/src/components/separator/Separator.astro +60 -60
- package/src/components/separator/index.ts +2 -2
- package/src/components/skeleton/CHANGELOG.md +5 -5
- package/src/components/skeleton/Skeleton.astro +121 -121
- package/src/components/skeleton/index.ts +1 -1
- package/src/components/slider/CHANGELOG.md +5 -5
- package/src/components/slider/Slider.astro +198 -198
- package/src/components/slider/index.ts +1 -1
- package/src/components/spinner/CHANGELOG.md +18 -18
- package/src/components/spinner/Spinner.astro +135 -135
- package/src/components/spinner/index.ts +1 -1
- package/src/components/sticky-surface/CHANGELOG.md +5 -5
- package/src/components/sticky-surface/StickySurface.astro +152 -152
- package/src/components/sticky-surface/index.ts +2 -2
- package/src/components/switch/CHANGELOG.md +29 -29
- package/src/components/switch/Switch.astro +106 -106
- package/src/components/switch/index.ts +1 -1
- package/src/components/table/CHANGELOG.md +24 -24
- package/src/components/table/Table.astro +358 -251
- package/src/components/table/TableBody.astro +17 -18
- package/src/components/table/TableBulkToolbar.astro +37 -0
- package/src/components/table/TableCaption.astro +17 -17
- package/src/components/table/TableCell.astro +45 -21
- package/src/components/table/TableCellText.astro +17 -0
- package/src/components/table/TableColumnMenu.astro +50 -0
- package/src/components/table/TableEditableCell.astro +287 -0
- package/src/components/table/TableEmpty.astro +17 -0
- package/src/components/table/TableFolderCell.astro +38 -0
- package/src/components/table/TableFooter.astro +21 -21
- package/src/components/table/TableHead.astro +90 -21
- package/src/components/table/TableHeader.astro +17 -17
- package/src/components/table/TablePagination.astro +57 -0
- package/src/components/table/TableRow.astro +52 -21
- package/src/components/table/TableSelectAllBanner.astro +34 -0
- package/src/components/table/TableSelectCell.astro +50 -0
- package/src/components/table/TableSkeletonRow.astro +21 -0
- package/src/components/table/index.ts +19 -9
- package/src/components/table/table-cell-variants.ts +29 -0
- package/src/components/tabs/CHANGELOG.md +5 -5
- package/src/components/tabs/Tabs.astro +306 -306
- package/src/components/tabs/TabsContent.astro +47 -47
- package/src/components/tabs/TabsList.astro +69 -69
- package/src/components/tabs/TabsTrigger.astro +40 -40
- package/src/components/tabs/index.ts +6 -6
- package/src/components/textarea/CHANGELOG.md +22 -22
- package/src/components/textarea/Textarea.astro +58 -58
- package/src/components/textarea/index.ts +2 -2
- package/src/components/toggle/CHANGELOG.md +23 -23
- package/src/components/toggle/Toggle.astro +150 -150
- package/src/components/toggle/index.ts +1 -1
- package/src/components/toggle-group/CHANGELOG.md +18 -18
- package/src/components/toggle-group/ToggleGroup.astro +236 -236
- package/src/components/toggle-group/ToggleGroupItem.astro +48 -48
- package/src/components/toggle-group/index.ts +2 -2
- package/src/components/tooltip/CHANGELOG.md +22 -22
- package/src/components/tooltip/Tooltip.astro +357 -357
- package/src/components/tooltip/TooltipContent.astro +85 -85
- package/src/components/tooltip/TooltipTrigger.astro +34 -34
- package/src/components/tooltip/index.ts +3 -3
- package/src/lib/table-controller.ts +1090 -0
- package/src/lib/toggle-group-controller.ts +113 -113
- package/src/lib/toggle-shared.ts +27 -27
- package/src/lib/utils.ts +6 -6
- package/src/styles/globals.css +209 -177
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour for `<Table interactive>`.
|
|
3
|
+
*
|
|
4
|
+
* The controller never sorts, pages or persists data. It manages UI state
|
|
5
|
+
* (sort indicators, selection, edit lifecycle, tree visibility, column layout,
|
|
6
|
+
* keyboard focus) and reports intent through bubbling `table:*` events on the
|
|
7
|
+
* `[data-slot="table-root"]` element. The app responds by re-rendering rows;
|
|
8
|
+
* a MutationObserver re-applies layout and selection state to the new rows.
|
|
9
|
+
*
|
|
10
|
+
* Outbound events (all bubble, all cancelable unless noted):
|
|
11
|
+
* - `table:sort` `{ column, direction: "asc" | "desc" | null }`
|
|
12
|
+
* - `table:edit` `{ rowId, column, value, previous, respond(promise) }`
|
|
13
|
+
* Call `respond` with a promise; reject it (Error or string)
|
|
14
|
+
* to show the error state. `preventDefault()` rejects the edit.
|
|
15
|
+
* - `table:selection` `{ rowIds, allResults }` (not cancelable)
|
|
16
|
+
* - `table:page` `{ page, pageSize }`
|
|
17
|
+
* - `table:toggle-row` `{ rowId, expanded }`
|
|
18
|
+
* - `table:columns` `{ widths, hidden }` (not cancelable)
|
|
19
|
+
*
|
|
20
|
+
* Inbound event:
|
|
21
|
+
* - `table:set` `{ loading?: boolean, totalCount?: number, page?: number }`
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type SortState = "asc" | "desc" | "none";
|
|
25
|
+
type EditValue = string | string[];
|
|
26
|
+
|
|
27
|
+
export interface TableEditDetail {
|
|
28
|
+
rowId: string | null;
|
|
29
|
+
column: string;
|
|
30
|
+
value: EditValue;
|
|
31
|
+
previous: EditValue;
|
|
32
|
+
respond: (result: unknown) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TableSortDetail {
|
|
36
|
+
column: string;
|
|
37
|
+
direction: "asc" | "desc" | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TableSetDetail {
|
|
41
|
+
loading?: boolean;
|
|
42
|
+
totalCount?: number;
|
|
43
|
+
page?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bound = new WeakSet<HTMLElement>();
|
|
47
|
+
const MIN_COLUMN_WIDTH = 64;
|
|
48
|
+
const ERROR_TIMEOUT = 4000;
|
|
49
|
+
|
|
50
|
+
export function initTables(scope: ParentNode = document) {
|
|
51
|
+
scope
|
|
52
|
+
.querySelectorAll<HTMLElement>('[data-slot="table-root"][data-interactive]')
|
|
53
|
+
.forEach((root) => {
|
|
54
|
+
if (bound.has(root)) return;
|
|
55
|
+
bound.add(root);
|
|
56
|
+
createTable(root);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function errorMessage(error: unknown, fallback: string): string {
|
|
61
|
+
if (typeof error === "string" && error) return error;
|
|
62
|
+
if (error instanceof Error && error.message) return error.message;
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseValue(td: HTMLElement): EditValue {
|
|
67
|
+
const raw = td.dataset.value ?? "";
|
|
68
|
+
if (td.dataset.editor !== "multiselect") return raw;
|
|
69
|
+
try {
|
|
70
|
+
const parsed: unknown = JSON.parse(raw || "[]");
|
|
71
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serializeValue(td: HTMLElement, value: EditValue): string {
|
|
78
|
+
return td.dataset.editor === "multiselect" ? JSON.stringify(value) : String(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sameValue(a: EditValue, b: EditValue): boolean {
|
|
82
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
83
|
+
return a.length === b.length && a.every((v) => b.includes(v));
|
|
84
|
+
}
|
|
85
|
+
return a === b;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isTextEntry(el: Element | null): boolean {
|
|
89
|
+
return !!el && (el.matches("input:not([type=checkbox]), textarea, select") || !!el.closest('[data-slot="table-cell-editor"]'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createTable(root: HTMLElement) {
|
|
93
|
+
const tableElement = root.querySelector<HTMLTableElement>('table[data-slot="table"]');
|
|
94
|
+
if (!tableElement) return;
|
|
95
|
+
const table: HTMLTableElement = tableElement;
|
|
96
|
+
|
|
97
|
+
const live = root.querySelector<HTMLElement>('[data-slot="table-live"]');
|
|
98
|
+
const pagination = root.querySelector<HTMLElement>('[data-slot="table-pagination"]');
|
|
99
|
+
const banner = root.querySelector<HTMLElement>('[data-slot="table-select-all-banner"]');
|
|
100
|
+
const columnMenu = root.querySelector<HTMLElement>("[data-table-column-menu]");
|
|
101
|
+
const pinCount = Number(root.dataset.pinnedColumns ?? 0) || 0;
|
|
102
|
+
|
|
103
|
+
let allResults = false;
|
|
104
|
+
let activeText: HTMLTableCellElement | null = null;
|
|
105
|
+
let current: HTMLTableCellElement | null = null;
|
|
106
|
+
const hiddenColumns = new Set<string>();
|
|
107
|
+
const visibleErrors = new Set<HTMLTableCellElement>();
|
|
108
|
+
const errorTimers = new WeakMap<HTMLElement, number>();
|
|
109
|
+
const boundWidgets = new WeakSet<Element>();
|
|
110
|
+
|
|
111
|
+
table.setAttribute("role", "grid");
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------- helpers
|
|
114
|
+
|
|
115
|
+
const emit = <T,>(name: string, detail: T, cancelable = true) =>
|
|
116
|
+
root.dispatchEvent(new CustomEvent<T>(name, { bubbles: true, cancelable, detail }));
|
|
117
|
+
|
|
118
|
+
const announce = (message: string) => {
|
|
119
|
+
if (!live) return;
|
|
120
|
+
live.textContent = "";
|
|
121
|
+
window.setTimeout(() => {
|
|
122
|
+
live.textContent = message;
|
|
123
|
+
}, 50);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const headerRow = () => table.tHead?.rows[table.tHead.rows.length - 1] ?? null;
|
|
127
|
+
|
|
128
|
+
const bodyRows = () =>
|
|
129
|
+
Array.from(table.tBodies).flatMap((tbody) =>
|
|
130
|
+
Array.from(tbody.rows).filter((r) => r.dataset.slot === "table-row"),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const allRows = () => Array.from(table.rows);
|
|
134
|
+
|
|
135
|
+
const cellLabel = (td: HTMLElement) => td.dataset.label ?? td.dataset.column ?? "value";
|
|
136
|
+
|
|
137
|
+
const hasTree = () => !!table.querySelector("tbody tr[data-folder]");
|
|
138
|
+
|
|
139
|
+
const totalCount = () => {
|
|
140
|
+
const value = Number(root.dataset.totalCount ?? pagination?.dataset.totalCount);
|
|
141
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------- sorting
|
|
145
|
+
|
|
146
|
+
const syncSortAvailability = () => {
|
|
147
|
+
const tree = hasTree();
|
|
148
|
+
table.querySelectorAll<HTMLButtonElement>('[data-slot="table-sort-button"]').forEach((button) => {
|
|
149
|
+
button.disabled = tree;
|
|
150
|
+
button.title = tree ? "Sorting is not available in tree tables" : "";
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const setSortState = (th: HTMLElement, state: SortState) => {
|
|
155
|
+
th.dataset.sort = state;
|
|
156
|
+
th.setAttribute("aria-sort", state === "asc" ? "ascending" : state === "desc" ? "descending" : "none");
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const cycleSort = (th: HTMLTableCellElement) => {
|
|
160
|
+
if (hasTree()) return;
|
|
161
|
+
const column = th.dataset.column;
|
|
162
|
+
if (!column) return;
|
|
163
|
+
const sortable = Array.from(table.querySelectorAll<HTMLTableCellElement>("th[data-sortable]"));
|
|
164
|
+
const previous = sortable.map((h) => [h, (h.dataset.sort as SortState) ?? "none"] as const);
|
|
165
|
+
const now = (th.dataset.sort as SortState) ?? "none";
|
|
166
|
+
const next: SortState = now === "asc" ? "desc" : now === "desc" ? "none" : "asc";
|
|
167
|
+
|
|
168
|
+
sortable.forEach((h) => setSortState(h, h === th ? next : "none"));
|
|
169
|
+
|
|
170
|
+
const accepted = emit<TableSortDetail>("table:sort", {
|
|
171
|
+
column,
|
|
172
|
+
direction: next === "none" ? null : next,
|
|
173
|
+
});
|
|
174
|
+
if (!accepted) {
|
|
175
|
+
previous.forEach(([h, state]) => setSortState(h, state));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const name = th.textContent?.trim() || column;
|
|
179
|
+
announce(next === "none" ? "Sorting cleared" : `Sorted by ${name}, ${next === "asc" ? "ascending" : "descending"}`);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// -------------------------------------------------------------- selection
|
|
183
|
+
|
|
184
|
+
const rowCheckbox = (tr: HTMLTableRowElement) =>
|
|
185
|
+
tr.querySelector<HTMLInputElement>('input[data-table-select="row"]');
|
|
186
|
+
|
|
187
|
+
const headerCheckbox = () => table.querySelector<HTMLInputElement>('input[data-table-select="all"]');
|
|
188
|
+
|
|
189
|
+
const selectionRows = () => bodyRows().filter((tr) => rowCheckbox(tr));
|
|
190
|
+
|
|
191
|
+
const descendantsOf = (tr: HTMLTableRowElement) => {
|
|
192
|
+
const id = tr.dataset.rowId;
|
|
193
|
+
if (!id) return [];
|
|
194
|
+
const ids = new Set([id]);
|
|
195
|
+
const result: HTMLTableRowElement[] = [];
|
|
196
|
+
for (const row of bodyRows()) {
|
|
197
|
+
const parent = row.dataset.parentId;
|
|
198
|
+
if (parent && ids.has(parent)) {
|
|
199
|
+
result.push(row);
|
|
200
|
+
if (row.dataset.rowId) ids.add(row.dataset.rowId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const setText = (selector: string, text: string, scope: ParentNode = root) =>
|
|
207
|
+
scope.querySelectorAll(selector).forEach((el) => {
|
|
208
|
+
el.textContent = text;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const syncSelection = () => {
|
|
212
|
+
const rows = selectionRows();
|
|
213
|
+
const boxes = rows.map((tr) => rowCheckbox(tr)!).filter((b) => !b.disabled);
|
|
214
|
+
let checkedCount = 0;
|
|
215
|
+
|
|
216
|
+
rows.forEach((tr) => {
|
|
217
|
+
const checked = rowCheckbox(tr)!.checked;
|
|
218
|
+
if (checked) {
|
|
219
|
+
checkedCount += 1;
|
|
220
|
+
tr.dataset.state = "selected";
|
|
221
|
+
tr.setAttribute("aria-selected", "true");
|
|
222
|
+
} else {
|
|
223
|
+
if (tr.dataset.state === "selected") delete tr.dataset.state;
|
|
224
|
+
tr.setAttribute("aria-selected", "false");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const everyChecked = boxes.length > 0 && boxes.every((b) => b.checked);
|
|
229
|
+
const header = headerCheckbox();
|
|
230
|
+
if (header) {
|
|
231
|
+
header.checked = everyChecked;
|
|
232
|
+
header.indeterminate = checkedCount > 0 && !everyChecked;
|
|
233
|
+
}
|
|
234
|
+
if (!everyChecked) allResults = false;
|
|
235
|
+
|
|
236
|
+
const total = totalCount();
|
|
237
|
+
const count = allResults && total ? total : checkedCount;
|
|
238
|
+
root.toggleAttribute("data-has-selection", checkedCount > 0);
|
|
239
|
+
setText('[data-slot="table-selection-count"]', String(count));
|
|
240
|
+
|
|
241
|
+
if (banner) {
|
|
242
|
+
const show = everyChecked && total !== null && total > boxes.length;
|
|
243
|
+
banner.hidden = !show;
|
|
244
|
+
setText('[data-slot="table-page-count"]', String(boxes.length), banner);
|
|
245
|
+
setText('[data-slot="table-total-count"]', String(total ?? 0), banner);
|
|
246
|
+
banner.querySelectorAll<HTMLElement>("[data-banner-page]").forEach((el) => (el.hidden = allResults));
|
|
247
|
+
banner.querySelectorAll<HTMLElement>("[data-banner-all]").forEach((el) => (el.hidden = !allResults));
|
|
248
|
+
}
|
|
249
|
+
return { rows, checkedCount, count };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const selectionChanged = () => {
|
|
253
|
+
const { rows, count } = syncSelection();
|
|
254
|
+
const rowIds = rows
|
|
255
|
+
.filter((tr) => rowCheckbox(tr)!.checked)
|
|
256
|
+
.map((tr) => tr.dataset.rowId)
|
|
257
|
+
.filter((id): id is string => !!id);
|
|
258
|
+
emit("table:selection", { rowIds, allResults }, false);
|
|
259
|
+
announce(count === 0 ? "No rows selected" : `${count} selected`);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const clearSelection = () => {
|
|
263
|
+
allResults = false;
|
|
264
|
+
selectionRows().forEach((tr) => (rowCheckbox(tr)!.checked = false));
|
|
265
|
+
selectionChanged();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// ------------------------------------------------------------------- tree
|
|
269
|
+
|
|
270
|
+
const applyTree = () => {
|
|
271
|
+
const hiddenParents = new Set<string>();
|
|
272
|
+
for (const tr of bodyRows()) {
|
|
273
|
+
const parent = tr.dataset.parentId;
|
|
274
|
+
const hiddenByParent = !!parent && hiddenParents.has(parent);
|
|
275
|
+
tr.hidden = hiddenByParent;
|
|
276
|
+
const id = tr.dataset.rowId;
|
|
277
|
+
const folder = tr.hasAttribute("data-folder");
|
|
278
|
+
const expanded = tr.hasAttribute("data-expanded");
|
|
279
|
+
if (id && (hiddenByParent || (folder && !expanded))) hiddenParents.add(id);
|
|
280
|
+
if (folder) {
|
|
281
|
+
tr.setAttribute("aria-expanded", String(expanded));
|
|
282
|
+
const toggle = tr.querySelector<HTMLElement>('[data-slot="table-row-toggle"]');
|
|
283
|
+
toggle?.setAttribute("aria-label", expanded ? "Collapse row" : "Expand row");
|
|
284
|
+
toggle?.setAttribute("aria-expanded", String(expanded));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const setExpanded = (tr: HTMLTableRowElement, expanded: boolean) => {
|
|
290
|
+
if (!tr.hasAttribute("data-folder") || tr.hasAttribute("data-expanded") === expanded) return;
|
|
291
|
+
tr.toggleAttribute("data-expanded", expanded);
|
|
292
|
+
applyTree();
|
|
293
|
+
emit("table:toggle-row", { rowId: tr.dataset.rowId ?? null, expanded });
|
|
294
|
+
const name = tr.querySelector("[data-folder-cell]")?.textContent?.trim() ?? "Row";
|
|
295
|
+
announce(`${name} ${expanded ? "expanded" : "collapsed"}`);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------- columns
|
|
299
|
+
|
|
300
|
+
const columnCells = (index: number, column: string | undefined, headerLength: number) =>
|
|
301
|
+
allRows().flatMap((tr) => {
|
|
302
|
+
if (tr.cells.length === headerLength) return tr.cells[index] ? [tr.cells[index]] : [];
|
|
303
|
+
if (!column) return [];
|
|
304
|
+
return Array.from(tr.cells).filter((c) => c.dataset.column === column);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const applyColumns = () => {
|
|
308
|
+
const header = headerRow();
|
|
309
|
+
if (!header) return;
|
|
310
|
+
Array.from(header.cells).forEach((th, index) => {
|
|
311
|
+
const column = th.dataset.column;
|
|
312
|
+
const hide = !!column && hiddenColumns.has(column);
|
|
313
|
+
columnCells(index, column, header.cells.length).forEach((cell) => (cell.hidden = hide));
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const applyPinned = () => {
|
|
318
|
+
const header = headerRow();
|
|
319
|
+
if (!header || pinCount <= 0) return;
|
|
320
|
+
const ths = Array.from(header.cells);
|
|
321
|
+
const pinned = ths.map((_, i) => i).filter((i) => !ths[i]!.hidden).slice(0, pinCount);
|
|
322
|
+
const offsets = new Map<number, number>();
|
|
323
|
+
let left = 0;
|
|
324
|
+
pinned.forEach((i) => {
|
|
325
|
+
offsets.set(i, left);
|
|
326
|
+
left += ths[i]!.getBoundingClientRect().width;
|
|
327
|
+
});
|
|
328
|
+
const last = pinned[pinned.length - 1];
|
|
329
|
+
ths.forEach((th, index) => {
|
|
330
|
+
columnCells(index, th.dataset.column, ths.length).forEach((cell) => {
|
|
331
|
+
const offset = offsets.get(index);
|
|
332
|
+
if (offset === undefined) {
|
|
333
|
+
delete cell.dataset.pinned;
|
|
334
|
+
delete cell.dataset.pinnedLast;
|
|
335
|
+
cell.style.left = "";
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
cell.dataset.pinned = "";
|
|
339
|
+
cell.toggleAttribute("data-pinned-last", index === last);
|
|
340
|
+
cell.style.left = `${offset}px`;
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const emitColumns = () => {
|
|
346
|
+
const widths: Record<string, number> = {};
|
|
347
|
+
headerRow()
|
|
348
|
+
?.querySelectorAll<HTMLElement>("th[data-column]")
|
|
349
|
+
.forEach((th) => {
|
|
350
|
+
if (th.style.width) widths[th.dataset.column!] = Math.round(parseFloat(th.style.width));
|
|
351
|
+
});
|
|
352
|
+
emit("table:columns", { widths, hidden: Array.from(hiddenColumns) }, false);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const setColumnWidth = (th: HTMLElement, width: number) => {
|
|
356
|
+
const px = `${Math.max(MIN_COLUMN_WIDTH, Math.round(width))}px`;
|
|
357
|
+
th.style.width = px;
|
|
358
|
+
th.style.minWidth = px;
|
|
359
|
+
applyPinned();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const bindResize = (handle: HTMLElement) => {
|
|
363
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
364
|
+
const th = handle.closest("th");
|
|
365
|
+
if (!th) return;
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
event.stopPropagation();
|
|
368
|
+
const startX = event.clientX;
|
|
369
|
+
const startWidth = th.getBoundingClientRect().width;
|
|
370
|
+
handle.setPointerCapture(event.pointerId);
|
|
371
|
+
root.dataset.resizing = "";
|
|
372
|
+
const move = (e: PointerEvent) => setColumnWidth(th, startWidth + e.clientX - startX);
|
|
373
|
+
const up = () => {
|
|
374
|
+
handle.removeEventListener("pointermove", move);
|
|
375
|
+
handle.removeEventListener("pointerup", up);
|
|
376
|
+
handle.removeEventListener("pointercancel", up);
|
|
377
|
+
delete root.dataset.resizing;
|
|
378
|
+
emitColumns();
|
|
379
|
+
};
|
|
380
|
+
handle.addEventListener("pointermove", move);
|
|
381
|
+
handle.addEventListener("pointerup", up);
|
|
382
|
+
handle.addEventListener("pointercancel", up);
|
|
383
|
+
});
|
|
384
|
+
// A click on the handle must not toggle the sort button underneath.
|
|
385
|
+
handle.addEventListener("click", (e) => e.stopPropagation());
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (columnMenu) {
|
|
389
|
+
let columns: string[] = [];
|
|
390
|
+
let visible: string[] = [];
|
|
391
|
+
try {
|
|
392
|
+
columns = JSON.parse(columnMenu.dataset.columns ?? "[]");
|
|
393
|
+
visible = JSON.parse(columnMenu.dataset.defaultValues ?? "[]");
|
|
394
|
+
} catch {
|
|
395
|
+
/* malformed attributes: treat every column as visible */
|
|
396
|
+
}
|
|
397
|
+
columns.filter((c) => !visible.includes(c)).forEach((c) => hiddenColumns.add(c));
|
|
398
|
+
columnMenu.addEventListener("dropdown-menu:values-change", (event) => {
|
|
399
|
+
const { values } = (event as CustomEvent<{ values: string[] }>).detail;
|
|
400
|
+
hiddenColumns.clear();
|
|
401
|
+
columns.filter((c) => !values.includes(c)).forEach((c) => hiddenColumns.add(c));
|
|
402
|
+
applyColumns();
|
|
403
|
+
applyPinned();
|
|
404
|
+
ensureTabStop();
|
|
405
|
+
emitColumns();
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ----------------------------------------------------------------- errors
|
|
410
|
+
|
|
411
|
+
// Bubbles are moved to <body> on first use: pinned (sticky, z-indexed) cells
|
|
412
|
+
// form stacking contexts that would paint later rows over the bubble.
|
|
413
|
+
const errorElements = new WeakMap<HTMLElement, HTMLElement>();
|
|
414
|
+
const errorElement = (td: HTMLElement) => {
|
|
415
|
+
let el = errorElements.get(td) ?? null;
|
|
416
|
+
if (!el) {
|
|
417
|
+
el = td.querySelector<HTMLElement>('[data-slot="table-cell-error"]');
|
|
418
|
+
if (!el) return null;
|
|
419
|
+
errorElements.set(td, el);
|
|
420
|
+
document.body.append(el);
|
|
421
|
+
}
|
|
422
|
+
return el;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const positionError = (td: HTMLTableCellElement) => {
|
|
426
|
+
const el = errorElement(td);
|
|
427
|
+
if (!el) return;
|
|
428
|
+
const rect = td.getBoundingClientRect();
|
|
429
|
+
el.style.left = `${rect.left + 8}px`;
|
|
430
|
+
el.style.top = `${rect.bottom + 4}px`;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const showError = (td: HTMLTableCellElement, message: string, autoHide = false) => {
|
|
434
|
+
const el = errorElement(td);
|
|
435
|
+
if (!el) return;
|
|
436
|
+
el.textContent = message;
|
|
437
|
+
el.id ||= `table-error-${Math.random().toString(36).slice(2)}`;
|
|
438
|
+
el.hidden = false;
|
|
439
|
+
td.querySelector("[data-table-input]")?.setAttribute("aria-describedby", el.id);
|
|
440
|
+
visibleErrors.add(td);
|
|
441
|
+
positionError(td);
|
|
442
|
+
window.clearTimeout(errorTimers.get(td));
|
|
443
|
+
if (autoHide) errorTimers.set(td, window.setTimeout(() => hideError(td), ERROR_TIMEOUT));
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const hideError = (td: HTMLTableCellElement) => {
|
|
447
|
+
const el = errorElement(td);
|
|
448
|
+
if (el) el.hidden = true;
|
|
449
|
+
td.querySelector("[data-table-input]")?.removeAttribute("aria-describedby");
|
|
450
|
+
visibleErrors.delete(td);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const repositionErrors = () =>
|
|
454
|
+
visibleErrors.forEach((td) => (td.isConnected ? positionError(td) : hideError(td)));
|
|
455
|
+
root.querySelector('[data-slot="table-container"]')?.addEventListener("scroll", repositionErrors, { passive: true });
|
|
456
|
+
window.addEventListener("scroll", repositionErrors, { passive: true, capture: true });
|
|
457
|
+
window.addEventListener("resize", () => {
|
|
458
|
+
repositionErrors();
|
|
459
|
+
applyPinned();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------- editing
|
|
463
|
+
|
|
464
|
+
const requestEdit = (td: HTMLTableCellElement, value: EditValue, previous: EditValue): Promise<unknown> => {
|
|
465
|
+
let pending: Promise<unknown> | undefined;
|
|
466
|
+
const accepted = emit<TableEditDetail>("table:edit", {
|
|
467
|
+
rowId: td.closest("tr")?.dataset.rowId ?? null,
|
|
468
|
+
column: td.dataset.column ?? "",
|
|
469
|
+
value,
|
|
470
|
+
previous,
|
|
471
|
+
respond: (result) => {
|
|
472
|
+
pending = Promise.resolve(result);
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
if (!accepted) return Promise.reject(new Error("This change was not accepted."));
|
|
476
|
+
return pending ?? Promise.resolve(undefined);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const textInput = (td: HTMLElement) => td.querySelector<HTMLInputElement>("input[data-table-input]");
|
|
480
|
+
const confirmButton = (td: HTMLElement) => td.querySelector<HTMLButtonElement>('[data-table-action="confirm"]');
|
|
481
|
+
|
|
482
|
+
const validateText = (td: HTMLTableCellElement, showMessage: boolean) => {
|
|
483
|
+
const input = textInput(td);
|
|
484
|
+
if (!input) return true;
|
|
485
|
+
const valid = input.checkValidity();
|
|
486
|
+
const confirm = confirmButton(td);
|
|
487
|
+
if (confirm) confirm.disabled = !valid;
|
|
488
|
+
if (valid) {
|
|
489
|
+
input.removeAttribute("aria-invalid");
|
|
490
|
+
hideError(td);
|
|
491
|
+
} else if (showMessage) {
|
|
492
|
+
input.setAttribute("aria-invalid", "true");
|
|
493
|
+
showError(td, input.validationMessage);
|
|
494
|
+
}
|
|
495
|
+
return valid;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const renderTextValue = (td: HTMLElement, value: string) => {
|
|
499
|
+
const target = td.querySelector<HTMLElement>('[data-slot="table-cell-value"]');
|
|
500
|
+
if (!target) return;
|
|
501
|
+
target.textContent = value || td.dataset.placeholder || "";
|
|
502
|
+
target.classList.toggle("text-muted-foreground", !value);
|
|
503
|
+
target.classList.toggle("font-normal", !value);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const cancelText = (td: HTMLTableCellElement, refocus = true) => {
|
|
507
|
+
const input = textInput(td);
|
|
508
|
+
td.dataset.state = "idle";
|
|
509
|
+
input?.removeAttribute("aria-invalid");
|
|
510
|
+
hideError(td);
|
|
511
|
+
if (activeText === td) activeText = null;
|
|
512
|
+
if (refocus) focusCell(td);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const startText = (td: HTMLTableCellElement) => {
|
|
516
|
+
const input = textInput(td);
|
|
517
|
+
if (!input || td.dataset.state === "saving") return;
|
|
518
|
+
if (activeText && activeText !== td) {
|
|
519
|
+
const other = activeText;
|
|
520
|
+
if (textInput(other)?.value !== (other.dataset.value ?? "")) {
|
|
521
|
+
textInput(other)?.focus();
|
|
522
|
+
announce(`Save or cancel the ${cellLabel(other)} edit first`);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
cancelText(other, false);
|
|
526
|
+
}
|
|
527
|
+
activeText = td;
|
|
528
|
+
input.value = td.dataset.value ?? "";
|
|
529
|
+
td.dataset.state = "editing";
|
|
530
|
+
validateText(td, false);
|
|
531
|
+
input.focus();
|
|
532
|
+
input.select();
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const commitText = async (td: HTMLTableCellElement) => {
|
|
536
|
+
const input = textInput(td);
|
|
537
|
+
if (!input || td.dataset.state === "saving") return;
|
|
538
|
+
if (!validateText(td, true)) {
|
|
539
|
+
input.focus();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const previous = td.dataset.value ?? "";
|
|
543
|
+
const value = input.value;
|
|
544
|
+
if (value === previous) {
|
|
545
|
+
cancelText(td);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
td.dataset.state = "saving";
|
|
549
|
+
td.setAttribute("aria-busy", "true");
|
|
550
|
+
input.readOnly = true;
|
|
551
|
+
try {
|
|
552
|
+
const result = await requestEdit(td, value, previous);
|
|
553
|
+
const saved = typeof result === "string" ? result : value;
|
|
554
|
+
td.dataset.value = saved;
|
|
555
|
+
renderTextValue(td, saved);
|
|
556
|
+
td.dataset.state = "idle";
|
|
557
|
+
activeText = null;
|
|
558
|
+
hideError(td);
|
|
559
|
+
focusCell(td);
|
|
560
|
+
announce(`${cellLabel(td)} saved`);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
td.dataset.state = "error";
|
|
563
|
+
input.setAttribute("aria-invalid", "true");
|
|
564
|
+
const message = errorMessage(error, "Could not save the change.");
|
|
565
|
+
showError(td, message);
|
|
566
|
+
input.focus();
|
|
567
|
+
announce(`${cellLabel(td)} not saved. ${message}`);
|
|
568
|
+
} finally {
|
|
569
|
+
input.readOnly = false;
|
|
570
|
+
td.removeAttribute("aria-busy");
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// -------------------------------------------------------- choice editors
|
|
575
|
+
|
|
576
|
+
const renderChoice = (td: HTMLTableCellElement, value: EditValue) => {
|
|
577
|
+
const target = td.querySelector<HTMLElement>('[data-slot="table-cell-value"]');
|
|
578
|
+
const template = td.querySelector<HTMLTemplateElement>('template[data-slot="table-cell-templates"]');
|
|
579
|
+
if (!target || !template) return;
|
|
580
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
581
|
+
const option = (v: string) =>
|
|
582
|
+
template.content.querySelector<HTMLElement>(`[data-option="${CSS.escape(v)}"]`);
|
|
583
|
+
|
|
584
|
+
if (td.dataset.editor === "combobox") {
|
|
585
|
+
const text = values.map((v) => option(v)?.textContent?.trim() ?? v).join(", ");
|
|
586
|
+
renderTextValue(td, text);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const max = Number(td.dataset.maxVisible ?? 2) || 2;
|
|
591
|
+
const nodes: Node[] = values.slice(0, max).map((v) => {
|
|
592
|
+
const node = option(v)?.cloneNode(true) as HTMLElement | undefined;
|
|
593
|
+
if (node) {
|
|
594
|
+
node.removeAttribute("data-option");
|
|
595
|
+
return node;
|
|
596
|
+
}
|
|
597
|
+
return document.createTextNode(v);
|
|
598
|
+
});
|
|
599
|
+
if (values.length > max) {
|
|
600
|
+
const more = template.content.querySelector("[data-option-overflow]")?.cloneNode(true) as HTMLElement | undefined;
|
|
601
|
+
if (more) {
|
|
602
|
+
more.removeAttribute("data-option-overflow");
|
|
603
|
+
more.textContent = `+${values.length - max}`;
|
|
604
|
+
nodes.push(more);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (nodes.length === 0) {
|
|
608
|
+
const empty = document.createElement("span");
|
|
609
|
+
empty.className = "text-muted-foreground font-normal";
|
|
610
|
+
empty.textContent = td.dataset.placeholder ?? "";
|
|
611
|
+
nodes.push(empty);
|
|
612
|
+
}
|
|
613
|
+
target.replaceChildren(...nodes);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const commitChoice = async (
|
|
617
|
+
td: HTMLTableCellElement,
|
|
618
|
+
value: EditValue,
|
|
619
|
+
previous: EditValue,
|
|
620
|
+
revert: (value: EditValue) => void,
|
|
621
|
+
) => {
|
|
622
|
+
if (sameValue(value, previous)) return;
|
|
623
|
+
td.dataset.value = serializeValue(td, value);
|
|
624
|
+
renderChoice(td, value);
|
|
625
|
+
td.dataset.state = "saving";
|
|
626
|
+
td.setAttribute("aria-busy", "true");
|
|
627
|
+
hideError(td);
|
|
628
|
+
try {
|
|
629
|
+
await requestEdit(td, value, previous);
|
|
630
|
+
announce(`${cellLabel(td)} saved`);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
td.dataset.value = serializeValue(td, previous);
|
|
633
|
+
renderChoice(td, previous);
|
|
634
|
+
revert(previous);
|
|
635
|
+
const message = errorMessage(error, "Could not save the change.");
|
|
636
|
+
showError(td, message, true);
|
|
637
|
+
announce(`${cellLabel(td)} not saved. ${message}`);
|
|
638
|
+
} finally {
|
|
639
|
+
td.dataset.state = "idle";
|
|
640
|
+
td.removeAttribute("aria-busy");
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const bindMenuCell = (menu: HTMLElement) => {
|
|
645
|
+
const td = menu.closest<HTMLTableCellElement>("td[data-editable]");
|
|
646
|
+
if (!td) return;
|
|
647
|
+
const restore = (value: EditValue) =>
|
|
648
|
+
menu.dispatchEvent(
|
|
649
|
+
new CustomEvent("dropdown-menu:set", {
|
|
650
|
+
detail: Array.isArray(value) ? { values: value, source: "restore" } : { value, source: "restore" },
|
|
651
|
+
}),
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
if (td.dataset.editor === "select") {
|
|
655
|
+
menu.addEventListener("dropdown-menu:value-change", (event) => {
|
|
656
|
+
const { value, previousValue, source } = (
|
|
657
|
+
event as CustomEvent<{ value: string | null; previousValue: string | null; source: string }>
|
|
658
|
+
).detail;
|
|
659
|
+
if (source === "programmatic" || source === "restore") return;
|
|
660
|
+
void commitChoice(td, value ?? "", previousValue ?? "", restore);
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Multiselect: preview while open, commit once when the menu closes.
|
|
666
|
+
let snapshot: string[] | null = null;
|
|
667
|
+
let working: string[] = [];
|
|
668
|
+
menu.addEventListener("dropdown-menu:open-change", (event) => {
|
|
669
|
+
const { open } = (event as CustomEvent<{ open: boolean }>).detail;
|
|
670
|
+
if (open) {
|
|
671
|
+
snapshot = parseValue(td) as string[];
|
|
672
|
+
working = [...snapshot];
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!snapshot) return;
|
|
676
|
+
const before = snapshot;
|
|
677
|
+
snapshot = null;
|
|
678
|
+
if (sameValue(working, before)) {
|
|
679
|
+
renderChoice(td, before);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
void commitChoice(td, working, before, restore);
|
|
683
|
+
});
|
|
684
|
+
menu.addEventListener("dropdown-menu:values-change", (event) => {
|
|
685
|
+
const { values, source } = (event as CustomEvent<{ values: string[]; source: string }>).detail;
|
|
686
|
+
if (source === "programmatic" || source === "restore" || !snapshot) return;
|
|
687
|
+
working = values;
|
|
688
|
+
renderChoice(td, values);
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
const comboboxOf = (td: HTMLElement) => td.querySelector<HTMLElement>("[data-table-combobox]");
|
|
693
|
+
|
|
694
|
+
const startCombobox = (td: HTMLTableCellElement) => {
|
|
695
|
+
const combobox = comboboxOf(td);
|
|
696
|
+
if (!combobox || td.dataset.state === "saving") return;
|
|
697
|
+
td.dataset.state = "editing";
|
|
698
|
+
const input = combobox.querySelector<HTMLInputElement>('[data-slot="combobox-input"]');
|
|
699
|
+
input?.focus();
|
|
700
|
+
combobox.dispatchEvent(new CustomEvent("combobox:set", { detail: { open: true } }));
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const bindCombobox = (combobox: HTMLElement) => {
|
|
704
|
+
const td = combobox.closest<HTMLTableCellElement>("td[data-editable]");
|
|
705
|
+
if (!td) return;
|
|
706
|
+
const restore = (value: EditValue) =>
|
|
707
|
+
combobox.dispatchEvent(new CustomEvent("combobox:set", { detail: { value: String(value) || null } }));
|
|
708
|
+
const close = () => {
|
|
709
|
+
if (td.dataset.state !== "editing") return;
|
|
710
|
+
td.dataset.state = "idle";
|
|
711
|
+
restore(td.dataset.value ?? "");
|
|
712
|
+
focusCell(td);
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
combobox.addEventListener("combobox:change", (event) => {
|
|
716
|
+
if (td.dataset.state !== "editing") return;
|
|
717
|
+
const { value } = (event as CustomEvent<{ value: string | null }>).detail;
|
|
718
|
+
const previous = td.dataset.value ?? "";
|
|
719
|
+
td.dataset.state = "idle";
|
|
720
|
+
focusCell(td);
|
|
721
|
+
if (value !== null && value !== previous) void commitChoice(td, value, previous, restore);
|
|
722
|
+
});
|
|
723
|
+
combobox.addEventListener("combobox:open-change", (event) => {
|
|
724
|
+
if ((event as CustomEvent<{ open: boolean }>).detail.open) return;
|
|
725
|
+
// Let a pending `combobox:change` land first.
|
|
726
|
+
window.setTimeout(close, 0);
|
|
727
|
+
});
|
|
728
|
+
combobox.addEventListener("focusout", (event) => {
|
|
729
|
+
const next = (event as FocusEvent).relatedTarget as Node | null;
|
|
730
|
+
if (next && combobox.contains(next)) return;
|
|
731
|
+
window.setTimeout(() => {
|
|
732
|
+
if (!combobox.contains(document.activeElement)) close();
|
|
733
|
+
}, 0);
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// --------------------------------------------------------------- keyboard
|
|
738
|
+
|
|
739
|
+
const visibleCells = (tr: HTMLTableRowElement) => Array.from(tr.cells).filter((c) => !c.hidden);
|
|
740
|
+
const gridRows = () => allRows().filter((tr) => !tr.hidden && visibleCells(tr).length > 0);
|
|
741
|
+
|
|
742
|
+
function focusCell(cell: HTMLTableCellElement) {
|
|
743
|
+
if (current && current !== cell) current.tabIndex = -1;
|
|
744
|
+
current = cell;
|
|
745
|
+
cell.tabIndex = 0;
|
|
746
|
+
cell.focus();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function ensureTabStop() {
|
|
750
|
+
const cells = Array.from(table.querySelectorAll<HTMLTableCellElement>("th, td"));
|
|
751
|
+
cells.forEach((cell) => {
|
|
752
|
+
if (cell !== current) cell.tabIndex = -1;
|
|
753
|
+
});
|
|
754
|
+
// Inner controls are reached with Enter/Space, not Tab (ARIA grid pattern).
|
|
755
|
+
table
|
|
756
|
+
.querySelectorAll<HTMLElement>("th button, th input, td button, td input[type=checkbox], td a[href]")
|
|
757
|
+
.forEach((el) => {
|
|
758
|
+
if (!el.closest('[data-slot="table-cell-editor"]')) el.tabIndex = -1;
|
|
759
|
+
});
|
|
760
|
+
if (!current || !table.contains(current) || current.hidden || current.closest("tr")?.hidden) {
|
|
761
|
+
current = gridRows()[0] ? visibleCells(gridRows()[0]!)[0] ?? null : null;
|
|
762
|
+
}
|
|
763
|
+
if (current) current.tabIndex = 0;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const moveFocus = (cell: HTMLTableCellElement, key: string, ctrl: boolean) => {
|
|
767
|
+
const rows = gridRows();
|
|
768
|
+
const tr = cell.closest("tr");
|
|
769
|
+
let rowIndex = tr ? rows.indexOf(tr) : -1;
|
|
770
|
+
if (rowIndex < 0) return;
|
|
771
|
+
let colIndex = visibleCells(tr!).indexOf(cell);
|
|
772
|
+
const lastRow = rows.length - 1;
|
|
773
|
+
switch (key) {
|
|
774
|
+
case "ArrowUp":
|
|
775
|
+
rowIndex = Math.max(0, rowIndex - 1);
|
|
776
|
+
break;
|
|
777
|
+
case "ArrowDown":
|
|
778
|
+
rowIndex = Math.min(lastRow, rowIndex + 1);
|
|
779
|
+
break;
|
|
780
|
+
case "ArrowLeft":
|
|
781
|
+
colIndex = Math.max(0, colIndex - 1);
|
|
782
|
+
break;
|
|
783
|
+
case "ArrowRight":
|
|
784
|
+
colIndex += 1;
|
|
785
|
+
break;
|
|
786
|
+
case "Home":
|
|
787
|
+
colIndex = 0;
|
|
788
|
+
if (ctrl) rowIndex = 0;
|
|
789
|
+
break;
|
|
790
|
+
case "End":
|
|
791
|
+
colIndex = Number.MAX_SAFE_INTEGER;
|
|
792
|
+
if (ctrl) rowIndex = lastRow;
|
|
793
|
+
break;
|
|
794
|
+
case "PageUp":
|
|
795
|
+
rowIndex = Math.max(0, rowIndex - 10);
|
|
796
|
+
break;
|
|
797
|
+
case "PageDown":
|
|
798
|
+
rowIndex = Math.min(lastRow, rowIndex + 10);
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
const target = visibleCells(rows[rowIndex]!);
|
|
802
|
+
const next = target[Math.min(colIndex, target.length - 1)];
|
|
803
|
+
if (next) focusCell(next);
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const activate = (cell: HTMLTableCellElement) => {
|
|
807
|
+
if (cell.matches('td[data-editable][data-editor="text"]')) return startText(cell);
|
|
808
|
+
if (cell.matches('td[data-editable][data-editor="combobox"]')) return startCombobox(cell);
|
|
809
|
+
const control = cell.querySelector<HTMLElement>(
|
|
810
|
+
'[data-slot="table-cell-display"], [data-slot="table-sort-button"], [data-slot="table-row-toggle"], input[type=checkbox], button, a[href]',
|
|
811
|
+
);
|
|
812
|
+
if (!control || control.closest('[data-slot="table-cell-editor"]')) return;
|
|
813
|
+
// Checkboxes toggle in place; menus and buttons take focus so it returns to them.
|
|
814
|
+
if (!control.matches("input[type=checkbox]")) control.focus();
|
|
815
|
+
control.click();
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const NAV_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
819
|
+
|
|
820
|
+
// Capture phase so arrow keys on a focused trigger navigate the grid
|
|
821
|
+
// instead of opening its menu.
|
|
822
|
+
table.addEventListener(
|
|
823
|
+
"keydown",
|
|
824
|
+
(event) => {
|
|
825
|
+
const target = event.target as HTMLElement;
|
|
826
|
+
if (isTextEntry(target)) return;
|
|
827
|
+
const cell = target.closest<HTMLTableCellElement>("th, td");
|
|
828
|
+
if (!cell || cell.closest("table") !== table || !NAV_KEYS.has(event.key)) return;
|
|
829
|
+
|
|
830
|
+
const tr = cell.closest("tr");
|
|
831
|
+
if (tr?.hasAttribute("data-folder") && cell.hasAttribute("data-folder-cell")) {
|
|
832
|
+
const expanded = tr.hasAttribute("data-expanded");
|
|
833
|
+
if (event.key === "ArrowRight" && !expanded) {
|
|
834
|
+
event.preventDefault();
|
|
835
|
+
event.stopPropagation();
|
|
836
|
+
return setExpanded(tr, true);
|
|
837
|
+
}
|
|
838
|
+
if (event.key === "ArrowLeft" && expanded) {
|
|
839
|
+
event.preventDefault();
|
|
840
|
+
event.stopPropagation();
|
|
841
|
+
return setExpanded(tr, false);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (event.altKey && cell.tagName === "TH" && cell.querySelector('[data-slot="table-resize-handle"]')) {
|
|
845
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
event.stopPropagation();
|
|
848
|
+
setColumnWidth(cell, cell.getBoundingClientRect().width + (event.key === "ArrowRight" ? 16 : -16));
|
|
849
|
+
emitColumns();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
event.preventDefault();
|
|
854
|
+
event.stopPropagation();
|
|
855
|
+
moveFocus(cell, event.key, event.ctrlKey || event.metaKey);
|
|
856
|
+
},
|
|
857
|
+
true,
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
table.addEventListener("keydown", (event) => {
|
|
861
|
+
const target = event.target as HTMLElement;
|
|
862
|
+
const cell = target.closest<HTMLTableCellElement>("th, td");
|
|
863
|
+
if (!cell || cell.closest("table") !== table) return;
|
|
864
|
+
|
|
865
|
+
// Text editor keys.
|
|
866
|
+
if (target.matches("input[data-table-input]")) {
|
|
867
|
+
if (event.key === "Enter") {
|
|
868
|
+
event.preventDefault();
|
|
869
|
+
void commitText(cell);
|
|
870
|
+
} else if (event.key === "Escape") {
|
|
871
|
+
event.preventDefault();
|
|
872
|
+
event.stopPropagation();
|
|
873
|
+
cancelText(cell);
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (isTextEntry(target)) return;
|
|
878
|
+
if (target !== cell) return; // inner controls handle their own Enter/Space
|
|
879
|
+
|
|
880
|
+
if (event.key === "Enter" || event.key === "F2") {
|
|
881
|
+
event.preventDefault();
|
|
882
|
+
activate(cell);
|
|
883
|
+
} else if (event.key === " ") {
|
|
884
|
+
const checkbox = cell.querySelector<HTMLInputElement>("input[type=checkbox]");
|
|
885
|
+
if (checkbox) {
|
|
886
|
+
event.preventDefault();
|
|
887
|
+
checkbox.click();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
table.addEventListener("focusin", (event) => {
|
|
893
|
+
const cell = (event.target as HTMLElement).closest<HTMLTableCellElement>("th, td");
|
|
894
|
+
if (!cell || cell.closest("table") !== table || cell === current) return;
|
|
895
|
+
if (current) current.tabIndex = -1;
|
|
896
|
+
current = cell;
|
|
897
|
+
cell.tabIndex = 0;
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// ----------------------------------------------------------------- clicks
|
|
901
|
+
|
|
902
|
+
root.addEventListener("click", (event) => {
|
|
903
|
+
const target = event.target as HTMLElement;
|
|
904
|
+
|
|
905
|
+
const action = target.closest<HTMLElement>("[data-table-action]");
|
|
906
|
+
if (action && root.contains(action)) {
|
|
907
|
+
const td = action.closest<HTMLTableCellElement>("td");
|
|
908
|
+
switch (action.dataset.tableAction) {
|
|
909
|
+
case "confirm":
|
|
910
|
+
if (td) void commitText(td);
|
|
911
|
+
return;
|
|
912
|
+
case "cancel":
|
|
913
|
+
if (td) cancelText(td);
|
|
914
|
+
return;
|
|
915
|
+
case "clear-selection":
|
|
916
|
+
clearSelection();
|
|
917
|
+
return;
|
|
918
|
+
case "select-all-results":
|
|
919
|
+
allResults = true;
|
|
920
|
+
selectionChanged();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const sort = target.closest<HTMLButtonElement>('[data-slot="table-sort-button"]');
|
|
926
|
+
if (sort && table.contains(sort)) {
|
|
927
|
+
const th = sort.closest("th");
|
|
928
|
+
if (th) cycleSort(th);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const toggle = target.closest<HTMLElement>('[data-slot="table-row-toggle"]');
|
|
933
|
+
if (toggle && table.contains(toggle)) {
|
|
934
|
+
const tr = toggle.closest("tr");
|
|
935
|
+
if (tr) setExpanded(tr, !tr.hasAttribute("data-expanded"));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const display = target.closest<HTMLElement>('[data-slot="table-cell-display"]');
|
|
940
|
+
const td = display?.closest<HTMLTableCellElement>("td[data-editable]");
|
|
941
|
+
if (display && td && table.contains(td)) {
|
|
942
|
+
if (td.dataset.editor === "text") startText(td);
|
|
943
|
+
else if (td.dataset.editor === "combobox") startCombobox(td);
|
|
944
|
+
// select/multiselect: the dropdown trigger opens itself.
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const page = target.closest<HTMLButtonElement>("[data-table-page]");
|
|
948
|
+
if (page && pagination?.contains(page)) goToPage(page.dataset.tablePage ?? "");
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
table.addEventListener("input", (event) => {
|
|
952
|
+
const input = event.target as HTMLElement;
|
|
953
|
+
if (!input.matches("input[data-table-input]")) return;
|
|
954
|
+
const td = input.closest<HTMLTableCellElement>("td");
|
|
955
|
+
if (!td) return;
|
|
956
|
+
if (td.dataset.state === "error") td.dataset.state = "editing";
|
|
957
|
+
validateText(td, true);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
table.addEventListener("change", (event) => {
|
|
961
|
+
const input = event.target as HTMLInputElement;
|
|
962
|
+
const kind = input.dataset?.tableSelect;
|
|
963
|
+
if (!kind) return;
|
|
964
|
+
if (kind === "all") {
|
|
965
|
+
allResults = false;
|
|
966
|
+
selectionRows().forEach((tr) => {
|
|
967
|
+
const box = rowCheckbox(tr)!;
|
|
968
|
+
if (!box.disabled) box.checked = input.checked;
|
|
969
|
+
});
|
|
970
|
+
} else {
|
|
971
|
+
const tr = input.closest("tr");
|
|
972
|
+
if (!input.checked) allResults = false;
|
|
973
|
+
if (tr?.hasAttribute("data-folder")) {
|
|
974
|
+
descendantsOf(tr).forEach((row) => {
|
|
975
|
+
const box = rowCheckbox(row);
|
|
976
|
+
if (box && !box.disabled) box.checked = input.checked;
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
selectionChanged();
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// ------------------------------------------------------------- pagination
|
|
984
|
+
|
|
985
|
+
const paginationState = () => {
|
|
986
|
+
const page = Number(pagination?.dataset.page ?? 1) || 1;
|
|
987
|
+
const pageSize = Number(pagination?.dataset.pageSize ?? 10) || 10;
|
|
988
|
+
const total = Number(pagination?.dataset.totalCount ?? 0) || 0;
|
|
989
|
+
return { page, pageSize, total, pageCount: Math.max(1, Math.ceil(total / pageSize)) };
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const renderPagination = () => {
|
|
993
|
+
if (!pagination) return;
|
|
994
|
+
const { page, pageSize, total, pageCount } = paginationState();
|
|
995
|
+
const from = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
996
|
+
const to = Math.min(page * pageSize, total);
|
|
997
|
+
setText('[data-slot="table-pagination-summary"]', `Showing ${from}–${to} of ${total}`, pagination);
|
|
998
|
+
setText('[data-slot="table-pagination-status"]', `Page ${page} of ${pageCount}`, pagination);
|
|
999
|
+
pagination.querySelectorAll<HTMLButtonElement>("[data-table-page]").forEach((button) => {
|
|
1000
|
+
const dir = button.dataset.tablePage;
|
|
1001
|
+
button.disabled = dir === "first" || dir === "prev" ? page <= 1 : page >= pageCount;
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const setPage = (page: number, pageSize: number) => {
|
|
1006
|
+
if (!pagination) return;
|
|
1007
|
+
const accepted = emit("table:page", { page, pageSize });
|
|
1008
|
+
if (!accepted) return;
|
|
1009
|
+
pagination.dataset.page = String(page);
|
|
1010
|
+
pagination.dataset.pageSize = String(pageSize);
|
|
1011
|
+
allResults = false;
|
|
1012
|
+
renderPagination();
|
|
1013
|
+
announce(`Page ${page}`);
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
function goToPage(direction: string) {
|
|
1017
|
+
const { page, pageSize, pageCount } = paginationState();
|
|
1018
|
+
const next =
|
|
1019
|
+
direction === "first" ? 1 : direction === "prev" ? page - 1 : direction === "next" ? page + 1 : pageCount;
|
|
1020
|
+
const clamped = Math.min(pageCount, Math.max(1, next));
|
|
1021
|
+
if (clamped !== page) setPage(clamped, pageSize);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
pagination?.addEventListener("change", (event) => {
|
|
1025
|
+
const select = event.target as HTMLSelectElement;
|
|
1026
|
+
if (!select.matches("[data-table-page-size]")) return;
|
|
1027
|
+
setPage(1, Number(select.value) || 10);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// ---------------------------------------------------------------- inbound
|
|
1031
|
+
|
|
1032
|
+
root.addEventListener("table:set", (event) => {
|
|
1033
|
+
const detail = (event as CustomEvent<TableSetDetail>).detail ?? {};
|
|
1034
|
+
if (detail.loading !== undefined) {
|
|
1035
|
+
root.toggleAttribute("data-loading", detail.loading);
|
|
1036
|
+
table.setAttribute("aria-busy", String(detail.loading));
|
|
1037
|
+
}
|
|
1038
|
+
if (detail.totalCount !== undefined) {
|
|
1039
|
+
root.dataset.totalCount = String(detail.totalCount);
|
|
1040
|
+
if (pagination) pagination.dataset.totalCount = String(detail.totalCount);
|
|
1041
|
+
}
|
|
1042
|
+
if (detail.page !== undefined && pagination) pagination.dataset.page = String(detail.page);
|
|
1043
|
+
renderPagination();
|
|
1044
|
+
syncSelection();
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// ------------------------------------------------------------------- sync
|
|
1048
|
+
|
|
1049
|
+
const bindWidgets = () => {
|
|
1050
|
+
table.querySelectorAll<HTMLElement>("[data-table-menu]").forEach((menu) => {
|
|
1051
|
+
if (boundWidgets.has(menu)) return;
|
|
1052
|
+
boundWidgets.add(menu);
|
|
1053
|
+
bindMenuCell(menu);
|
|
1054
|
+
});
|
|
1055
|
+
table.querySelectorAll<HTMLElement>("[data-table-combobox]").forEach((combobox) => {
|
|
1056
|
+
if (boundWidgets.has(combobox)) return;
|
|
1057
|
+
boundWidgets.add(combobox);
|
|
1058
|
+
bindCombobox(combobox);
|
|
1059
|
+
});
|
|
1060
|
+
table.querySelectorAll<HTMLElement>('[data-slot="table-resize-handle"]').forEach((handle) => {
|
|
1061
|
+
if (boundWidgets.has(handle)) return;
|
|
1062
|
+
boundWidgets.add(handle);
|
|
1063
|
+
bindResize(handle);
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
const sync = () => {
|
|
1068
|
+
repositionErrors();
|
|
1069
|
+
bindWidgets();
|
|
1070
|
+
applyTree();
|
|
1071
|
+
applyColumns();
|
|
1072
|
+
applyPinned();
|
|
1073
|
+
syncSortAvailability();
|
|
1074
|
+
syncSelection();
|
|
1075
|
+
ensureTabStop();
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
let scheduled = false;
|
|
1079
|
+
new MutationObserver(() => {
|
|
1080
|
+
if (scheduled) return;
|
|
1081
|
+
scheduled = true;
|
|
1082
|
+
requestAnimationFrame(() => {
|
|
1083
|
+
scheduled = false;
|
|
1084
|
+
sync();
|
|
1085
|
+
});
|
|
1086
|
+
}).observe(table, { childList: true, subtree: true });
|
|
1087
|
+
|
|
1088
|
+
sync();
|
|
1089
|
+
renderPagination();
|
|
1090
|
+
}
|