@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,66 @@
|
|
|
1
|
+
import { inject, provide, reactive } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type ToastTone = 'neutral' | 'success' | 'warn' | 'danger';
|
|
4
|
+
|
|
5
|
+
export type Toast = {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
tone?: ToastTone;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ToastApi = {
|
|
14
|
+
toasts: Toast[];
|
|
15
|
+
push: (t: Omit<Toast, 'id'>) => void;
|
|
16
|
+
remove: (id: string) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const KEY = Symbol('ngb-toasts');
|
|
20
|
+
|
|
21
|
+
export function provideToasts(): ToastApi {
|
|
22
|
+
const state = reactive({
|
|
23
|
+
toasts: [] as Toast[],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function remove(id: string) {
|
|
27
|
+
state.toasts = state.toasts.filter(t => t.id !== id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function push(t: Omit<Toast, 'id'>) {
|
|
31
|
+
const id = crypto?.randomUUID?.() ?? String(Date.now() + Math.random());
|
|
32
|
+
const toast: Toast = {
|
|
33
|
+
id,
|
|
34
|
+
tone: 'neutral',
|
|
35
|
+
timeoutMs: 3500,
|
|
36
|
+
...t,
|
|
37
|
+
};
|
|
38
|
+
state.toasts = [toast, ...state.toasts].slice(0, 5);
|
|
39
|
+
|
|
40
|
+
const timeout = toast.timeoutMs ?? 0;
|
|
41
|
+
if (timeout > 0) {
|
|
42
|
+
window.setTimeout(() => remove(id), timeout);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const api: ToastApi = {
|
|
47
|
+
get toasts() {
|
|
48
|
+
return state.toasts;
|
|
49
|
+
},
|
|
50
|
+
push,
|
|
51
|
+
remove,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
provide(KEY, api);
|
|
55
|
+
return api;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useToasts(): ToastApi {
|
|
59
|
+
const api = inject<ToastApi>(KEY);
|
|
60
|
+
if (!api) throw new Error('useToasts(): missing provideToasts()');
|
|
61
|
+
return api;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function useOptionalToasts(): ToastApi | null {
|
|
65
|
+
return inject<ToastApi>(KEY, null);
|
|
66
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { computed, onBeforeUnmount, ref, watch, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
type UseAsyncComboboxQueryArgs<TItem> = {
|
|
4
|
+
disabled: ComputedRef<boolean>
|
|
5
|
+
items: ComputedRef<readonly TItem[]>
|
|
6
|
+
emitQuery: (query: string) => void
|
|
7
|
+
debounceMs?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ResetOptions = {
|
|
11
|
+
emitEmptyQuery?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useAsyncComboboxQuery<TItem>(args: UseAsyncComboboxQueryArgs<TItem>) {
|
|
15
|
+
const query = ref('')
|
|
16
|
+
const pendingEmit = ref(false)
|
|
17
|
+
const pendingResults = ref(false)
|
|
18
|
+
const lastSentQuery = ref('')
|
|
19
|
+
const debounceMs = args.debounceMs ?? 220
|
|
20
|
+
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
22
|
+
|
|
23
|
+
const isSearching = computed(() => pendingEmit.value || pendingResults.value)
|
|
24
|
+
|
|
25
|
+
function clearTimer(): void {
|
|
26
|
+
if (!timer) return
|
|
27
|
+
clearTimeout(timer)
|
|
28
|
+
timer = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearPendingState(): void {
|
|
32
|
+
pendingEmit.value = false
|
|
33
|
+
pendingResults.value = false
|
|
34
|
+
lastSentQuery.value = ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resetQueryState(options: ResetOptions = {}): void {
|
|
38
|
+
query.value = ''
|
|
39
|
+
clearTimer()
|
|
40
|
+
clearPendingState()
|
|
41
|
+
if (options.emitEmptyQuery) args.emitQuery('')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onInput(value: string): void {
|
|
45
|
+
if (args.disabled.value) return
|
|
46
|
+
|
|
47
|
+
query.value = value
|
|
48
|
+
const trimmed = value.trim()
|
|
49
|
+
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
resetQueryState({ emitEmptyQuery: true })
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pendingEmit.value = true
|
|
56
|
+
pendingResults.value = false
|
|
57
|
+
|
|
58
|
+
clearTimer()
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
|
+
pendingEmit.value = false
|
|
61
|
+
pendingResults.value = true
|
|
62
|
+
lastSentQuery.value = query.value
|
|
63
|
+
args.emitQuery(query.value)
|
|
64
|
+
}, debounceMs)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
watch(
|
|
68
|
+
() => args.items.value,
|
|
69
|
+
() => {
|
|
70
|
+
if (pendingResults.value && query.value === lastSentQuery.value) {
|
|
71
|
+
pendingResults.value = false
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
onBeforeUnmount(() => clearTimer())
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
query,
|
|
80
|
+
isSearching,
|
|
81
|
+
clearPendingState,
|
|
82
|
+
resetQueryState,
|
|
83
|
+
onInput,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { computed, onBeforeUnmount, onMounted, ref, type ComponentPublicInstance, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
type FloatingHost = Element | ComponentPublicInstance | null
|
|
4
|
+
type FloatingHostRef = Ref<FloatingHost>
|
|
5
|
+
|
|
6
|
+
const DROPDOWN_GAP_PX = 8
|
|
7
|
+
const VIEWPORT_PADDING_PX = 8
|
|
8
|
+
const DEFAULT_DROPDOWN_HEIGHT_PX = 288
|
|
9
|
+
|
|
10
|
+
function resolveHostElement(host: FloatingHost | undefined): HTMLElement | null {
|
|
11
|
+
const raw = host && '$el' in host ? host.$el : host
|
|
12
|
+
return raw instanceof HTMLElement ? raw : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useFloatingDropdownPosition(anchorRef: FloatingHostRef, overlayRefs: FloatingHostRef[]) {
|
|
16
|
+
const floatingLeft = ref(0)
|
|
17
|
+
const floatingTop = ref(0)
|
|
18
|
+
const floatingWidth = ref(0)
|
|
19
|
+
const floatingMaxHeight = ref(DEFAULT_DROPDOWN_HEIGHT_PX)
|
|
20
|
+
let rafId: number | null = null
|
|
21
|
+
|
|
22
|
+
const floatingStyle = computed(() => ({
|
|
23
|
+
left: `${floatingLeft.value}px`,
|
|
24
|
+
top: `${floatingTop.value}px`,
|
|
25
|
+
width: `${floatingWidth.value}px`,
|
|
26
|
+
maxHeight: `${floatingMaxHeight.value}px`,
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
function resolveViewportBounds() {
|
|
30
|
+
if (typeof window === 'undefined') {
|
|
31
|
+
return {
|
|
32
|
+
width: 0,
|
|
33
|
+
height: 0,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const viewport = window.visualViewport
|
|
38
|
+
return {
|
|
39
|
+
width: Math.round(viewport?.width ?? window.innerWidth ?? Number.MAX_SAFE_INTEGER),
|
|
40
|
+
height: Math.round(viewport?.height ?? window.innerHeight ?? Number.MAX_SAFE_INTEGER),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveOverlayHeight() {
|
|
45
|
+
const overlayEl = overlayRefs
|
|
46
|
+
.map((overlayRef) => resolveHostElement(overlayRef.value))
|
|
47
|
+
.find((element) => !!element)
|
|
48
|
+
|
|
49
|
+
const renderedHeight = overlayEl?.offsetHeight ?? 0
|
|
50
|
+
return renderedHeight > 0 ? renderedHeight : DEFAULT_DROPDOWN_HEIGHT_PX
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function computePosition() {
|
|
54
|
+
const anchorEl = resolveHostElement(anchorRef.value)
|
|
55
|
+
if (!anchorEl) return
|
|
56
|
+
|
|
57
|
+
const rect = anchorEl.getBoundingClientRect()
|
|
58
|
+
const viewport = resolveViewportBounds()
|
|
59
|
+
const overlayHeight = resolveOverlayHeight()
|
|
60
|
+
const maxWidth = Math.max(0, viewport.width - VIEWPORT_PADDING_PX * 2)
|
|
61
|
+
const width = Math.max(0, Math.min(Math.round(rect.width), maxWidth))
|
|
62
|
+
const left = Math.min(
|
|
63
|
+
Math.max(Math.round(rect.left), VIEWPORT_PADDING_PX),
|
|
64
|
+
Math.max(VIEWPORT_PADDING_PX, viewport.width - VIEWPORT_PADDING_PX - width),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const availableBelow = Math.max(0, viewport.height - rect.bottom - DROPDOWN_GAP_PX - VIEWPORT_PADDING_PX)
|
|
68
|
+
const availableAbove = Math.max(0, rect.top - DROPDOWN_GAP_PX - VIEWPORT_PADDING_PX)
|
|
69
|
+
const shouldOpenAbove = availableAbove > availableBelow && availableBelow < overlayHeight
|
|
70
|
+
const availableHeight = shouldOpenAbove ? availableAbove : availableBelow
|
|
71
|
+
const maxHeight = Math.max(0, Math.min(DEFAULT_DROPDOWN_HEIGHT_PX, Math.round(availableHeight)))
|
|
72
|
+
const renderedHeight = Math.min(overlayHeight, maxHeight || overlayHeight)
|
|
73
|
+
const top = shouldOpenAbove
|
|
74
|
+
? Math.max(VIEWPORT_PADDING_PX, Math.round(rect.top - DROPDOWN_GAP_PX - renderedHeight))
|
|
75
|
+
: Math.round(rect.bottom + DROPDOWN_GAP_PX)
|
|
76
|
+
|
|
77
|
+
floatingLeft.value = left
|
|
78
|
+
floatingTop.value = top
|
|
79
|
+
floatingWidth.value = width
|
|
80
|
+
floatingMaxHeight.value = maxHeight
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function schedulePositionRecalc() {
|
|
84
|
+
if (typeof window === 'undefined') return
|
|
85
|
+
if (typeof window.requestAnimationFrame !== 'function') {
|
|
86
|
+
computePosition()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (rafId != null && typeof window.cancelAnimationFrame === 'function') {
|
|
90
|
+
window.cancelAnimationFrame(rafId)
|
|
91
|
+
}
|
|
92
|
+
rafId = window.requestAnimationFrame(() => {
|
|
93
|
+
rafId = null
|
|
94
|
+
computePosition()
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updatePosition() {
|
|
99
|
+
computePosition()
|
|
100
|
+
schedulePositionRecalc()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onWindowChange() {
|
|
104
|
+
const hasVisibleOverlay = overlayRefs.some((overlayRef) => !!resolveHostElement(overlayRef.value))
|
|
105
|
+
if (!hasVisibleOverlay) return
|
|
106
|
+
computePosition()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onMounted(() => {
|
|
110
|
+
if (typeof window === 'undefined') return
|
|
111
|
+
window.addEventListener('resize', onWindowChange)
|
|
112
|
+
window.addEventListener('scroll', onWindowChange, true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
onBeforeUnmount(() => {
|
|
116
|
+
if (typeof window === 'undefined') return
|
|
117
|
+
window.removeEventListener('resize', onWindowChange)
|
|
118
|
+
window.removeEventListener('scroll', onWindowChange, true)
|
|
119
|
+
if (rafId != null && typeof window.cancelAnimationFrame === 'function') {
|
|
120
|
+
window.cancelAnimationFrame(rafId)
|
|
121
|
+
}
|
|
122
|
+
rafId = null
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
floatingStyle,
|
|
127
|
+
updatePosition,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts" generic="TItem">
|
|
2
|
+
import type { VNodeChild } from 'vue'
|
|
3
|
+
import NgbButton from '../primitives/NgbButton.vue'
|
|
4
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
5
|
+
|
|
6
|
+
type ComposerCollectionColumn = {
|
|
7
|
+
title: string
|
|
8
|
+
width?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
title: string
|
|
13
|
+
addLabel: string
|
|
14
|
+
items: TItem[]
|
|
15
|
+
columns: ComposerCollectionColumn[]
|
|
16
|
+
emptyMessage: string
|
|
17
|
+
section: string
|
|
18
|
+
rowKey: (item: TItem, index: number) => string
|
|
19
|
+
addDisabled?: boolean
|
|
20
|
+
tableClass?: string
|
|
21
|
+
}>(), {
|
|
22
|
+
addDisabled: false,
|
|
23
|
+
tableClass: 'w-full table-fixed text-sm',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
(e: 'add'): void
|
|
28
|
+
(e: 'remove', index: number): void
|
|
29
|
+
(e: 'dragstart', payload: { section: string; index: number; event: DragEvent }): void
|
|
30
|
+
(e: 'dragover', event: DragEvent): void
|
|
31
|
+
(e: 'drop', payload: { section: string; index: number; event: DragEvent }): void
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
defineSlots<{
|
|
35
|
+
cells(props: { item: TItem; index: number }): VNodeChild
|
|
36
|
+
}>()
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="space-y-4">
|
|
41
|
+
<div class="flex items-center justify-between gap-3">
|
|
42
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-ngb-muted">{{ title }}</div>
|
|
43
|
+
<NgbButton size="sm" :disabled="addDisabled" @click="emit('add')">
|
|
44
|
+
<NgbIcon name="plus" :size="16" />
|
|
45
|
+
<span>{{ addLabel }}</span>
|
|
46
|
+
</NgbButton>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="overflow-x-auto overflow-y-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card">
|
|
50
|
+
<table v-if="items.length > 0" :class="tableClass">
|
|
51
|
+
<colgroup>
|
|
52
|
+
<col style="width: 28px" />
|
|
53
|
+
<col v-for="(column, index) in columns" :key="`${title}:col:${index}`" :style="column.width ? `width: ${column.width}` : undefined" />
|
|
54
|
+
<col style="width: 40px" />
|
|
55
|
+
</colgroup>
|
|
56
|
+
<thead class="bg-ngb-bg text-xs text-ngb-muted">
|
|
57
|
+
<tr>
|
|
58
|
+
<th class="px-2 py-2"></th>
|
|
59
|
+
<th
|
|
60
|
+
v-for="(column, index) in columns"
|
|
61
|
+
:key="`${title}:head:${index}`"
|
|
62
|
+
class="border-r border-dotted border-ngb-border px-3 py-2 text-left font-semibold truncate"
|
|
63
|
+
>
|
|
64
|
+
{{ column.title }}
|
|
65
|
+
</th>
|
|
66
|
+
<th class="px-2 py-2"></th>
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody>
|
|
70
|
+
<tr
|
|
71
|
+
v-for="(item, index) in items"
|
|
72
|
+
:key="rowKey(item, index)"
|
|
73
|
+
class="border-t border-ngb-border transition-colors hover:bg-ngb-bg align-top"
|
|
74
|
+
draggable="true"
|
|
75
|
+
@dragstart="emit('dragstart', { section, index, event: $event })"
|
|
76
|
+
@dragover="emit('dragover', $event)"
|
|
77
|
+
@drop="emit('drop', { section, index, event: $event })"
|
|
78
|
+
>
|
|
79
|
+
<td class="px-1 py-1 align-middle">
|
|
80
|
+
<div class="flex h-8 w-6 items-center justify-center text-ngb-muted cursor-grab active:cursor-grabbing" title="Drag to reorder">
|
|
81
|
+
<NgbIcon name="grip-vertical" :size="16" />
|
|
82
|
+
</div>
|
|
83
|
+
</td>
|
|
84
|
+
|
|
85
|
+
<slot name="cells" :item="item" :index="index" />
|
|
86
|
+
|
|
87
|
+
<td class="px-1 py-1 align-middle">
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
class="flex h-8 w-8 items-center justify-center rounded-[var(--ngb-radius)] text-ngb-muted hover:bg-ngb-bg hover:text-ngb-text ngb-focus"
|
|
91
|
+
title="Delete"
|
|
92
|
+
@click="emit('remove', index)"
|
|
93
|
+
>
|
|
94
|
+
<NgbIcon name="trash" :size="16" />
|
|
95
|
+
</button>
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
|
|
101
|
+
<div v-else class="px-4 py-3 text-sm text-ngb-muted">
|
|
102
|
+
{{ emptyMessage }}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|