@ngbplatform/ui 1.3.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/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full min-w-0 overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card" :class="fillHeight ? 'flex flex-col min-h-0' : ''">
|
|
3
|
+
<div v-if="showPanel" class="px-4 py-3 border-b border-ngb-border flex items-center justify-between gap-3">
|
|
4
|
+
<div class="min-w-0">
|
|
5
|
+
<div class="text-sm font-semibold text-ngb-text truncate">{{ title }}</div>
|
|
6
|
+
<div class="text-xs text-ngb-muted truncate">{{ subtitle }}</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="flex items-center gap-2">
|
|
10
|
+
<slot name="toolbar" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Sticky header -->
|
|
15
|
+
<div class="bg-[var(--ngb-grid-header)] border-b border-ngb-border sticky top-0 z-10">
|
|
16
|
+
<div
|
|
17
|
+
class="grid items-center text-sm font-semibold text-ngb-text select-none"
|
|
18
|
+
:style="{ gridTemplateColumns }"
|
|
19
|
+
>
|
|
20
|
+
<div v-if="showStatusColumn" class="px-0 py-2 flex items-center justify-center">
|
|
21
|
+
<!--
|
|
22
|
+
Status column header is intentionally blank by default.
|
|
23
|
+
Some screens (e.g. Chart of Accounts) may use this as a global group expand/collapse toggle.
|
|
24
|
+
-->
|
|
25
|
+
<slot
|
|
26
|
+
name="statusHeader"
|
|
27
|
+
:has-groups="hasGroups"
|
|
28
|
+
:all-groups-expanded="allGroupsExpanded"
|
|
29
|
+
:toggle-all-groups="toggleAllGroups"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
v-for="col in visibleColumns"
|
|
35
|
+
:key="col.key"
|
|
36
|
+
class="relative px-3 py-2 whitespace-nowrap flex items-center gap-2 border-l border-ngb-border min-w-0 overflow-hidden"
|
|
37
|
+
:style="stickyStyle(col)"
|
|
38
|
+
draggable="true"
|
|
39
|
+
@dragstart="onDragStart(col.key)"
|
|
40
|
+
@dragover.prevent
|
|
41
|
+
@drop="onDrop(col.key)"
|
|
42
|
+
>
|
|
43
|
+
<button
|
|
44
|
+
v-if="col.sortable !== false"
|
|
45
|
+
class="flex items-center gap-1 hover:text-ngb-blue ngb-focus rounded-[var(--ngb-radius)] px-1 -mx-1 min-w-0"
|
|
46
|
+
@click="toggleSort(col.key, $event)"
|
|
47
|
+
:title="sortHint(col.key)"
|
|
48
|
+
>
|
|
49
|
+
<span class="truncate">{{ col.title }}</span>
|
|
50
|
+
<span v-if="sortIndex(col.key) >= 0" class="text-xs text-ngb-muted">
|
|
51
|
+
{{ sortBadge(col.key) }}
|
|
52
|
+
</span>
|
|
53
|
+
</button>
|
|
54
|
+
<span v-else>{{ col.title }}</span>
|
|
55
|
+
|
|
56
|
+
<!-- resize handle -->
|
|
57
|
+
<div
|
|
58
|
+
class="absolute right-0 top-0 h-full w-2 cursor-col-resize"
|
|
59
|
+
@pointerdown.prevent="startResize(col.key, $event)"
|
|
60
|
+
title="Resize"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- viewport -->
|
|
67
|
+
<div
|
|
68
|
+
ref="viewport"
|
|
69
|
+
:class="fillHeight ? 'flex-1 min-h-0 overflow-auto' : 'overflow-auto'"
|
|
70
|
+
:style="fillHeight ? {} : { height: heightPx + 'px' }"
|
|
71
|
+
tabindex="0"
|
|
72
|
+
@scroll="onScroll"
|
|
73
|
+
@keydown="onKeyDown"
|
|
74
|
+
>
|
|
75
|
+
<div class="relative" :style="{ height: totalHeight + 'px' }">
|
|
76
|
+
<div class="absolute left-0 right-0" :style="{ transform: `translateY(${offsetTop}px)` }">
|
|
77
|
+
<div
|
|
78
|
+
v-for="(row, idx) in visibleRows"
|
|
79
|
+
:key="row.key"
|
|
80
|
+
class="border-b border-ngb-border"
|
|
81
|
+
>
|
|
82
|
+
<!-- group header -->
|
|
83
|
+
<button
|
|
84
|
+
v-if="row.type === 'group'"
|
|
85
|
+
class="w-full text-left grid items-center text-sm bg-ngb-card hover:bg-[var(--ngb-row-hover)]"
|
|
86
|
+
:style="{ gridTemplateColumns }"
|
|
87
|
+
@click="toggleGroup(row.groupId)"
|
|
88
|
+
>
|
|
89
|
+
<div v-if="showStatusColumn" class="px-0 py-2 flex items-center justify-center">
|
|
90
|
+
<span
|
|
91
|
+
v-if="showGroupToggleIcons"
|
|
92
|
+
class="inline-flex h-5 w-5 items-center justify-center text-ngb-muted"
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
>
|
|
95
|
+
<NgbIcon :name="isGroupExpanded(row.groupId) ? 'minus' : 'plus'" :size="14" />
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="px-3 py-2 flex items-center gap-2" :style="{ gridColumn: showStatusColumn ? '2 / -1' : '1 / -1' }">
|
|
99
|
+
<span class="font-semibold text-ngb-text">
|
|
100
|
+
{{ row.label }}<template v-if="showGroupCounts"> ({{ row.count }})</template>
|
|
101
|
+
</span>
|
|
102
|
+
<span v-if="showTotals" class="ml-auto text-xs text-ngb-muted tabular-nums">
|
|
103
|
+
D {{ fmt(row.totalDebit) }} · C {{ fmt(row.totalCredit) }}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
<!-- data row -->
|
|
109
|
+
<div
|
|
110
|
+
v-else
|
|
111
|
+
class="grid text-sm"
|
|
112
|
+
:class="rowClass(row)"
|
|
113
|
+
:style="{ gridTemplateColumns }"
|
|
114
|
+
@click="onRowClick(row.key, row.__index, $event)"
|
|
115
|
+
>
|
|
116
|
+
<div v-if="showStatusColumn" class="px-3 py-2 flex items-center justify-center">
|
|
117
|
+
<NgbStatusIcon v-if="showRowStatusIcons" :status="inferRowStatus(row)" :title="statusTitle(row)" />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div
|
|
121
|
+
v-for="(col, colIdx) in visibleColumns"
|
|
122
|
+
:key="col.key"
|
|
123
|
+
class="px-3 py-2 min-w-0 overflow-hidden border-l border-ngb-border self-stretch"
|
|
124
|
+
:class="cellClass(col, row[col.key])"
|
|
125
|
+
:style="cellStickyStyle(col)"
|
|
126
|
+
>
|
|
127
|
+
<div class="flex h-full min-w-0 gap-2 items-center" :class="cellInnerClass(col)">
|
|
128
|
+
<template v-if="Array.isArray(row[col.key])">
|
|
129
|
+
<div class="min-w-0 py-0.5 self-center" :class="col.wrap ? 'whitespace-normal break-words' : ''">
|
|
130
|
+
<div
|
|
131
|
+
v-for="(line, lineIdx) in formatCellLines(col, row[col.key], row)"
|
|
132
|
+
:key="lineIdx"
|
|
133
|
+
:class="col.wrap ? 'leading-5 whitespace-normal break-words' : 'truncate leading-5'"
|
|
134
|
+
>{{ line }}</div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
<template v-else>
|
|
138
|
+
<input
|
|
139
|
+
v-if="col.type === 'checkbox'"
|
|
140
|
+
type="checkbox"
|
|
141
|
+
class="h-4 w-4 rounded border-ngb-border text-ngb-blue"
|
|
142
|
+
:checked="asBool(row[col.key])"
|
|
143
|
+
:aria-label="`${col.title}: ${asBool(row[col.key]) ? 'Yes' : 'No'}`"
|
|
144
|
+
disabled
|
|
145
|
+
>
|
|
146
|
+
<span v-else :class="col.wrap ? 'whitespace-normal break-words leading-5' : 'truncate'">{{ formatCell(col, row[col.key], row) }}</span>
|
|
147
|
+
</template>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div v-if="showTotals" class="px-4 py-3 text-sm flex items-center justify-between bg-ngb-card">
|
|
157
|
+
<div class="text-ngb-muted">Total</div>
|
|
158
|
+
<div class="flex items-center gap-6 tabular-nums">
|
|
159
|
+
<div><span class="text-ngb-muted">Debit</span> <span class="font-semibold">{{ fmt(totals.debit) }}</span></div>
|
|
160
|
+
<div><span class="text-ngb-muted">Credit</span> <span class="font-semibold">{{ fmt(totals.credit) }}</span></div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<script setup lang="ts">
|
|
167
|
+
import { computed, ref } from 'vue';
|
|
168
|
+
import NgbStatusIcon from '../../primitives/NgbStatusIcon.vue';
|
|
169
|
+
import NgbIcon from '../../primitives/NgbIcon.vue';
|
|
170
|
+
import type { RegisterColumn, RegisterDataRow, RegisterSortSpec } from './registerTypes';
|
|
171
|
+
import { useRegisterColumnResize } from './useRegisterColumnResize';
|
|
172
|
+
import { useRegisterColumnState } from './useRegisterColumnState';
|
|
173
|
+
import { useRegisterRows } from './useRegisterRows';
|
|
174
|
+
import { useRegisterViewport } from './useRegisterViewport';
|
|
175
|
+
|
|
176
|
+
const props = withDefaults(defineProps<{
|
|
177
|
+
title?: string;
|
|
178
|
+
subtitle?: string;
|
|
179
|
+
showPanel?: boolean;
|
|
180
|
+
heightPx?: number;
|
|
181
|
+
fillHeight?: boolean;
|
|
182
|
+
columns: RegisterColumn[];
|
|
183
|
+
rows: RegisterDataRow[];
|
|
184
|
+
groupBy?: string[];
|
|
185
|
+
showTotals?: boolean;
|
|
186
|
+
defaultExpanded?: boolean;
|
|
187
|
+
storageKey?: string; // persist column order + widths + pinned/visible for demo
|
|
188
|
+
visibleColumnKeys?: string[];
|
|
189
|
+
sortBy?: RegisterSortSpec[];
|
|
190
|
+
selectedKeys?: string[];
|
|
191
|
+
// If true, single-click activates the row (emits rowActivate).
|
|
192
|
+
// Multi-select modifiers (Ctrl/Meta/Shift) keep selection behavior without activation.
|
|
193
|
+
activateOnRowClick?: boolean;
|
|
194
|
+
rowHeightPx?: number;
|
|
195
|
+
showStatusColumn?: boolean;
|
|
196
|
+
showGroupCounts?: boolean;
|
|
197
|
+
showGroupToggleIcons?: boolean;
|
|
198
|
+
showRowStatusIcons?: boolean;
|
|
199
|
+
}>(), {
|
|
200
|
+
showPanel: true,
|
|
201
|
+
heightPx: 460,
|
|
202
|
+
fillHeight: false,
|
|
203
|
+
showTotals: true,
|
|
204
|
+
defaultExpanded: false,
|
|
205
|
+
activateOnRowClick: false,
|
|
206
|
+
rowHeightPx: 36,
|
|
207
|
+
showStatusColumn: true,
|
|
208
|
+
showGroupCounts: true,
|
|
209
|
+
showGroupToggleIcons: false,
|
|
210
|
+
showRowStatusIcons: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const emit = defineEmits<{
|
|
214
|
+
(e: 'update:groupBy', v: string[]): void;
|
|
215
|
+
(e: 'update:visibleColumnKeys', v: string[]): void;
|
|
216
|
+
(e: 'update:sortBy', v: RegisterSortSpec[]): void;
|
|
217
|
+
(e: 'update:selectedKeys', v: string[]): void;
|
|
218
|
+
(e: 'rowActivate', key: string): void;
|
|
219
|
+
}>();
|
|
220
|
+
|
|
221
|
+
const title = computed(() => props.title ?? 'Register');
|
|
222
|
+
const subtitle = computed(() => props.subtitle ?? '');
|
|
223
|
+
const showPanel = computed(() => props.showPanel);
|
|
224
|
+
const heightPx = computed(() => props.heightPx);
|
|
225
|
+
const fillHeight = computed(() => props.fillHeight);
|
|
226
|
+
const showTotals = computed(() => props.showTotals);
|
|
227
|
+
|
|
228
|
+
const statusColWidth = computed(() => props.showStatusColumn ? 40 : 0); // px (icon-only status column)
|
|
229
|
+
const showStatusColumn = computed(() => props.showStatusColumn);
|
|
230
|
+
const showGroupCounts = computed(() => props.showGroupCounts);
|
|
231
|
+
const showGroupToggleIcons = computed(() => props.showGroupToggleIcons);
|
|
232
|
+
const showRowStatusIcons = computed(() => props.showRowStatusIcons);
|
|
233
|
+
|
|
234
|
+
const rowHeight = computed(() => props.rowHeightPx);
|
|
235
|
+
const viewport = ref<HTMLElement | null>(null);
|
|
236
|
+
|
|
237
|
+
function fmt(v: number) {
|
|
238
|
+
if (!v) return '—';
|
|
239
|
+
const n = Math.round(v * 100) / 100;
|
|
240
|
+
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sortBy = computed<RegisterSortSpec[]>(() => props.sortBy ?? []);
|
|
244
|
+
const groupBy = computed(() => props.groupBy ?? []);
|
|
245
|
+
const defaultExpanded = computed(() => props.defaultExpanded);
|
|
246
|
+
|
|
247
|
+
const {
|
|
248
|
+
localOrder,
|
|
249
|
+
localWidths,
|
|
250
|
+
visibleColumns,
|
|
251
|
+
gridTemplateColumns,
|
|
252
|
+
colWidth,
|
|
253
|
+
stickyStyle,
|
|
254
|
+
cellStickyStyle,
|
|
255
|
+
} = useRegisterColumnState({
|
|
256
|
+
columns: computed(() => props.columns),
|
|
257
|
+
visibleColumnKeys: computed(() => props.visibleColumnKeys),
|
|
258
|
+
storageKey: computed(() => props.storageKey),
|
|
259
|
+
showStatusColumn,
|
|
260
|
+
statusColWidth,
|
|
261
|
+
emitVisibleColumnKeys: (value) => emit('update:visibleColumnKeys', value),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const { startResize } = useRegisterColumnResize({
|
|
265
|
+
columns: computed(() => props.columns),
|
|
266
|
+
localWidths,
|
|
267
|
+
colWidth,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const {
|
|
271
|
+
hasGroups,
|
|
272
|
+
displayRows,
|
|
273
|
+
dataRows,
|
|
274
|
+
totals,
|
|
275
|
+
allGroupsExpanded,
|
|
276
|
+
isGroupExpanded,
|
|
277
|
+
toggleGroup,
|
|
278
|
+
toggleAllGroups,
|
|
279
|
+
inferRowStatus,
|
|
280
|
+
} = useRegisterRows({
|
|
281
|
+
rows: computed(() => props.rows),
|
|
282
|
+
groupBy,
|
|
283
|
+
sortBy,
|
|
284
|
+
defaultExpanded,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const { onScroll, totalHeight, offsetTop, visibleRows } = useRegisterViewport({
|
|
288
|
+
viewport,
|
|
289
|
+
heightPx,
|
|
290
|
+
rowHeight,
|
|
291
|
+
displayRows,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
function sortIndex(key: string) {
|
|
295
|
+
return sortBy.value.findIndex(s => s.key === key);
|
|
296
|
+
}
|
|
297
|
+
function sortBadge(key: string) {
|
|
298
|
+
const i = sortIndex(key);
|
|
299
|
+
if (i < 0) return '';
|
|
300
|
+
const dir = sortBy.value[i].dir === 'asc' ? '↑' : '↓';
|
|
301
|
+
return `${i + 1}${dir}`;
|
|
302
|
+
}
|
|
303
|
+
function sortHint(key: string) {
|
|
304
|
+
return 'Click: sort · Shift+Click: multi-sort';
|
|
305
|
+
}
|
|
306
|
+
function toggleSort(key: string, ev: MouseEvent) {
|
|
307
|
+
const multi = ev.shiftKey;
|
|
308
|
+
const current = [...sortBy.value];
|
|
309
|
+
const idx = current.findIndex(s => s.key === key);
|
|
310
|
+
if (idx < 0) {
|
|
311
|
+
const next = multi ? [...current, { key, dir: 'asc' as const }] : [{ key, dir: 'asc' as const }];
|
|
312
|
+
emit('update:sortBy', next);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const dir = current[idx].dir === 'asc' ? 'desc' : 'asc';
|
|
316
|
+
current[idx] = { key, dir };
|
|
317
|
+
emit('update:sortBy', multi ? current : [current[idx]]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ---------------- Selection + keyboard ---------------- */
|
|
321
|
+
const selected = computed(() => new Set(props.selectedKeys ?? []));
|
|
322
|
+
const activeIndex = ref<number>(0);
|
|
323
|
+
const lastSelectedIndex = ref<number | null>(null);
|
|
324
|
+
|
|
325
|
+
function rowClass(row: RegisterDataRow) {
|
|
326
|
+
const key = row.key;
|
|
327
|
+
const isSel = selected.value.has(key);
|
|
328
|
+
const isMarked = inferRowStatus(row) === 'marked';
|
|
329
|
+
return [
|
|
330
|
+
'hover:bg-[var(--ngb-row-hover)] cursor-pointer',
|
|
331
|
+
isSel ? 'bg-[var(--ngb-row-selected)]' : 'bg-ngb-card',
|
|
332
|
+
isMarked ? 'opacity-70' : '',
|
|
333
|
+
].join(' ');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function statusTitle(row: RegisterDataRow): string | undefined {
|
|
337
|
+
return typeof row.__statusTitle === 'string' && row.__statusTitle.trim()
|
|
338
|
+
? row.__statusTitle
|
|
339
|
+
: undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function onRowClick(key: string, index: number, ev: MouseEvent) {
|
|
343
|
+
// ignore group rows (they don't call this)
|
|
344
|
+
const mult = ev.metaKey || ev.ctrlKey;
|
|
345
|
+
const range = ev.shiftKey;
|
|
346
|
+
|
|
347
|
+
const current = new Set(selected.value);
|
|
348
|
+
|
|
349
|
+
if (range && lastSelectedIndex.value != null) {
|
|
350
|
+
const a = Math.min(lastSelectedIndex.value, index);
|
|
351
|
+
const b = Math.max(lastSelectedIndex.value, index);
|
|
352
|
+
const rowKeys = dataRows.value.map((row) => row.key);
|
|
353
|
+
for (let i = a; i <= b && i < rowKeys.length; i++) current.add(rowKeys[i]);
|
|
354
|
+
} else if (mult) {
|
|
355
|
+
if (current.has(key)) current.delete(key);
|
|
356
|
+
else current.add(key);
|
|
357
|
+
lastSelectedIndex.value = index;
|
|
358
|
+
} else {
|
|
359
|
+
current.clear();
|
|
360
|
+
current.add(key);
|
|
361
|
+
lastSelectedIndex.value = index;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
activeIndex.value = index;
|
|
365
|
+
emit('update:selectedKeys', Array.from(current));
|
|
366
|
+
|
|
367
|
+
// Activation: open on single-click when enabled.
|
|
368
|
+
if (props.activateOnRowClick && !mult && !range) {
|
|
369
|
+
emit('rowActivate', key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function onKeyDown(ev: KeyboardEvent) {
|
|
374
|
+
const rowKeys = dataRows.value;
|
|
375
|
+
if (!rowKeys.length) return;
|
|
376
|
+
|
|
377
|
+
const clamp = (v: number) => Math.max(0, Math.min(rowKeys.length - 1, v));
|
|
378
|
+
|
|
379
|
+
if (ev.key === 'ArrowDown') {
|
|
380
|
+
ev.preventDefault();
|
|
381
|
+
activeIndex.value = clamp(activeIndex.value + 1);
|
|
382
|
+
scrollToActive();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (ev.key === 'ArrowUp') {
|
|
386
|
+
ev.preventDefault();
|
|
387
|
+
activeIndex.value = clamp(activeIndex.value - 1);
|
|
388
|
+
scrollToActive();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (ev.key === 'Enter') {
|
|
392
|
+
ev.preventDefault();
|
|
393
|
+
const key = rowKeys[activeIndex.value]?.key;
|
|
394
|
+
if (key) emit('rowActivate', key);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (ev.key === ' ' || ev.key === 'Spacebar') {
|
|
398
|
+
ev.preventDefault();
|
|
399
|
+
const key = rowKeys[activeIndex.value]?.key;
|
|
400
|
+
if (!key) return;
|
|
401
|
+
const current = new Set(selected.value);
|
|
402
|
+
if (current.has(key)) current.delete(key);
|
|
403
|
+
else current.add(key);
|
|
404
|
+
emit('update:selectedKeys', Array.from(current));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function scrollToActive() {
|
|
409
|
+
const vp = viewport.value;
|
|
410
|
+
if (!vp) return;
|
|
411
|
+
const top = activeIndex.value * rowHeight.value;
|
|
412
|
+
const bottom = top + rowHeight.value;
|
|
413
|
+
if (top < vp.scrollTop) vp.scrollTop = top;
|
|
414
|
+
else if (bottom > vp.scrollTop + vp.clientHeight) vp.scrollTop = bottom - vp.clientHeight;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* ---------------- Cell formatting ---------------- */
|
|
418
|
+
function formatCell(col: RegisterColumn, v: unknown, row: RegisterDataRow) {
|
|
419
|
+
if (col.format) return col.format(v, row);
|
|
420
|
+
if (v == null || v === '') return '—';
|
|
421
|
+
if (typeof v === 'number') {
|
|
422
|
+
if (col.align === 'right') return fmt(v);
|
|
423
|
+
return String(v);
|
|
424
|
+
}
|
|
425
|
+
return String(v);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function formatCellLines(col: RegisterColumn, v: unknown, row: RegisterDataRow): string[] {
|
|
429
|
+
if (Array.isArray(v)) {
|
|
430
|
+
const lines = v
|
|
431
|
+
.map((item) => formatCell(col, item, row))
|
|
432
|
+
.filter((item) => item && item !== '—');
|
|
433
|
+
return lines.length > 0 ? lines : ['—'];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return [formatCell(col, v, row)];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function asBool(v: unknown): boolean {
|
|
440
|
+
return v === true || v === 'true' || v === 1 || v === '1';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function cellClass(col: RegisterColumn, v: unknown) {
|
|
444
|
+
const align = col.align === 'right' ? 'text-right tabular-nums' : col.align === 'center' ? 'text-center' : '';
|
|
445
|
+
const neg = typeof v === 'number' && v < 0 ? 'text-ngb-danger' : '';
|
|
446
|
+
return [align, neg].filter(Boolean).join(' ');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function cellInnerClass(col: RegisterColumn) {
|
|
450
|
+
if (col.align === 'right') return 'justify-end';
|
|
451
|
+
if (col.align === 'center') return 'justify-center';
|
|
452
|
+
return '';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* ---------------- Column reorder (drag) ---------------- */
|
|
456
|
+
const dragKey = ref<string | null>(null);
|
|
457
|
+
|
|
458
|
+
function onDragStart(key: string) {
|
|
459
|
+
dragKey.value = key;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function onDrop(targetKey: string) {
|
|
463
|
+
if (!dragKey.value || dragKey.value === targetKey) return;
|
|
464
|
+
const order = [...localOrder.value];
|
|
465
|
+
const from = order.indexOf(dragKey.value);
|
|
466
|
+
const to = order.indexOf(targetKey);
|
|
467
|
+
if (from < 0 || to < 0) return;
|
|
468
|
+
order.splice(from, 1);
|
|
469
|
+
order.splice(to, 0, dragKey.value);
|
|
470
|
+
localOrder.value = order;
|
|
471
|
+
dragKey.value = null;
|
|
472
|
+
}
|
|
473
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type RegisterColumn = {
|
|
2
|
+
key: string;
|
|
3
|
+
title: string;
|
|
4
|
+
width?: number;
|
|
5
|
+
minWidth?: number;
|
|
6
|
+
align?: 'left' | 'right' | 'center';
|
|
7
|
+
pinned?: 'left';
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
wrap?: boolean;
|
|
10
|
+
type?: 'text' | 'checkbox';
|
|
11
|
+
format?: (value: unknown, row: RegisterDataRow) => string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type RegisterDataRow = Record<string, unknown> & {
|
|
15
|
+
key: string;
|
|
16
|
+
debit?: number;
|
|
17
|
+
credit?: number;
|
|
18
|
+
__status?: 'active' | 'saved' | 'posted' | 'marked';
|
|
19
|
+
__statusTitle?: string;
|
|
20
|
+
isMarkedForDeletion?: boolean;
|
|
21
|
+
isDeleted?: boolean;
|
|
22
|
+
isActive?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RegisterSortSpec = {
|
|
26
|
+
key: string;
|
|
27
|
+
dir: 'asc' | 'desc';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type DisplayGroupRow = {
|
|
31
|
+
type: 'group';
|
|
32
|
+
key: string;
|
|
33
|
+
groupId: string;
|
|
34
|
+
label: string;
|
|
35
|
+
count: number;
|
|
36
|
+
totalDebit: number;
|
|
37
|
+
totalCredit: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type DisplayDataRow = RegisterDataRow & {
|
|
41
|
+
type: 'row';
|
|
42
|
+
__index: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type DisplayRow = DisplayGroupRow | DisplayDataRow;
|
|
46
|
+
|
|
47
|
+
export type RowStatus = 'active' | 'saved' | 'posted' | 'marked';
|
|
48
|
+
|
|
49
|
+
export function isDisplayDataRow(row: DisplayRow): row is DisplayDataRow {
|
|
50
|
+
return row.type === 'row';
|
|
51
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { onBeforeUnmount, type ComputedRef, type Ref } from 'vue';
|
|
2
|
+
import type { RegisterColumn } from './registerTypes';
|
|
3
|
+
|
|
4
|
+
type UseRegisterColumnResizeArgs = {
|
|
5
|
+
columns: ComputedRef<RegisterColumn[]>;
|
|
6
|
+
localWidths: Ref<Record<string, number>>;
|
|
7
|
+
colWidth: (column: RegisterColumn) => number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ResizeSession = {
|
|
11
|
+
key: string;
|
|
12
|
+
pointerId: number;
|
|
13
|
+
startX: number;
|
|
14
|
+
startWidth: number;
|
|
15
|
+
minWidth: number;
|
|
16
|
+
handle: HTMLElement | null;
|
|
17
|
+
controller: AbortController;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function useRegisterColumnResize(args: UseRegisterColumnResizeArgs) {
|
|
21
|
+
let activeSession: ResizeSession | null = null;
|
|
22
|
+
|
|
23
|
+
function setPointerCaptureSafely(handle: HTMLElement | null, pointerId: number) {
|
|
24
|
+
if (!handle?.setPointerCapture) return;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
handle.setPointerCapture(pointerId);
|
|
28
|
+
} catch {
|
|
29
|
+
// Firefox can reject synthetic pointer ids in tests; resizing still works without capture.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function releasePointerCaptureSafely(handle: HTMLElement | null, pointerId: number) {
|
|
34
|
+
if (!handle?.releasePointerCapture) return;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
handle.releasePointerCapture(pointerId);
|
|
38
|
+
} catch {
|
|
39
|
+
// Ignore invalid-pointer errors during teardown and fall back to listener cleanup.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stopResize() {
|
|
44
|
+
if (!activeSession) return;
|
|
45
|
+
activeSession.controller.abort();
|
|
46
|
+
releasePointerCaptureSafely(activeSession.handle, activeSession.pointerId);
|
|
47
|
+
activeSession = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function startResize(key: string, event: PointerEvent) {
|
|
51
|
+
const column = args.columns.value.find((entry) => entry.key === key);
|
|
52
|
+
if (!column) return;
|
|
53
|
+
|
|
54
|
+
stopResize();
|
|
55
|
+
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const handle = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
|
58
|
+
const session: ResizeSession = {
|
|
59
|
+
key,
|
|
60
|
+
pointerId: event.pointerId,
|
|
61
|
+
startX: event.clientX,
|
|
62
|
+
startWidth: args.colWidth(column),
|
|
63
|
+
minWidth: column.minWidth ?? 80,
|
|
64
|
+
handle,
|
|
65
|
+
controller,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
activeSession = session;
|
|
69
|
+
setPointerCaptureSafely(handle, event.pointerId);
|
|
70
|
+
|
|
71
|
+
const onMove = (nextEvent: PointerEvent) => {
|
|
72
|
+
if (!activeSession || nextEvent.pointerId !== session.pointerId) return;
|
|
73
|
+
const delta = nextEvent.clientX - session.startX;
|
|
74
|
+
const nextWidth = Math.max(session.minWidth, session.startWidth + delta);
|
|
75
|
+
args.localWidths.value = {
|
|
76
|
+
...args.localWidths.value,
|
|
77
|
+
[session.key]: nextWidth,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onFinish = () => {
|
|
82
|
+
stopResize();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const target: HTMLElement | Window = handle ?? window;
|
|
86
|
+
target.addEventListener('pointermove', onMove, { signal: controller.signal });
|
|
87
|
+
target.addEventListener('pointerup', onFinish, { signal: controller.signal });
|
|
88
|
+
target.addEventListener('pointercancel', onFinish, { signal: controller.signal });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onBeforeUnmount(() => {
|
|
92
|
+
stopResize();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
startResize,
|
|
97
|
+
stopResize,
|
|
98
|
+
};
|
|
99
|
+
}
|