@getdom/studio 0.1.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.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,2308 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} WeekPlannerCell
|
|
4
|
+
* @property {Date} date Local Date instance for the day.
|
|
5
|
+
* @property {string} value YYYY-MM-DD date key shared with DomMonthCalendar.
|
|
6
|
+
* @property {number} day Day of the month.
|
|
7
|
+
* @property {string} weekday Formatted weekday label.
|
|
8
|
+
* @property {string} label Full accessible date label.
|
|
9
|
+
* @property {boolean} currentMonth True when the day belongs to the anchor month.
|
|
10
|
+
* @property {boolean} previousMonth True when the day is before the anchor month.
|
|
11
|
+
* @property {boolean} nextMonth True when the day is after the anchor month.
|
|
12
|
+
* @property {boolean} adjacentMonth True when the day is outside the anchor month.
|
|
13
|
+
* @property {boolean} hidden Always false in the week planner.
|
|
14
|
+
* @property {string} monthValue YYYY-MM month key.
|
|
15
|
+
* @property {number} row Zero-based week index.
|
|
16
|
+
* @property {number} column Zero-based weekday column.
|
|
17
|
+
* @property {number} weekNumber ISO-8601 week number.
|
|
18
|
+
* @property {boolean} today
|
|
19
|
+
* @property {boolean} past
|
|
20
|
+
* @property {boolean} future
|
|
21
|
+
* @property {boolean} muted
|
|
22
|
+
* @property {boolean} disabled
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} WeekPlannerWeek
|
|
27
|
+
* @property {number} index Zero-based week index.
|
|
28
|
+
* @property {number} weekNumber ISO-8601 week number.
|
|
29
|
+
* @property {string} label Accessible week label.
|
|
30
|
+
* @property {string} value YYYY-MM-DD key for the first day in the week.
|
|
31
|
+
* @property {Array<WeekPlannerCell>} days Seven ordered day cells.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} WeekPlannerMonth
|
|
36
|
+
* @property {Date} date Local Date instance for the first day of the month.
|
|
37
|
+
* @property {string} value YYYY-MM month key.
|
|
38
|
+
* @property {string} id Element id for the month.
|
|
39
|
+
* @property {number} month Month number, 1-12.
|
|
40
|
+
* @property {number} year Four-digit year.
|
|
41
|
+
* @property {string} label Formatted month label.
|
|
42
|
+
* @property {Array<WeekPlannerCell>} cells Visible cells belonging to the month.
|
|
43
|
+
* @property {Array<WeekPlannerWeek>} weeks Visible week fragments belonging to the month.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} WeekPlannerTimeSlot
|
|
48
|
+
* @property {number} index Zero-based time-row index.
|
|
49
|
+
* @property {number} minutes Minutes from midnight at the start of the row.
|
|
50
|
+
* @property {number} endMinutes Minutes from midnight at the end of the row.
|
|
51
|
+
* @property {string} time HH:mm start time.
|
|
52
|
+
* @property {string} endTime HH:mm end time.
|
|
53
|
+
* @property {string} label Locale-aware time label.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} WeekPlannerDragState
|
|
58
|
+
* @property {boolean} active True while a planner drag is in progress.
|
|
59
|
+
* @property {boolean} source True for the day where the drag started.
|
|
60
|
+
* @property {boolean} over True for the current drop-preview day.
|
|
61
|
+
* @property {WeekPlannerCell|null} sourceCell Source day when it is visible.
|
|
62
|
+
* @property {WeekPlannerCell|null} overCell Current drop-preview day.
|
|
63
|
+
* @property {unknown} data App-owned data passed to dragAttrs.
|
|
64
|
+
* @property {string} sourceTime HH:mm source time when supplied.
|
|
65
|
+
* @property {string} overTime HH:mm preview time, or an empty string for all-day.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {object} WeekPlannerDragAttrsOptions
|
|
70
|
+
* @property {string} [effectAllowed] Native DataTransfer effectAllowed value.
|
|
71
|
+
* @property {string} [sourceTime] HH:mm time represented by the draggable record.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {(data?: unknown, options?: WeekPlannerDragAttrsOptions) => Record<string, unknown>} WeekPlannerDragAttrs
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {object} WeekPlannerDaySlotPayload
|
|
80
|
+
* @property {WeekPlannerCell} cell
|
|
81
|
+
* @property {WeekPlannerMonth} month
|
|
82
|
+
* @property {WeekPlannerWeek} week
|
|
83
|
+
* @property {Array<WeekPlannerTimeSlot>} timeSlots
|
|
84
|
+
* @property {WeekPlannerDragState} drag
|
|
85
|
+
* @property {WeekPlannerDragAttrs} dragAttrs
|
|
86
|
+
* @property {(recordOrStartTime: Record<string, unknown>|string, endTime?: string) => Record<string, string>} rangeStyle
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} WeekPlannerDayClickPayload
|
|
91
|
+
* @property {WeekPlannerCell} cell
|
|
92
|
+
* @property {string} targetTime HH:mm selected time, or an empty string for the day header or all-day row.
|
|
93
|
+
* @property {number|null} targetMinutes Minutes from midnight, or null for the day header or all-day row.
|
|
94
|
+
* @property {MouseEvent} event
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @typedef {object} WeekPlannerTimeRangePayload
|
|
99
|
+
* @property {WeekPlannerCell} cell Selected day.
|
|
100
|
+
* @property {string} value YYYY-MM-DD selected date key.
|
|
101
|
+
* @property {string} startTime HH:mm inclusive start time.
|
|
102
|
+
* @property {string} endTime HH:mm exclusive end time.
|
|
103
|
+
* @property {number} startMinutes Inclusive minutes after midnight.
|
|
104
|
+
* @property {number} endMinutes Exclusive minutes after midnight.
|
|
105
|
+
* @property {PointerEvent} event Native pointer event that completed the selection.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {object} WeekPlannerDayDragPayload
|
|
110
|
+
* @property {WeekPlannerCell} cell
|
|
111
|
+
* @property {WeekPlannerCell|null} sourceCell
|
|
112
|
+
* @property {WeekPlannerCell} targetCell
|
|
113
|
+
* @property {string} sourceValue
|
|
114
|
+
* @property {string} targetValue
|
|
115
|
+
* @property {string} sourceTime
|
|
116
|
+
* @property {string} targetTime
|
|
117
|
+
* @property {number|null} sourceMinutes
|
|
118
|
+
* @property {number|null} targetMinutes
|
|
119
|
+
* @property {unknown} data
|
|
120
|
+
* @property {DragEvent} event
|
|
121
|
+
* @property {DataTransfer|null} dataTransfer
|
|
122
|
+
* @property {Array<string>} types
|
|
123
|
+
* @property {(type: string) => string} getData
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @typedef {object} WeekPlannerViewportPayload
|
|
128
|
+
* @property {number} scrollLeft Horizontal scroll offset.
|
|
129
|
+
* @property {number} scrollWidth Total scrollable width.
|
|
130
|
+
* @property {number} clientWidth Visible viewport width.
|
|
131
|
+
* @property {number} remaining Pixels remaining before the horizontal end.
|
|
132
|
+
* @property {boolean} atStart Whether the viewport is at the horizontal start threshold.
|
|
133
|
+
* @property {boolean} atEnd Whether the viewport is at the horizontal end threshold.
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @typedef {object} WeekPlannerRenderWindowPayload
|
|
138
|
+
* @property {number} startIndex Inclusive first mounted week index.
|
|
139
|
+
* @property {number} endIndex Exclusive final mounted week index.
|
|
140
|
+
* @property {number} firstVisibleIndex First week intersecting the viewport.
|
|
141
|
+
* @property {number} lastVisibleIndex Last week intersecting the viewport.
|
|
142
|
+
* @property {number} renderedWeeks Number of mounted weeks including overscan.
|
|
143
|
+
* @property {number} totalWeeks Number of weeks in the loaded range.
|
|
144
|
+
* @property {string} start YYYY-MM-DD key for the first mounted day.
|
|
145
|
+
* @property {string} end YYYY-MM-DD key for the final mounted day.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @typedef {(cell: WeekPlannerCell) => boolean} WeekPlannerDisabledDate
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @typedef {string|Array<unknown>|Record<string, boolean>|((cell: WeekPlannerCell) => unknown)} WeekPlannerDayClass
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {string|Record<string, string|number>|((cell: WeekPlannerCell) => string|Record<string, string|number>|null|undefined)} WeekPlannerDayStyle
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
export const weekPlannerProps = {
|
|
161
|
+
startDate: {
|
|
162
|
+
type: String,
|
|
163
|
+
default: '',
|
|
164
|
+
_edit: { group: 'Range', description: 'Date inside the first rendered week as YYYY-MM-DD.' },
|
|
165
|
+
},
|
|
166
|
+
initialMonth: {
|
|
167
|
+
type: Number,
|
|
168
|
+
default: null,
|
|
169
|
+
_edit: { group: 'Range', description: 'Initial month, 1-12. Used when startDate is not set.' },
|
|
170
|
+
},
|
|
171
|
+
initialYear: {
|
|
172
|
+
type: Number,
|
|
173
|
+
default: null,
|
|
174
|
+
_edit: { group: 'Range', description: 'Initial year. Used when startDate is not set.' },
|
|
175
|
+
},
|
|
176
|
+
weeks: {
|
|
177
|
+
type: Number,
|
|
178
|
+
default: 3,
|
|
179
|
+
_edit: { group: 'Range', description: 'Number of consecutive weeks to render as fixed-width day columns.' },
|
|
180
|
+
},
|
|
181
|
+
initialScrollDate: {
|
|
182
|
+
type: String,
|
|
183
|
+
default: '',
|
|
184
|
+
_edit: { group: 'Range', description: 'Date to reveal after mount or a range reset. Useful when a virtual year starts before the focused date.' },
|
|
185
|
+
},
|
|
186
|
+
virtualWeeks: {
|
|
187
|
+
type: Boolean,
|
|
188
|
+
default: false,
|
|
189
|
+
_edit: { group: 'Range', description: 'Mount only the horizontally visible weeks plus the configured buffer.' },
|
|
190
|
+
},
|
|
191
|
+
virtualWeekBuffer: {
|
|
192
|
+
type: Number,
|
|
193
|
+
default: 2,
|
|
194
|
+
_edit: { group: 'Range', description: 'Extra weeks mounted before and after the visible horizontal window.' },
|
|
195
|
+
},
|
|
196
|
+
locale: {
|
|
197
|
+
type: String,
|
|
198
|
+
default: 'en-GB',
|
|
199
|
+
_edit: { description: 'Locale used for weekday, date, month, and time labels.' },
|
|
200
|
+
},
|
|
201
|
+
weekStartsOn: {
|
|
202
|
+
type: Number,
|
|
203
|
+
default: 1,
|
|
204
|
+
_edit: { options: [0, 1, 2, 3, 4, 5, 6], description: 'First day of week. 0 is Sunday, 1 is Monday.' },
|
|
205
|
+
},
|
|
206
|
+
weekdayFormat: {
|
|
207
|
+
type: String,
|
|
208
|
+
default: 'short',
|
|
209
|
+
validator: (value) => ['narrow', 'short', 'long'].includes(value),
|
|
210
|
+
_edit: { options: ['narrow', 'short', 'long'], description: 'Weekday label length.' },
|
|
211
|
+
},
|
|
212
|
+
startTime: {
|
|
213
|
+
type: String,
|
|
214
|
+
default: '07:00',
|
|
215
|
+
_edit: { group: 'Time grid', description: 'First visible time as HH:mm.' },
|
|
216
|
+
},
|
|
217
|
+
endTime: {
|
|
218
|
+
type: String,
|
|
219
|
+
default: '20:00',
|
|
220
|
+
_edit: { group: 'Time grid', description: 'Exclusive final visible time as HH:mm. Use 24:00 to include the full day.' },
|
|
221
|
+
},
|
|
222
|
+
timeStep: {
|
|
223
|
+
type: Number,
|
|
224
|
+
default: 60,
|
|
225
|
+
_edit: { group: 'Time grid', description: 'Minutes represented by each horizontal time row.' },
|
|
226
|
+
},
|
|
227
|
+
slotHeight: {
|
|
228
|
+
type: Number,
|
|
229
|
+
default: 56,
|
|
230
|
+
_edit: { group: 'Time grid', description: 'Pixel height of each time row.' },
|
|
231
|
+
},
|
|
232
|
+
dayWidth: {
|
|
233
|
+
type: Number,
|
|
234
|
+
default: 192,
|
|
235
|
+
_edit: { group: 'Layout', description: 'Fixed pixel width of every day swim lane. Additional weeks scroll horizontally.' },
|
|
236
|
+
},
|
|
237
|
+
timeRailWidth: {
|
|
238
|
+
type: Number,
|
|
239
|
+
default: 72,
|
|
240
|
+
_edit: { group: 'Layout', description: 'Pixel width of the sticky time-label rail.' },
|
|
241
|
+
},
|
|
242
|
+
viewportHeight: {
|
|
243
|
+
type: Number,
|
|
244
|
+
default: 640,
|
|
245
|
+
_edit: { group: 'Layout', description: 'Pixel height of the internal two-axis scroll viewport.' },
|
|
246
|
+
},
|
|
247
|
+
showWeekNumbers: {
|
|
248
|
+
type: Boolean,
|
|
249
|
+
default: false,
|
|
250
|
+
_edit: { group: 'Layout', description: 'Show the ISO week number on the first day of each week.' },
|
|
251
|
+
},
|
|
252
|
+
showAllDayRow: {
|
|
253
|
+
type: Boolean,
|
|
254
|
+
default: true,
|
|
255
|
+
_edit: { group: 'Layout', description: 'Render a sticky all-day row above the timed grid.' },
|
|
256
|
+
},
|
|
257
|
+
showCurrentTime: {
|
|
258
|
+
type: Boolean,
|
|
259
|
+
default: true,
|
|
260
|
+
_edit: { group: 'Layout', description: 'Show a live current-time line in today’s time lane when the current time is visible.' },
|
|
261
|
+
},
|
|
262
|
+
loading: {
|
|
263
|
+
type: Boolean,
|
|
264
|
+
default: false,
|
|
265
|
+
_edit: { group: 'State', description: 'Show an in-viewport loading indicator while the app fetches the requested render window.' },
|
|
266
|
+
},
|
|
267
|
+
mutedBefore: {
|
|
268
|
+
type: String,
|
|
269
|
+
default: '',
|
|
270
|
+
_edit: { description: 'Dates before this YYYY-MM-DD value render muted.' },
|
|
271
|
+
},
|
|
272
|
+
mutedAfter: {
|
|
273
|
+
type: String,
|
|
274
|
+
default: '',
|
|
275
|
+
_edit: { description: 'Dates after this YYYY-MM-DD value render muted.' },
|
|
276
|
+
},
|
|
277
|
+
min: {
|
|
278
|
+
type: String,
|
|
279
|
+
default: '',
|
|
280
|
+
_edit: { group: 'Interaction', description: 'Minimum clickable or droppable date as YYYY-MM-DD.' },
|
|
281
|
+
},
|
|
282
|
+
max: {
|
|
283
|
+
type: String,
|
|
284
|
+
default: '',
|
|
285
|
+
_edit: { group: 'Interaction', description: 'Maximum clickable or droppable date as YYYY-MM-DD.' },
|
|
286
|
+
},
|
|
287
|
+
clickable: {
|
|
288
|
+
type: Boolean,
|
|
289
|
+
default: false,
|
|
290
|
+
_edit: { group: 'Interaction', description: 'Emit day-click with the selected day and snapped time.' },
|
|
291
|
+
},
|
|
292
|
+
rangeSelection: {
|
|
293
|
+
type: Boolean,
|
|
294
|
+
default: false,
|
|
295
|
+
_edit: { group: 'Interaction', description: 'Allow mouse or pen dragging across a time lane and emit time-range-select.' },
|
|
296
|
+
},
|
|
297
|
+
selectionStep: {
|
|
298
|
+
type: Number,
|
|
299
|
+
default: 5,
|
|
300
|
+
_edit: { group: 'Interaction', description: 'Minutes used to snap drag-created time ranges independently of the visible time rows.' },
|
|
301
|
+
},
|
|
302
|
+
dragAndDrop: {
|
|
303
|
+
type: Boolean,
|
|
304
|
+
default: false,
|
|
305
|
+
_edit: { group: 'Interaction', description: 'Expose dragAttrs and emit day-drop with the target day and snapped time.' },
|
|
306
|
+
},
|
|
307
|
+
dropEffect: {
|
|
308
|
+
type: String,
|
|
309
|
+
default: 'move',
|
|
310
|
+
validator: (value) => ['copy', 'move', 'link'].includes(value),
|
|
311
|
+
_edit: { group: 'Interaction', options: ['copy', 'move', 'link'], description: 'Native drop effect shown when a time lane can receive a dragged item.' },
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* @type {WeekPlannerDisabledDate}
|
|
315
|
+
*/
|
|
316
|
+
disabledDate: {
|
|
317
|
+
type: Function,
|
|
318
|
+
default: null,
|
|
319
|
+
_edit: { group: 'Interaction', description: 'Function that receives a day cell and returns true when the day should not be clickable or droppable.' },
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* @type {WeekPlannerDayClass}
|
|
323
|
+
*/
|
|
324
|
+
dayClass: {
|
|
325
|
+
type: [String, Array, Object, Function],
|
|
326
|
+
default: '',
|
|
327
|
+
_edit: { group: 'Styling', description: 'Extra classes, or a function that receives a day cell and returns classes for the day lane.' },
|
|
328
|
+
},
|
|
329
|
+
/**
|
|
330
|
+
* @type {WeekPlannerDayStyle}
|
|
331
|
+
*/
|
|
332
|
+
dayStyle: {
|
|
333
|
+
type: [String, Object, Function],
|
|
334
|
+
default: '',
|
|
335
|
+
_edit: { group: 'Styling', description: 'Extra styles, or a function that receives a day cell and returns styles for the day lane.' },
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
</script>
|
|
339
|
+
|
|
340
|
+
<script setup>
|
|
341
|
+
import {
|
|
342
|
+
computed,
|
|
343
|
+
nextTick,
|
|
344
|
+
onBeforeUnmount,
|
|
345
|
+
onMounted,
|
|
346
|
+
ref,
|
|
347
|
+
useId,
|
|
348
|
+
watch,
|
|
349
|
+
} from 'vue';
|
|
350
|
+
import { calculateVirtualWindow } from '../_shared/calculateVirtualWindow';
|
|
351
|
+
|
|
352
|
+
defineOptions({
|
|
353
|
+
__doc: {
|
|
354
|
+
name: 'Week planner',
|
|
355
|
+
tag: '<DomWeekPlanner>',
|
|
356
|
+
description: 'A fixed-width week scheduler with days as horizontally scrolling swim lanes and time chunks as rows.',
|
|
357
|
+
icon: 'M7 4v3M17 4v3M5 8h14M6 6h12v13H6V6Zm4 5h6M9 16h4',
|
|
358
|
+
events: [
|
|
359
|
+
{ name: 'day-click', payload: '({ cell, targetTime, targetMinutes, event })', typeRef: 'WeekPlannerDayClickPayload', description: 'Fired when a clickable day header, all-day cell, or timed row is activated.' },
|
|
360
|
+
{ name: 'time-range-select', payload: '({ cell, value, startTime, endTime, startMinutes, endMinutes, event })', typeRef: 'WeekPlannerTimeRangePayload', description: 'Fired after a mouse or pen drag selects a snapped time range. The planner does not create or persist records.' },
|
|
361
|
+
{ name: 'day-drag-start', payload: '({ cell, sourceValue, sourceTime, data, event })', typeRef: 'WeekPlannerDayDragPayload', description: 'Fired when an item using dragAttrs starts dragging.' },
|
|
362
|
+
{ name: 'day-drag-enter', payload: '({ sourceCell, targetCell, targetValue, targetTime, data, event })', typeRef: 'WeekPlannerDayDragPayload', description: 'Fired when a draggable item previews a day and time.' },
|
|
363
|
+
{ name: 'day-drag-leave', payload: '({ sourceCell, targetCell, targetValue, targetTime, data, event })', typeRef: 'WeekPlannerDayDragPayload', description: 'Fired when a draggable item leaves a day lane.' },
|
|
364
|
+
{ name: 'day-drop', payload: '({ sourceCell, targetCell, sourceValue, targetValue, sourceTime, targetTime, data, event })', typeRef: 'WeekPlannerDayDragPayload', description: 'Fired when an item is dropped. The planner does not mutate records.' },
|
|
365
|
+
{ name: 'day-drag-end', payload: '({ sourceCell, targetCell, sourceValue, targetValue, sourceTime, targetTime, data, event })', typeRef: 'WeekPlannerDayDragPayload', description: 'Fired when a drag started with dragAttrs ends.' },
|
|
366
|
+
{ name: 'viewport-scroll', payload: '({ scrollLeft, scrollWidth, clientWidth, remaining, atStart, atEnd })', typeRef: 'WeekPlannerViewportPayload', description: 'Fired while the internal two-axis viewport scrolls horizontally.' },
|
|
367
|
+
{ name: 'viewport-end', payload: '({ scrollLeft, scrollWidth, clientWidth, remaining, atStart, atEnd })', typeRef: 'WeekPlannerViewportPayload', description: 'Fired when horizontal scrolling reaches the end threshold. Apps can append another date range.' },
|
|
368
|
+
{ name: 'render-window-change', payload: '({ startIndex, endIndex, renderedWeeks, totalWeeks, start, end })', typeRef: 'WeekPlannerRenderWindowPayload', description: 'Fired when the mounted virtual week window changes so apps can fetch its record range.' },
|
|
369
|
+
],
|
|
370
|
+
slots: [
|
|
371
|
+
{ name: '(default)', payload: '{ cell, month, week, timeSlots, drag, dragAttrs, rangeStyle }', typeRef: 'WeekPlannerDaySlotPayload', description: 'Timed content layer for each day. Use rangeStyle(record) with records containing startTime and endTime.' },
|
|
372
|
+
{ name: 'day-header', payload: '{ cell, month, week }', description: 'Custom day-column header content.' },
|
|
373
|
+
{ name: 'all-day', payload: '{ cell, month, week, drag, dragAttrs }', description: 'All-day content rendered above the timed rows.' },
|
|
374
|
+
{ name: 'time-label', payload: '{ slot }', description: 'Custom sticky time-rail label.' },
|
|
375
|
+
{ name: 'corner', payload: '{ range }', description: 'Content above the sticky time rail.' },
|
|
376
|
+
],
|
|
377
|
+
keyboard: [
|
|
378
|
+
{ keys: 'Tab', action: 'Move through day headers, all-day cells, and time rows when clickable is enabled.' },
|
|
379
|
+
{ keys: 'Enter / Space', action: 'Activate the focused day or time row.' },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const props = defineProps(weekPlannerProps);
|
|
385
|
+
const emit = defineEmits([
|
|
386
|
+
'day-click',
|
|
387
|
+
'time-range-select',
|
|
388
|
+
'day-drag-start',
|
|
389
|
+
'day-drag-enter',
|
|
390
|
+
'day-drag-leave',
|
|
391
|
+
'day-drop',
|
|
392
|
+
'day-drag-end',
|
|
393
|
+
'viewport-scroll',
|
|
394
|
+
'viewport-end',
|
|
395
|
+
'render-window-change',
|
|
396
|
+
]);
|
|
397
|
+
const rootId = useId();
|
|
398
|
+
const calendarDragType = 'application/x-dom-month-calendar-drag';
|
|
399
|
+
const mounted = ref(false);
|
|
400
|
+
const now = ref(new Date());
|
|
401
|
+
const dragState = ref(createEmptyDragState());
|
|
402
|
+
const timeSelection = ref(createEmptyTimeSelection());
|
|
403
|
+
const scrollViewport = ref(null);
|
|
404
|
+
const viewportState = ref({
|
|
405
|
+
scrollLeft: 0,
|
|
406
|
+
clientWidth: 0,
|
|
407
|
+
});
|
|
408
|
+
let currentTimeTimer = null;
|
|
409
|
+
let viewportResizeObserver = null;
|
|
410
|
+
let timeSelectionLane = null;
|
|
411
|
+
const resolvedWeekStartsOn = computed(resolveWeekStartsOn);
|
|
412
|
+
const anchorDate = computed(resolveAnchorDate);
|
|
413
|
+
const normalizedTimeRange = computed(resolveTimeRange);
|
|
414
|
+
const allWeeks = computed(buildVisibleWeeks);
|
|
415
|
+
const weekRenderWindow = computed(buildWeekRenderWindow);
|
|
416
|
+
const visibleWeeks = computed(renderVisibleWeeks);
|
|
417
|
+
const visibleDays = computed(flattenVisibleDays);
|
|
418
|
+
const loadedRange = computed(buildLoadedRange);
|
|
419
|
+
const visibleRange = computed(buildVisibleRange);
|
|
420
|
+
const timeSlots = computed(buildTimeSlots);
|
|
421
|
+
const plannerHeight = computed(calculatePlannerHeight);
|
|
422
|
+
const plannerGridStyle = computed(buildPlannerGridStyle);
|
|
423
|
+
const viewportStyle = computed(buildViewportStyle);
|
|
424
|
+
const monthRecords = computed(buildMonthRecords);
|
|
425
|
+
const rangeLabel = computed(formatRangeLabel);
|
|
426
|
+
const todayValue = computed(() => formatDate(now.value));
|
|
427
|
+
const currentTimeVisible = computed(isCurrentTimeVisible);
|
|
428
|
+
const currentTimeStyle = computed(buildCurrentTimeStyle);
|
|
429
|
+
const timeSelectionStyle = computed(buildTimeSelectionStyle);
|
|
430
|
+
const weekByIndex = computed(indexVisibleWeeks);
|
|
431
|
+
|
|
432
|
+
const weekdayLabelFormatter = computed(createWeekdayLabelFormatter);
|
|
433
|
+
const dateLabelFormatter = computed(createDateLabelFormatter);
|
|
434
|
+
const monthLabelFormatter = computed(createMonthLabelFormatter);
|
|
435
|
+
const rangeLabelFormatter = computed(createRangeLabelFormatter);
|
|
436
|
+
const timeLabelFormatter = computed(createTimeLabelFormatter);
|
|
437
|
+
|
|
438
|
+
watch(weekRenderWindow, emitRenderWindowChange, { immediate: true });
|
|
439
|
+
watch(
|
|
440
|
+
() => [props.startDate, props.initialMonth, props.initialYear, props.weeks, props.virtualWeeks, props.initialScrollDate],
|
|
441
|
+
scheduleInitialScroll,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
onMounted(mountPlanner);
|
|
445
|
+
onBeforeUnmount(unmountPlanner);
|
|
446
|
+
|
|
447
|
+
defineExpose({
|
|
448
|
+
scrollToDate,
|
|
449
|
+
scrollToTime,
|
|
450
|
+
scrollToToday,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Resolves the configured first day of week.
|
|
455
|
+
*
|
|
456
|
+
* @returns {number} Weekday index from zero to six.
|
|
457
|
+
*/
|
|
458
|
+
function resolveWeekStartsOn() {
|
|
459
|
+
return clampNumber(props.weekStartsOn, 0, 6, 1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Resolves the date used to select the first visible week.
|
|
464
|
+
*
|
|
465
|
+
* @returns {Date} Local anchor date.
|
|
466
|
+
*/
|
|
467
|
+
function resolveAnchorDate() {
|
|
468
|
+
return parseDate(props.startDate) || initialDateFromProps() || new Date();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Creates the locale-aware weekday formatter.
|
|
473
|
+
*
|
|
474
|
+
* @returns {Intl.DateTimeFormat} Weekday formatter.
|
|
475
|
+
*/
|
|
476
|
+
function createWeekdayLabelFormatter() {
|
|
477
|
+
return new Intl.DateTimeFormat(props.locale, {
|
|
478
|
+
weekday: props.weekdayFormat,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Creates the locale-aware accessible date formatter.
|
|
484
|
+
*
|
|
485
|
+
* @returns {Intl.DateTimeFormat} Full date formatter.
|
|
486
|
+
*/
|
|
487
|
+
function createDateLabelFormatter() {
|
|
488
|
+
return new Intl.DateTimeFormat(props.locale, {
|
|
489
|
+
weekday: 'long',
|
|
490
|
+
day: 'numeric',
|
|
491
|
+
month: 'long',
|
|
492
|
+
year: 'numeric',
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Creates the locale-aware month formatter used by slot payloads.
|
|
498
|
+
*
|
|
499
|
+
* @returns {Intl.DateTimeFormat} Month formatter.
|
|
500
|
+
*/
|
|
501
|
+
function createMonthLabelFormatter() {
|
|
502
|
+
return new Intl.DateTimeFormat(props.locale, {
|
|
503
|
+
month: 'long',
|
|
504
|
+
year: 'numeric',
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Creates the compact formatter used for the visible-range label.
|
|
510
|
+
*
|
|
511
|
+
* @returns {Intl.DateTimeFormat} Range endpoint formatter.
|
|
512
|
+
*/
|
|
513
|
+
function createRangeLabelFormatter() {
|
|
514
|
+
return new Intl.DateTimeFormat(props.locale, {
|
|
515
|
+
day: 'numeric',
|
|
516
|
+
month: 'short',
|
|
517
|
+
year: 'numeric',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Creates the locale-aware time formatter used by the sticky rail.
|
|
523
|
+
*
|
|
524
|
+
* @returns {Intl.DateTimeFormat} Time formatter.
|
|
525
|
+
*/
|
|
526
|
+
function createTimeLabelFormatter() {
|
|
527
|
+
return new Intl.DateTimeFormat(props.locale, {
|
|
528
|
+
hour: 'numeric',
|
|
529
|
+
minute: '2-digit',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Normalizes the visible time range and row dimensions.
|
|
535
|
+
*
|
|
536
|
+
* @returns {{ startMinutes: number, endMinutes: number, stepMinutes: number, selectionStepMinutes: number, slotHeight: number }} Time-grid configuration.
|
|
537
|
+
*/
|
|
538
|
+
function resolveTimeRange() {
|
|
539
|
+
const startMinutes = parseTime(props.startTime, false) ?? 420;
|
|
540
|
+
const requestedEnd = parseTime(props.endTime, true) ?? 1200;
|
|
541
|
+
const endMinutes = requestedEnd > startMinutes ? requestedEnd : Math.min(1440, startMinutes + 60);
|
|
542
|
+
return {
|
|
543
|
+
startMinutes,
|
|
544
|
+
endMinutes,
|
|
545
|
+
stepMinutes: clampNumber(props.timeStep, 5, 720, 60),
|
|
546
|
+
selectionStepMinutes: clampNumber(props.selectionStep, 1, 720, 5),
|
|
547
|
+
slotHeight: clampNumber(props.slotHeight, 24, 160, 56),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Builds the requested consecutive weeks and their compatible day-cell payloads.
|
|
553
|
+
*
|
|
554
|
+
* @returns {Array<WeekPlannerWeek>} Visible week records.
|
|
555
|
+
*/
|
|
556
|
+
function buildVisibleWeeks() {
|
|
557
|
+
const firstDate = startOfWeek(anchorDate.value, resolvedWeekStartsOn.value);
|
|
558
|
+
const count = clampNumber(props.weeks, 1, 520, 3);
|
|
559
|
+
const weeks = [];
|
|
560
|
+
|
|
561
|
+
for (let weekIndex = 0; weekIndex < count; weekIndex += 1) {
|
|
562
|
+
const weekStart = addDays(firstDate, weekIndex * 7);
|
|
563
|
+
const days = Array.from({ length: 7 }, (_, column) => buildCell(addDays(weekStart, column), weekIndex, column));
|
|
564
|
+
const weekNumber = isoWeekNumberForWeek(days);
|
|
565
|
+
weeks.push({
|
|
566
|
+
index: weekIndex,
|
|
567
|
+
weekNumber,
|
|
568
|
+
label: `Week ${weekNumber}`,
|
|
569
|
+
value: formatDate(weekStart),
|
|
570
|
+
days,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return weeks;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Calculates the mounted horizontal week window.
|
|
579
|
+
*
|
|
580
|
+
* This uses the same fixed-item virtual window calculation as DomDataGrid,
|
|
581
|
+
* translated to horizontal week groups instead of vertical rows.
|
|
582
|
+
*
|
|
583
|
+
* @returns {ReturnType<typeof calculateVirtualWindow>} Virtual week window.
|
|
584
|
+
*/
|
|
585
|
+
function buildWeekRenderWindow() {
|
|
586
|
+
const totalWeeks = allWeeks.value.length;
|
|
587
|
+
const weekWidth = clampNumber(props.dayWidth, 144, 480, 192) * 7;
|
|
588
|
+
|
|
589
|
+
if (!props.virtualWeeks) {
|
|
590
|
+
return calculateVirtualWindow({
|
|
591
|
+
itemCount: totalWeeks,
|
|
592
|
+
itemSize: weekWidth,
|
|
593
|
+
scrollOffset: 0,
|
|
594
|
+
viewportSize: totalWeeks * weekWidth,
|
|
595
|
+
overscan: 0,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const railWidth = clampNumber(props.timeRailWidth, 56, 160, 72);
|
|
600
|
+
return calculateVirtualWindow({
|
|
601
|
+
itemCount: totalWeeks,
|
|
602
|
+
itemSize: weekWidth,
|
|
603
|
+
scrollOffset: viewportState.value.scrollLeft,
|
|
604
|
+
viewportSize: Math.max(0, viewportState.value.clientWidth - railWidth),
|
|
605
|
+
overscan: clampNumber(props.virtualWeekBuffer, 0, 12, 2),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Slices the available weeks to the mounted virtual window.
|
|
611
|
+
*
|
|
612
|
+
* @returns {Array<WeekPlannerWeek>} Mounted week records.
|
|
613
|
+
*/
|
|
614
|
+
function renderVisibleWeeks() {
|
|
615
|
+
const window = weekRenderWindow.value;
|
|
616
|
+
return allWeeks.value.slice(window.startIndex, window.endIndex);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Flattens visible weeks into ordered day columns.
|
|
621
|
+
*
|
|
622
|
+
* @returns {Array<WeekPlannerCell>} Ordered day cells.
|
|
623
|
+
*/
|
|
624
|
+
function flattenVisibleDays() {
|
|
625
|
+
return visibleWeeks.value.flatMap((week) => week.days);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Builds the complete loaded date range before horizontal virtualization.
|
|
630
|
+
*
|
|
631
|
+
* @returns {{ start: string, end: string, startDate: Date|null, endDate: Date|null, weeks: number }} Loaded range.
|
|
632
|
+
*/
|
|
633
|
+
function buildLoadedRange() {
|
|
634
|
+
const firstWeek = allWeeks.value[0] || null;
|
|
635
|
+
const lastWeek = allWeeks.value[allWeeks.value.length - 1] || null;
|
|
636
|
+
const firstCell = firstWeek?.days?.[0] || null;
|
|
637
|
+
const lastCell = lastWeek?.days?.[lastWeek.days.length - 1] || null;
|
|
638
|
+
return {
|
|
639
|
+
start: firstCell?.value || '',
|
|
640
|
+
end: lastCell?.value || '',
|
|
641
|
+
startDate: firstCell?.date || null,
|
|
642
|
+
endDate: lastCell?.date || null,
|
|
643
|
+
weeks: allWeeks.value.length,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Builds the inclusive visible date range for fetching and the corner slot.
|
|
649
|
+
*
|
|
650
|
+
* @returns {{ start: string, end: string, startDate: Date|null, endDate: Date|null, weeks: number }} Visible range.
|
|
651
|
+
*/
|
|
652
|
+
function buildVisibleRange() {
|
|
653
|
+
const firstCell = visibleDays.value[0] || null;
|
|
654
|
+
const lastCell = visibleDays.value[visibleDays.value.length - 1] || null;
|
|
655
|
+
return {
|
|
656
|
+
start: firstCell?.value || '',
|
|
657
|
+
end: lastCell?.value || '',
|
|
658
|
+
startDate: firstCell?.date || null,
|
|
659
|
+
endDate: lastCell?.date || null,
|
|
660
|
+
weeks: visibleWeeks.value.length,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Indexes mounted weeks by their stable absolute week index.
|
|
666
|
+
*
|
|
667
|
+
* @returns {Map<number, WeekPlannerWeek>} Week records keyed by absolute index.
|
|
668
|
+
*/
|
|
669
|
+
function indexVisibleWeeks() {
|
|
670
|
+
return new Map(visibleWeeks.value.map((week) => [week.index, week]));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Builds one day cell with the same date identity and boundary state used by DomMonthCalendar.
|
|
675
|
+
*
|
|
676
|
+
* @param {Date} date Date represented by the column.
|
|
677
|
+
* @param {number} weekIndex Zero-based visible week index.
|
|
678
|
+
* @param {number} column Zero-based weekday column.
|
|
679
|
+
* @returns {WeekPlannerCell} Planner day payload.
|
|
680
|
+
*/
|
|
681
|
+
function buildCell(date, weekIndex, column) {
|
|
682
|
+
const value = formatDate(date);
|
|
683
|
+
const anchorMonth = new Date(anchorDate.value.getFullYear(), anchorDate.value.getMonth(), 1);
|
|
684
|
+
const distance = monthDistance(date, anchorMonth);
|
|
685
|
+
const cell = {
|
|
686
|
+
date,
|
|
687
|
+
value,
|
|
688
|
+
day: date.getDate(),
|
|
689
|
+
weekday: weekdayLabelFormatter.value.format(date),
|
|
690
|
+
label: dateLabelFormatter.value.format(date),
|
|
691
|
+
currentMonth: distance === 0,
|
|
692
|
+
previousMonth: distance < 0,
|
|
693
|
+
nextMonth: distance > 0,
|
|
694
|
+
adjacentMonth: distance !== 0,
|
|
695
|
+
hidden: false,
|
|
696
|
+
monthValue: monthValue(date),
|
|
697
|
+
row: weekIndex,
|
|
698
|
+
column,
|
|
699
|
+
weekNumber: isoWeekNumber(date),
|
|
700
|
+
today: value === todayValue.value,
|
|
701
|
+
past: value < todayValue.value,
|
|
702
|
+
future: value > todayValue.value,
|
|
703
|
+
muted: beforeBoundary(value, props.mutedBefore) || afterBoundary(value, props.mutedAfter),
|
|
704
|
+
disabled: false,
|
|
705
|
+
};
|
|
706
|
+
cell.disabled = outsideRange(value) || Boolean(props.disabledDate?.(cell));
|
|
707
|
+
return cell;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Builds one month record for every month represented by the visible day columns.
|
|
712
|
+
*
|
|
713
|
+
* @returns {Map<string, WeekPlannerMonth>} Month records keyed by YYYY-MM.
|
|
714
|
+
*/
|
|
715
|
+
function buildMonthRecords() {
|
|
716
|
+
const months = new Map();
|
|
717
|
+
|
|
718
|
+
for (const cell of visibleDays.value) {
|
|
719
|
+
if (!months.has(cell.monthValue)) {
|
|
720
|
+
const date = new Date(cell.date.getFullYear(), cell.date.getMonth(), 1);
|
|
721
|
+
months.set(cell.monthValue, {
|
|
722
|
+
date,
|
|
723
|
+
value: cell.monthValue,
|
|
724
|
+
id: `${rootId}-${cell.monthValue}`,
|
|
725
|
+
month: date.getMonth() + 1,
|
|
726
|
+
year: date.getFullYear(),
|
|
727
|
+
label: monthLabelFormatter.value.format(date),
|
|
728
|
+
cells: [],
|
|
729
|
+
weeks: [],
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
months.get(cell.monthValue).cells.push(cell);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
for (const week of visibleWeeks.value) {
|
|
736
|
+
for (const month of months.values()) {
|
|
737
|
+
const days = week.days.filter((cell) => cell.monthValue === month.value);
|
|
738
|
+
if (days.length) {
|
|
739
|
+
month.weeks.push({
|
|
740
|
+
...week,
|
|
741
|
+
days,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return months;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Resolves the month payload for a day slot.
|
|
752
|
+
*
|
|
753
|
+
* @param {WeekPlannerCell} cell Day whose month record is needed.
|
|
754
|
+
* @returns {WeekPlannerMonth} Month record.
|
|
755
|
+
*/
|
|
756
|
+
function monthForCell(cell) {
|
|
757
|
+
return monthRecords.value.get(cell.monthValue);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Resolves the week payload for a day slot.
|
|
762
|
+
*
|
|
763
|
+
* @param {WeekPlannerCell} cell Day whose week record is needed.
|
|
764
|
+
* @returns {WeekPlannerWeek} Week record.
|
|
765
|
+
*/
|
|
766
|
+
function weekForCell(cell) {
|
|
767
|
+
return weekByIndex.value.get(cell.row);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Builds the horizontal time rows.
|
|
772
|
+
*
|
|
773
|
+
* @returns {Array<WeekPlannerTimeSlot>} Ordered time-slot rows.
|
|
774
|
+
*/
|
|
775
|
+
function buildTimeSlots() {
|
|
776
|
+
const range = normalizedTimeRange.value;
|
|
777
|
+
const slots = [];
|
|
778
|
+
|
|
779
|
+
for (let minutes = range.startMinutes; minutes < range.endMinutes; minutes += range.stepMinutes) {
|
|
780
|
+
const endMinutes = Math.min(range.endMinutes, minutes + range.stepMinutes);
|
|
781
|
+
slots.push({
|
|
782
|
+
index: slots.length,
|
|
783
|
+
minutes,
|
|
784
|
+
endMinutes,
|
|
785
|
+
time: formatTimeKey(minutes),
|
|
786
|
+
endTime: formatTimeKey(endMinutes),
|
|
787
|
+
label: formatTimeLabel(minutes),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return slots;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Calculates the pixel height of the timed grid.
|
|
796
|
+
*
|
|
797
|
+
* @returns {number} Timed grid height in pixels.
|
|
798
|
+
*/
|
|
799
|
+
function calculatePlannerHeight() {
|
|
800
|
+
const range = normalizedTimeRange.value;
|
|
801
|
+
return ((range.endMinutes - range.startMinutes) / range.stepMinutes) * range.slotHeight;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Builds the fixed-width grid columns used by the header, all-day row, and body.
|
|
806
|
+
*
|
|
807
|
+
* @returns {Record<string, string>} Inline grid style.
|
|
808
|
+
*/
|
|
809
|
+
function buildPlannerGridStyle() {
|
|
810
|
+
const railWidth = clampNumber(props.timeRailWidth, 56, 160, 72);
|
|
811
|
+
const dayWidth = clampNumber(props.dayWidth, 144, 480, 192);
|
|
812
|
+
const window = weekRenderWindow.value;
|
|
813
|
+
const columns = [`${railWidth}px`];
|
|
814
|
+
|
|
815
|
+
if (window.beforeSpacerSize) columns.push(`${window.beforeSpacerSize}px`);
|
|
816
|
+
if (visibleDays.value.length) columns.push(`repeat(${visibleDays.value.length}, ${dayWidth}px)`);
|
|
817
|
+
if (window.afterSpacerSize) columns.push(`${window.afterSpacerSize}px`);
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
gridTemplateColumns: columns.join(' '),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Builds the explicit internal scrollport dimensions.
|
|
826
|
+
*
|
|
827
|
+
* A fixed height is intentional here: documentation iframes and application
|
|
828
|
+
* shells can otherwise size themselves to the full time grid, leaving the
|
|
829
|
+
* planner without a vertical scroll boundary.
|
|
830
|
+
*
|
|
831
|
+
* @returns {Record<string, string>} Scroll viewport style.
|
|
832
|
+
*/
|
|
833
|
+
function buildViewportStyle() {
|
|
834
|
+
return {
|
|
835
|
+
height: `${clampNumber(props.viewportHeight, 320, 1200, 640)}px`,
|
|
836
|
+
maxHeight: 'calc(100dvh - 6rem)',
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Checks whether the live current time falls inside the configured time grid.
|
|
842
|
+
*
|
|
843
|
+
* @returns {boolean} Whether the current-time line should be positioned.
|
|
844
|
+
*/
|
|
845
|
+
function isCurrentTimeVisible() {
|
|
846
|
+
const minutes = minutesForDate(now.value);
|
|
847
|
+
const range = normalizedTimeRange.value;
|
|
848
|
+
return minutes >= range.startMinutes && minutes <= range.endMinutes;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Builds the vertical position of the live current-time line.
|
|
853
|
+
*
|
|
854
|
+
* @returns {Record<string, string>} Absolute positioning style.
|
|
855
|
+
*/
|
|
856
|
+
function buildCurrentTimeStyle() {
|
|
857
|
+
const range = normalizedTimeRange.value;
|
|
858
|
+
const pixelsPerMinute = range.slotHeight / range.stepMinutes;
|
|
859
|
+
const minutes = Math.min(range.endMinutes, Math.max(range.startMinutes, minutesForDate(now.value)));
|
|
860
|
+
return {
|
|
861
|
+
top: `${(minutes - range.startMinutes) * pixelsPerMinute}px`,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Formats the inclusive visible date range.
|
|
867
|
+
*
|
|
868
|
+
* @returns {string} Human-readable planner range.
|
|
869
|
+
*/
|
|
870
|
+
function formatRangeLabel() {
|
|
871
|
+
const range = visibleRange.value;
|
|
872
|
+
if (!range.startDate || !range.endDate) return 'Week planner';
|
|
873
|
+
return `${rangeLabelFormatter.value.format(range.startDate)} – ${rangeLabelFormatter.value.format(range.endDate)}`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Formats a time-rail label from minutes after midnight.
|
|
878
|
+
*
|
|
879
|
+
* @param {number} minutes Minutes after midnight.
|
|
880
|
+
* @returns {string} Locale-aware time label.
|
|
881
|
+
*/
|
|
882
|
+
function formatTimeLabel(minutes) {
|
|
883
|
+
const date = new Date(2026, 0, 1, 0, minutes);
|
|
884
|
+
return timeLabelFormatter.value.format(date);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Resolves CSS positioning for a record or explicit start and end time.
|
|
889
|
+
*
|
|
890
|
+
* @param {Record<string, unknown>|string} recordOrStartTime Record with startTime/endTime, or an HH:mm start time.
|
|
891
|
+
* @param {string} [explicitEndTime] HH:mm end time when the first argument is a string.
|
|
892
|
+
* @returns {Record<string, string>} Absolute-positioning style for a timed record.
|
|
893
|
+
*/
|
|
894
|
+
function rangeStyle(recordOrStartTime, explicitEndTime = '') {
|
|
895
|
+
const startTime = typeof recordOrStartTime === 'string' ? recordOrStartTime : String(recordOrStartTime?.startTime || '');
|
|
896
|
+
const endTime = typeof recordOrStartTime === 'string' ? explicitEndTime : String(recordOrStartTime?.endTime || '');
|
|
897
|
+
const startMinutes = parseTime(startTime, false);
|
|
898
|
+
const endMinutes = parseTime(endTime, true);
|
|
899
|
+
const range = normalizedTimeRange.value;
|
|
900
|
+
|
|
901
|
+
if (startMinutes === null || endMinutes === null || endMinutes <= startMinutes) {
|
|
902
|
+
return {
|
|
903
|
+
display: 'none',
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const visibleStart = Math.max(range.startMinutes, startMinutes);
|
|
908
|
+
const visibleEnd = Math.min(range.endMinutes, endMinutes);
|
|
909
|
+
if (visibleEnd <= visibleStart) {
|
|
910
|
+
return {
|
|
911
|
+
display: 'none',
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const pixelsPerMinute = range.slotHeight / range.stepMinutes;
|
|
916
|
+
return {
|
|
917
|
+
top: `${(visibleStart - range.startMinutes) * pixelsPerMinute}px`,
|
|
918
|
+
height: `${Math.max(20, (visibleEnd - visibleStart) * pixelsPerMinute)}px`,
|
|
919
|
+
pointerEvents: 'auto',
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Builds the vertical placement of the active drag-selection preview.
|
|
925
|
+
*
|
|
926
|
+
* @returns {Record<string, string>} Absolute positioning style.
|
|
927
|
+
*/
|
|
928
|
+
function buildTimeSelectionStyle() {
|
|
929
|
+
if (!timeSelection.value.active) return { display: 'none' };
|
|
930
|
+
const range = normalizedTimeRange.value;
|
|
931
|
+
const pixelsPerMinute = range.slotHeight / range.stepMinutes;
|
|
932
|
+
return {
|
|
933
|
+
top: `${(timeSelection.value.startMinutes - range.startMinutes) * pixelsPerMinute}px`,
|
|
934
|
+
height: `${Math.max(2, (timeSelection.value.endMinutes - timeSelection.value.startMinutes) * pixelsPerMinute)}px`,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Resolves the proportional height of one time row, including a partial final row.
|
|
940
|
+
*
|
|
941
|
+
* @param {WeekPlannerTimeSlot} slot Time row being rendered.
|
|
942
|
+
* @returns {Record<string, string>} Time-row height style.
|
|
943
|
+
*/
|
|
944
|
+
function timeSlotStyle(slot) {
|
|
945
|
+
const range = normalizedTimeRange.value;
|
|
946
|
+
const rowMinutes = slot.endMinutes - slot.minutes;
|
|
947
|
+
return {
|
|
948
|
+
height: `${(rowMinutes / range.stepMinutes) * range.slotHeight}px`,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Resolves consumer-supplied classes for a day lane.
|
|
954
|
+
*
|
|
955
|
+
* @param {WeekPlannerCell} cell Day lane being rendered.
|
|
956
|
+
* @returns {unknown} Vue-compatible class binding.
|
|
957
|
+
*/
|
|
958
|
+
function dayClassFor(cell) {
|
|
959
|
+
if (typeof props.dayClass === 'function') return props.dayClass(cell);
|
|
960
|
+
return props.dayClass;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Resolves consumer-supplied styles for a day lane.
|
|
965
|
+
*
|
|
966
|
+
* @param {WeekPlannerCell} cell Day lane being rendered.
|
|
967
|
+
* @returns {string|Record<string, string|number>} Vue-compatible style binding.
|
|
968
|
+
*/
|
|
969
|
+
function dayStyleFor(cell) {
|
|
970
|
+
if (typeof props.dayStyle === 'function') return props.dayStyle(cell);
|
|
971
|
+
return props.dayStyle;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Emits a date-only day-click from a day header or all-day cell.
|
|
976
|
+
*
|
|
977
|
+
* @param {WeekPlannerCell} cell Activated day.
|
|
978
|
+
* @param {MouseEvent} event Native click event.
|
|
979
|
+
* @returns {void}
|
|
980
|
+
*/
|
|
981
|
+
function onUntimedDayClick(cell, event) {
|
|
982
|
+
if (!props.clickable || cell.disabled) return;
|
|
983
|
+
emit('day-click', {
|
|
984
|
+
cell,
|
|
985
|
+
targetTime: '',
|
|
986
|
+
targetMinutes: null,
|
|
987
|
+
event,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Emits a timed day-click from a time row.
|
|
993
|
+
*
|
|
994
|
+
* @param {WeekPlannerCell} cell Activated day.
|
|
995
|
+
* @param {WeekPlannerTimeSlot} slot Activated time row.
|
|
996
|
+
* @param {MouseEvent} event Native click event.
|
|
997
|
+
* @returns {void}
|
|
998
|
+
*/
|
|
999
|
+
function onTimeSlotClick(cell, slot, event) {
|
|
1000
|
+
if (!props.clickable || cell.disabled) return;
|
|
1001
|
+
emit('day-click', {
|
|
1002
|
+
cell,
|
|
1003
|
+
targetTime: slot.time,
|
|
1004
|
+
targetMinutes: slot.minutes,
|
|
1005
|
+
event,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Creates the inert time-selection state.
|
|
1011
|
+
*
|
|
1012
|
+
* @returns {{ active: boolean, cell: null, anchorMinutes: null, startMinutes: null, endMinutes: null, pointerId: null }} Empty state.
|
|
1013
|
+
*/
|
|
1014
|
+
function createEmptyTimeSelection() {
|
|
1015
|
+
return {
|
|
1016
|
+
active: false,
|
|
1017
|
+
cell: null,
|
|
1018
|
+
anchorMinutes: null,
|
|
1019
|
+
startMinutes: null,
|
|
1020
|
+
endMinutes: null,
|
|
1021
|
+
pointerId: null,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Starts a snapped time-range selection in one day lane.
|
|
1027
|
+
*
|
|
1028
|
+
* Touch pointers remain available for native two-axis scrolling. Applications
|
|
1029
|
+
* can mark interactive event content with data-week-planner-ignore-selection.
|
|
1030
|
+
*
|
|
1031
|
+
* @param {WeekPlannerCell} cell Day lane where selection starts.
|
|
1032
|
+
* @param {PointerEvent} event Native pointer event.
|
|
1033
|
+
* @returns {void}
|
|
1034
|
+
*/
|
|
1035
|
+
function onTimeSelectionStart(cell, event) {
|
|
1036
|
+
if (!props.rangeSelection || cell.disabled || event.button !== 0 || event.pointerType === 'touch') return;
|
|
1037
|
+
if (!(event.currentTarget instanceof HTMLElement) || isIgnoredTimeSelectionTarget(event.target)) return;
|
|
1038
|
+
|
|
1039
|
+
event.preventDefault();
|
|
1040
|
+
cancelTimeSelection();
|
|
1041
|
+
timeSelectionLane = event.currentTarget;
|
|
1042
|
+
const anchorMinutes = minutesFromClientY(
|
|
1043
|
+
timeSelectionLane,
|
|
1044
|
+
event.clientY,
|
|
1045
|
+
normalizedTimeRange.value.selectionStepMinutes,
|
|
1046
|
+
);
|
|
1047
|
+
timeSelection.value = buildTimeSelection(cell, anchorMinutes, anchorMinutes, event.pointerId);
|
|
1048
|
+
|
|
1049
|
+
if (typeof window !== 'undefined') {
|
|
1050
|
+
window.addEventListener('pointermove', onTimeSelectionMove, { passive: false });
|
|
1051
|
+
window.addEventListener('pointerup', onTimeSelectionEnd);
|
|
1052
|
+
window.addEventListener('pointercancel', cancelTimeSelection);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Updates the active selection while the pointer moves vertically.
|
|
1058
|
+
*
|
|
1059
|
+
* @param {PointerEvent} event Native pointer event.
|
|
1060
|
+
* @returns {void}
|
|
1061
|
+
*/
|
|
1062
|
+
function onTimeSelectionMove(event) {
|
|
1063
|
+
const selection = timeSelection.value;
|
|
1064
|
+
if (!selection.active || event.pointerId !== selection.pointerId || !(timeSelectionLane instanceof HTMLElement)) return;
|
|
1065
|
+
|
|
1066
|
+
event.preventDefault();
|
|
1067
|
+
const currentMinutes = minutesFromClientY(
|
|
1068
|
+
timeSelectionLane,
|
|
1069
|
+
event.clientY,
|
|
1070
|
+
normalizedTimeRange.value.selectionStepMinutes,
|
|
1071
|
+
);
|
|
1072
|
+
timeSelection.value = buildTimeSelection(selection.cell, selection.anchorMinutes, currentMinutes, selection.pointerId);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Emits the completed range and clears the temporary selection preview.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {PointerEvent} event Native pointer event.
|
|
1079
|
+
* @returns {void}
|
|
1080
|
+
*/
|
|
1081
|
+
function onTimeSelectionEnd(event) {
|
|
1082
|
+
const selection = timeSelection.value;
|
|
1083
|
+
if (!selection.active || event.pointerId !== selection.pointerId) return;
|
|
1084
|
+
|
|
1085
|
+
if (timeSelectionLane instanceof HTMLElement) {
|
|
1086
|
+
const currentMinutes = minutesFromClientY(
|
|
1087
|
+
timeSelectionLane,
|
|
1088
|
+
event.clientY,
|
|
1089
|
+
normalizedTimeRange.value.selectionStepMinutes,
|
|
1090
|
+
);
|
|
1091
|
+
timeSelection.value = buildTimeSelection(selection.cell, selection.anchorMinutes, currentMinutes, selection.pointerId);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const completed = timeSelection.value;
|
|
1095
|
+
emit('time-range-select', {
|
|
1096
|
+
cell: completed.cell,
|
|
1097
|
+
value: completed.cell.value,
|
|
1098
|
+
startTime: formatTimeKey(completed.startMinutes),
|
|
1099
|
+
endTime: formatTimeKey(completed.endMinutes),
|
|
1100
|
+
startMinutes: completed.startMinutes,
|
|
1101
|
+
endMinutes: completed.endMinutes,
|
|
1102
|
+
event,
|
|
1103
|
+
});
|
|
1104
|
+
cancelTimeSelection();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Clears active range selection and removes its global pointer listeners.
|
|
1109
|
+
*
|
|
1110
|
+
* @returns {void}
|
|
1111
|
+
*/
|
|
1112
|
+
function cancelTimeSelection() {
|
|
1113
|
+
if (typeof window !== 'undefined') {
|
|
1114
|
+
window.removeEventListener('pointermove', onTimeSelectionMove);
|
|
1115
|
+
window.removeEventListener('pointerup', onTimeSelectionEnd);
|
|
1116
|
+
window.removeEventListener('pointercancel', cancelTimeSelection);
|
|
1117
|
+
}
|
|
1118
|
+
timeSelection.value = createEmptyTimeSelection();
|
|
1119
|
+
timeSelectionLane = null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Builds a normalized selection with a minimum duration of one time step.
|
|
1124
|
+
*
|
|
1125
|
+
* @param {WeekPlannerCell} cell Selected day.
|
|
1126
|
+
* @param {number} anchorMinutes Snapped starting row.
|
|
1127
|
+
* @param {number} currentMinutes Snapped current row.
|
|
1128
|
+
* @param {number} pointerId Active pointer identifier.
|
|
1129
|
+
* @returns {{ active: boolean, cell: WeekPlannerCell, anchorMinutes: number, startMinutes: number, endMinutes: number, pointerId: number }} Active selection.
|
|
1130
|
+
*/
|
|
1131
|
+
function buildTimeSelection(cell, anchorMinutes, currentMinutes, pointerId) {
|
|
1132
|
+
const range = normalizedTimeRange.value;
|
|
1133
|
+
const selectionStep = range.selectionStepMinutes;
|
|
1134
|
+
const startMinutes = Math.min(anchorMinutes, currentMinutes);
|
|
1135
|
+
const endMinutes = Math.min(
|
|
1136
|
+
range.endMinutes,
|
|
1137
|
+
Math.max(anchorMinutes, currentMinutes) + selectionStep,
|
|
1138
|
+
);
|
|
1139
|
+
return {
|
|
1140
|
+
active: true,
|
|
1141
|
+
cell,
|
|
1142
|
+
anchorMinutes,
|
|
1143
|
+
startMinutes,
|
|
1144
|
+
endMinutes,
|
|
1145
|
+
pointerId,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Checks whether interactive app content should keep ownership of the pointer.
|
|
1151
|
+
*
|
|
1152
|
+
* @param {EventTarget|null} target Native event target.
|
|
1153
|
+
* @returns {boolean} Whether planner range selection should be skipped.
|
|
1154
|
+
*/
|
|
1155
|
+
function isIgnoredTimeSelectionTarget(target) {
|
|
1156
|
+
if (!(target instanceof Element)) return false;
|
|
1157
|
+
return Boolean(target.closest('[draggable="true"], [data-week-planner-ignore-selection]'));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Creates the inert planner drag state.
|
|
1162
|
+
*
|
|
1163
|
+
* @returns {{ active: boolean, sourceCell: null, overCell: null, sourceMinutes: null, overMinutes: null, data: null }} Empty state.
|
|
1164
|
+
*/
|
|
1165
|
+
function createEmptyDragState() {
|
|
1166
|
+
return {
|
|
1167
|
+
active: false,
|
|
1168
|
+
sourceCell: null,
|
|
1169
|
+
overCell: null,
|
|
1170
|
+
sourceMinutes: null,
|
|
1171
|
+
overMinutes: null,
|
|
1172
|
+
data: null,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Builds a slot helper that makes app-rendered records draggable.
|
|
1178
|
+
*
|
|
1179
|
+
* @param {WeekPlannerCell} cell Day that owns the record.
|
|
1180
|
+
* @returns {WeekPlannerDragAttrs} Drag-attribute helper.
|
|
1181
|
+
*/
|
|
1182
|
+
function dragAttrsFor(cell) {
|
|
1183
|
+
return (data = null, options = {}) => {
|
|
1184
|
+
if (!canUseDay(cell)) {
|
|
1185
|
+
return {
|
|
1186
|
+
draggable: false,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return {
|
|
1191
|
+
draggable: true,
|
|
1192
|
+
'data-dom-month-calendar-drag': '',
|
|
1193
|
+
onDragstart: (event) => onItemDragStart(cell, data, options, event),
|
|
1194
|
+
onDragend: (event) => onItemDragEnd(cell, data, event),
|
|
1195
|
+
};
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Resolves the public drag state for one day slot.
|
|
1201
|
+
*
|
|
1202
|
+
* @param {WeekPlannerCell} cell Day receiving scoped drag state.
|
|
1203
|
+
* @returns {WeekPlannerDragState} Scoped drag state.
|
|
1204
|
+
*/
|
|
1205
|
+
function dragStateFor(cell) {
|
|
1206
|
+
const state = dragState.value;
|
|
1207
|
+
return {
|
|
1208
|
+
active: state.active,
|
|
1209
|
+
source: state.sourceCell?.value === cell.value,
|
|
1210
|
+
over: state.overCell?.value === cell.value,
|
|
1211
|
+
sourceCell: state.sourceCell,
|
|
1212
|
+
overCell: state.overCell,
|
|
1213
|
+
data: state.data,
|
|
1214
|
+
sourceTime: timeKeyOrEmpty(state.sourceMinutes),
|
|
1215
|
+
overTime: timeKeyOrEmpty(state.overMinutes),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Checks whether a day supports click and drag interactions.
|
|
1221
|
+
*
|
|
1222
|
+
* @param {WeekPlannerCell} cell Candidate day.
|
|
1223
|
+
* @returns {boolean} True when the day is enabled and drag-and-drop is active.
|
|
1224
|
+
*/
|
|
1225
|
+
function canUseDay(cell) {
|
|
1226
|
+
return props.dragAndDrop && !cell.disabled;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Handles the start of an app-owned record drag.
|
|
1231
|
+
*
|
|
1232
|
+
* @param {WeekPlannerCell} cell Source day.
|
|
1233
|
+
* @param {unknown} data App-owned record metadata.
|
|
1234
|
+
* @param {WeekPlannerDragAttrsOptions} options Native drag options.
|
|
1235
|
+
* @param {DragEvent} event Native drag event.
|
|
1236
|
+
* @returns {void}
|
|
1237
|
+
*/
|
|
1238
|
+
function onItemDragStart(cell, data, options, event) {
|
|
1239
|
+
if (!canUseDay(cell)) {
|
|
1240
|
+
event.preventDefault();
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const sourceTime = options.sourceTime || timeFromData(data);
|
|
1245
|
+
const sourceMinutes = parseTime(sourceTime, false);
|
|
1246
|
+
dragState.value = {
|
|
1247
|
+
active: true,
|
|
1248
|
+
sourceCell: cell,
|
|
1249
|
+
overCell: null,
|
|
1250
|
+
sourceMinutes,
|
|
1251
|
+
overMinutes: null,
|
|
1252
|
+
data,
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
if (event.dataTransfer) {
|
|
1256
|
+
event.dataTransfer.effectAllowed = options.effectAllowed || 'move';
|
|
1257
|
+
safeSetData(event, calendarDragType, serializeDragData({
|
|
1258
|
+
sourceValue: cell.value,
|
|
1259
|
+
sourceMonth: cell.monthValue,
|
|
1260
|
+
sourceTime,
|
|
1261
|
+
data,
|
|
1262
|
+
}));
|
|
1263
|
+
safeSetData(event, 'text/plain', plainTextForDrag(data, cell));
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
emit('day-drag-start', dragPayload(cell, sourceMinutes, event));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Handles the end of a record drag and clears planner state.
|
|
1271
|
+
*
|
|
1272
|
+
* @param {WeekPlannerCell} cell Source day.
|
|
1273
|
+
* @param {unknown} data App-owned record metadata.
|
|
1274
|
+
* @param {DragEvent} event Native drag event.
|
|
1275
|
+
* @returns {void}
|
|
1276
|
+
*/
|
|
1277
|
+
function onItemDragEnd(cell, data, event) {
|
|
1278
|
+
emit('day-drag-end', {
|
|
1279
|
+
...dragPayload(cell, dragState.value.overMinutes, event),
|
|
1280
|
+
data,
|
|
1281
|
+
});
|
|
1282
|
+
dragState.value = createEmptyDragState();
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Handles drag entry into an all-day cell.
|
|
1287
|
+
*
|
|
1288
|
+
* @param {WeekPlannerCell} cell Candidate day.
|
|
1289
|
+
* @param {DragEvent} event Native drag event.
|
|
1290
|
+
* @returns {void}
|
|
1291
|
+
*/
|
|
1292
|
+
function onAllDayDragEnter(cell, event) {
|
|
1293
|
+
updateDropTarget(cell, null, event, true);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Handles drag movement over an all-day cell.
|
|
1298
|
+
*
|
|
1299
|
+
* @param {WeekPlannerCell} cell Candidate day.
|
|
1300
|
+
* @param {DragEvent} event Native drag event.
|
|
1301
|
+
* @returns {void}
|
|
1302
|
+
*/
|
|
1303
|
+
function onAllDayDragOver(cell, event) {
|
|
1304
|
+
updateDropTarget(cell, null, event, false);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Handles drag entry into a timed day lane.
|
|
1309
|
+
*
|
|
1310
|
+
* @param {WeekPlannerCell} cell Candidate day.
|
|
1311
|
+
* @param {DragEvent} event Native drag event.
|
|
1312
|
+
* @returns {void}
|
|
1313
|
+
*/
|
|
1314
|
+
function onTimedDragEnter(cell, event) {
|
|
1315
|
+
updateDropTarget(cell, minutesFromPointer(event), event, true);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Handles drag movement over a timed day lane.
|
|
1320
|
+
*
|
|
1321
|
+
* @param {WeekPlannerCell} cell Candidate day.
|
|
1322
|
+
* @param {DragEvent} event Native drag event.
|
|
1323
|
+
* @returns {void}
|
|
1324
|
+
*/
|
|
1325
|
+
function onTimedDragOver(cell, event) {
|
|
1326
|
+
updateDropTarget(cell, minutesFromPointer(event), event, false);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Updates the current drop-preview day and time.
|
|
1331
|
+
*
|
|
1332
|
+
* @param {WeekPlannerCell} cell Candidate target day.
|
|
1333
|
+
* @param {number|null} minutes Snapped target minutes, or null for all-day.
|
|
1334
|
+
* @param {DragEvent} event Native drag event.
|
|
1335
|
+
* @param {boolean} emitEntry Whether a changed target should emit day-drag-enter.
|
|
1336
|
+
* @returns {void}
|
|
1337
|
+
*/
|
|
1338
|
+
function updateDropTarget(cell, minutes, event, emitEntry) {
|
|
1339
|
+
if (!canUseDay(cell)) return;
|
|
1340
|
+
event.preventDefault();
|
|
1341
|
+
applyDropEffect(event);
|
|
1342
|
+
const changed = dragState.value.overCell?.value !== cell.value || dragState.value.overMinutes !== minutes;
|
|
1343
|
+
dragState.value = {
|
|
1344
|
+
...dragState.value,
|
|
1345
|
+
active: true,
|
|
1346
|
+
overCell: cell,
|
|
1347
|
+
overMinutes: minutes,
|
|
1348
|
+
};
|
|
1349
|
+
if (changed && emitEntry) emit('day-drag-enter', dragPayload(cell, minutes, event));
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Handles dragleave without clearing state for transitions between descendants.
|
|
1354
|
+
*
|
|
1355
|
+
* @param {WeekPlannerCell} cell Day being left.
|
|
1356
|
+
* @param {DragEvent} event Native drag event.
|
|
1357
|
+
* @returns {void}
|
|
1358
|
+
*/
|
|
1359
|
+
function onDayDragLeave(cell, event) {
|
|
1360
|
+
if (dragState.value.overCell?.value !== cell.value || event.currentTarget.contains(event.relatedTarget)) return;
|
|
1361
|
+
emit('day-drag-leave', dragPayload(cell, dragState.value.overMinutes, event));
|
|
1362
|
+
dragState.value = {
|
|
1363
|
+
...dragState.value,
|
|
1364
|
+
overCell: null,
|
|
1365
|
+
overMinutes: null,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Handles a drop into an all-day cell.
|
|
1371
|
+
*
|
|
1372
|
+
* @param {WeekPlannerCell} cell Target day.
|
|
1373
|
+
* @param {DragEvent} event Native drag event.
|
|
1374
|
+
* @returns {void}
|
|
1375
|
+
*/
|
|
1376
|
+
function onAllDayDrop(cell, event) {
|
|
1377
|
+
onDayDrop(cell, null, event);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Handles a drop into a timed day lane.
|
|
1382
|
+
*
|
|
1383
|
+
* @param {WeekPlannerCell} cell Target day.
|
|
1384
|
+
* @param {DragEvent} event Native drag event.
|
|
1385
|
+
* @returns {void}
|
|
1386
|
+
*/
|
|
1387
|
+
function onTimedDrop(cell, event) {
|
|
1388
|
+
onDayDrop(cell, minutesFromPointer(event), event);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Emits a completed drop without mutating application records.
|
|
1393
|
+
*
|
|
1394
|
+
* @param {WeekPlannerCell} cell Target day.
|
|
1395
|
+
* @param {number|null} minutes Snapped target minutes, or null for all-day.
|
|
1396
|
+
* @param {DragEvent} event Native drag event.
|
|
1397
|
+
* @returns {void}
|
|
1398
|
+
*/
|
|
1399
|
+
function onDayDrop(cell, minutes, event) {
|
|
1400
|
+
if (!canUseDay(cell)) return;
|
|
1401
|
+
event.preventDefault();
|
|
1402
|
+
applyDropEffect(event);
|
|
1403
|
+
emit('day-drop', dragPayload(cell, minutes, event));
|
|
1404
|
+
dragState.value = {
|
|
1405
|
+
...dragState.value,
|
|
1406
|
+
overCell: null,
|
|
1407
|
+
overMinutes: null,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Calculates a snapped target time from the pointer position inside a day lane.
|
|
1413
|
+
*
|
|
1414
|
+
* @param {DragEvent} event Native drag event.
|
|
1415
|
+
* @returns {number} Snapped minutes after midnight.
|
|
1416
|
+
*/
|
|
1417
|
+
function minutesFromPointer(event) {
|
|
1418
|
+
const target = event.currentTarget;
|
|
1419
|
+
if (!(target instanceof HTMLElement)) return normalizedTimeRange.value.startMinutes;
|
|
1420
|
+
return minutesFromClientY(target, event.clientY);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Calculates a snapped time from a client Y coordinate inside a day lane.
|
|
1425
|
+
*
|
|
1426
|
+
* @param {HTMLElement} target Timed day lane.
|
|
1427
|
+
* @param {number} clientY Pointer client Y coordinate.
|
|
1428
|
+
* @param {number} [stepMinutes] Minutes represented by each snap interval.
|
|
1429
|
+
* @returns {number} Snapped minutes after midnight.
|
|
1430
|
+
*/
|
|
1431
|
+
function minutesFromClientY(target, clientY, stepMinutes = normalizedTimeRange.value.stepMinutes) {
|
|
1432
|
+
const bounds = target.getBoundingClientRect();
|
|
1433
|
+
const relativeY = Math.max(0, Math.min(bounds.height, clientY - bounds.top));
|
|
1434
|
+
const range = normalizedTimeRange.value;
|
|
1435
|
+
const rawMinutes = range.startMinutes + ((relativeY / Math.max(1, bounds.height)) * (range.endMinutes - range.startMinutes));
|
|
1436
|
+
const normalizedStep = Math.max(1, stepMinutes);
|
|
1437
|
+
const snapped = range.startMinutes + (Math.floor((rawMinutes - range.startMinutes) / normalizedStep) * normalizedStep);
|
|
1438
|
+
return Math.max(range.startMinutes, Math.min(range.endMinutes - normalizedStep, snapped));
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Builds the public payload shared by all planner drag events.
|
|
1443
|
+
*
|
|
1444
|
+
* @param {WeekPlannerCell} cell Target or source day.
|
|
1445
|
+
* @param {number|null} targetMinutes Target minutes, or null for all-day.
|
|
1446
|
+
* @param {DragEvent} event Native drag event.
|
|
1447
|
+
* @returns {WeekPlannerDayDragPayload} Public drag payload.
|
|
1448
|
+
*/
|
|
1449
|
+
function dragPayload(cell, targetMinutes, event) {
|
|
1450
|
+
const transferPayload = dragTransferPayload(event);
|
|
1451
|
+
const sourceValue = dragState.value.sourceCell?.value || transferPayload.sourceValue || transferPayload.sourceDate || '';
|
|
1452
|
+
const sourceTime = timeKeyOrEmpty(dragState.value.sourceMinutes) || transferPayload.sourceTime || timeFromData(transferPayload.data);
|
|
1453
|
+
const sourceMinutes = parseTime(sourceTime, false);
|
|
1454
|
+
const data = dragState.value.data ?? transferPayload.data ?? null;
|
|
1455
|
+
return {
|
|
1456
|
+
cell,
|
|
1457
|
+
targetCell: cell,
|
|
1458
|
+
sourceCell: dragState.value.sourceCell || cellForValue(sourceValue),
|
|
1459
|
+
sourceValue,
|
|
1460
|
+
targetValue: cell.value,
|
|
1461
|
+
sourceTime,
|
|
1462
|
+
targetTime: timeKeyOrEmpty(targetMinutes),
|
|
1463
|
+
sourceMinutes,
|
|
1464
|
+
targetMinutes,
|
|
1465
|
+
data,
|
|
1466
|
+
event,
|
|
1467
|
+
dataTransfer: event.dataTransfer,
|
|
1468
|
+
types: dataTransferTypes(event),
|
|
1469
|
+
getData: (type) => safeGetData(event, type),
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Applies the configured native drop effect.
|
|
1475
|
+
*
|
|
1476
|
+
* @param {DragEvent} event Native drag event.
|
|
1477
|
+
* @returns {void}
|
|
1478
|
+
*/
|
|
1479
|
+
function applyDropEffect(event) {
|
|
1480
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = props.dropEffect;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Reads the shared DOM Studio calendar transfer payload.
|
|
1485
|
+
*
|
|
1486
|
+
* @param {DragEvent} event Native drag event.
|
|
1487
|
+
* @returns {Record<string, unknown>} Parsed drag payload.
|
|
1488
|
+
*/
|
|
1489
|
+
function dragTransferPayload(event) {
|
|
1490
|
+
const payload = safeGetData(event, calendarDragType);
|
|
1491
|
+
if (!payload) return {};
|
|
1492
|
+
try {
|
|
1493
|
+
const parsed = JSON.parse(payload);
|
|
1494
|
+
return parsed && typeof parsed === 'object' ? parsed : { data: parsed };
|
|
1495
|
+
} catch {
|
|
1496
|
+
return {};
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Serializes app-owned drag data with a date-and-time fallback.
|
|
1502
|
+
*
|
|
1503
|
+
* @param {{ sourceValue: string, sourceMonth: string, sourceTime: string, data: unknown }} payload Drag payload.
|
|
1504
|
+
* @returns {string} JSON transfer value.
|
|
1505
|
+
*/
|
|
1506
|
+
function serializeDragData(payload) {
|
|
1507
|
+
try {
|
|
1508
|
+
return JSON.stringify(payload);
|
|
1509
|
+
} catch {
|
|
1510
|
+
return JSON.stringify({
|
|
1511
|
+
sourceValue: payload.sourceValue,
|
|
1512
|
+
sourceMonth: payload.sourceMonth,
|
|
1513
|
+
sourceTime: payload.sourceTime,
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Chooses a plain-text fallback for native transfer compatibility.
|
|
1520
|
+
*
|
|
1521
|
+
* @param {unknown} data App-owned drag data.
|
|
1522
|
+
* @param {WeekPlannerCell} cell Source day.
|
|
1523
|
+
* @returns {string} Plain-text transfer value.
|
|
1524
|
+
*/
|
|
1525
|
+
function plainTextForDrag(data, cell) {
|
|
1526
|
+
if (typeof data === 'string') return data;
|
|
1527
|
+
if (data && typeof data === 'object' && 'id' in data) return String(data.id);
|
|
1528
|
+
return cell.value;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Extracts a record start time when one is present.
|
|
1533
|
+
*
|
|
1534
|
+
* @param {unknown} data App-owned drag data.
|
|
1535
|
+
* @returns {string} HH:mm time or an empty string.
|
|
1536
|
+
*/
|
|
1537
|
+
function timeFromData(data) {
|
|
1538
|
+
return data && typeof data === 'object' && 'startTime' in data ? String(data.startTime || '') : '';
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Finds a visible cell by date key.
|
|
1543
|
+
*
|
|
1544
|
+
* @param {string} value YYYY-MM-DD key.
|
|
1545
|
+
* @returns {WeekPlannerCell|null} Visible day or null.
|
|
1546
|
+
*/
|
|
1547
|
+
function cellForValue(value) {
|
|
1548
|
+
return visibleDays.value.find((cell) => cell.value === value) || null;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Safely lists native DataTransfer types.
|
|
1553
|
+
*
|
|
1554
|
+
* @param {DragEvent} event Native drag event.
|
|
1555
|
+
* @returns {Array<string>} Transfer types.
|
|
1556
|
+
*/
|
|
1557
|
+
function dataTransferTypes(event) {
|
|
1558
|
+
return Array.from(event.dataTransfer?.types || []);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Safely reads a native DataTransfer value.
|
|
1563
|
+
*
|
|
1564
|
+
* @param {DragEvent} event Native drag event.
|
|
1565
|
+
* @param {string} type Transfer MIME type.
|
|
1566
|
+
* @returns {string} Stored value or an empty string.
|
|
1567
|
+
*/
|
|
1568
|
+
function safeGetData(event, type) {
|
|
1569
|
+
try {
|
|
1570
|
+
return event.dataTransfer?.getData(type) || '';
|
|
1571
|
+
} catch {
|
|
1572
|
+
return '';
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Safely writes a native DataTransfer value.
|
|
1578
|
+
*
|
|
1579
|
+
* @param {DragEvent} event Native drag event.
|
|
1580
|
+
* @param {string} type Transfer MIME type.
|
|
1581
|
+
* @param {string} value Transfer value.
|
|
1582
|
+
* @returns {void}
|
|
1583
|
+
*/
|
|
1584
|
+
function safeSetData(event, type, value) {
|
|
1585
|
+
try {
|
|
1586
|
+
event.dataTransfer?.setData(type, value);
|
|
1587
|
+
} catch {
|
|
1588
|
+
// Some browsers reject unavailable or custom drag data stores.
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Starts live-time updates and viewport measurement after client mount.
|
|
1594
|
+
*
|
|
1595
|
+
* @returns {void}
|
|
1596
|
+
*/
|
|
1597
|
+
function mountPlanner() {
|
|
1598
|
+
mounted.value = true;
|
|
1599
|
+
updateCurrentTime();
|
|
1600
|
+
refreshViewportMetrics();
|
|
1601
|
+
|
|
1602
|
+
if (typeof ResizeObserver !== 'undefined' && scrollViewport.value) {
|
|
1603
|
+
viewportResizeObserver = new ResizeObserver(refreshViewportMetrics);
|
|
1604
|
+
viewportResizeObserver.observe(scrollViewport.value);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (typeof window !== 'undefined') {
|
|
1608
|
+
currentTimeTimer = window.setInterval(updateCurrentTime, 30000);
|
|
1609
|
+
scheduleInitialScroll();
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Releases live-time and viewport observers.
|
|
1615
|
+
*
|
|
1616
|
+
* @returns {void}
|
|
1617
|
+
*/
|
|
1618
|
+
function unmountPlanner() {
|
|
1619
|
+
mounted.value = false;
|
|
1620
|
+
viewportResizeObserver?.disconnect();
|
|
1621
|
+
viewportResizeObserver = null;
|
|
1622
|
+
cancelTimeSelection();
|
|
1623
|
+
|
|
1624
|
+
if (currentTimeTimer !== null && typeof window !== 'undefined') {
|
|
1625
|
+
window.clearInterval(currentTimeTimer);
|
|
1626
|
+
currentTimeTimer = null;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Refreshes the reactive clock used by the current-time line and day state.
|
|
1632
|
+
*
|
|
1633
|
+
* @returns {void}
|
|
1634
|
+
*/
|
|
1635
|
+
function updateCurrentTime() {
|
|
1636
|
+
now.value = new Date();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Reads the internal scrollport width and horizontal offset.
|
|
1641
|
+
*
|
|
1642
|
+
* @returns {void}
|
|
1643
|
+
*/
|
|
1644
|
+
function refreshViewportMetrics() {
|
|
1645
|
+
const element = scrollViewport.value;
|
|
1646
|
+
if (!element) return;
|
|
1647
|
+
viewportState.value = {
|
|
1648
|
+
scrollLeft: element.scrollLeft,
|
|
1649
|
+
clientWidth: element.clientWidth,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Emits horizontal scroll state while preserving native vertical scrolling.
|
|
1655
|
+
*
|
|
1656
|
+
* @param {Event} event Native scroll event.
|
|
1657
|
+
* @returns {void}
|
|
1658
|
+
*/
|
|
1659
|
+
function onViewportScroll(event) {
|
|
1660
|
+
const element = event.currentTarget;
|
|
1661
|
+
if (!(element instanceof HTMLElement)) return;
|
|
1662
|
+
viewportState.value = {
|
|
1663
|
+
scrollLeft: element.scrollLeft,
|
|
1664
|
+
clientWidth: element.clientWidth,
|
|
1665
|
+
};
|
|
1666
|
+
const payload = viewportPayload(element);
|
|
1667
|
+
emit('viewport-scroll', payload);
|
|
1668
|
+
if (payload.atEnd) emit('viewport-end', payload);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Builds the public horizontal scroll payload.
|
|
1673
|
+
*
|
|
1674
|
+
* @param {HTMLElement} element Planner scroll viewport.
|
|
1675
|
+
* @returns {{
|
|
1676
|
+
* scrollLeft: number,
|
|
1677
|
+
* scrollWidth: number,
|
|
1678
|
+
* clientWidth: number,
|
|
1679
|
+
* remaining: number,
|
|
1680
|
+
* atStart: boolean,
|
|
1681
|
+
* atEnd: boolean,
|
|
1682
|
+
* }} Horizontal viewport state.
|
|
1683
|
+
*/
|
|
1684
|
+
function viewportPayload(element) {
|
|
1685
|
+
const remaining = Math.max(0, element.scrollWidth - element.scrollLeft - element.clientWidth);
|
|
1686
|
+
return {
|
|
1687
|
+
scrollLeft: element.scrollLeft,
|
|
1688
|
+
scrollWidth: element.scrollWidth,
|
|
1689
|
+
clientWidth: element.clientWidth,
|
|
1690
|
+
remaining,
|
|
1691
|
+
atStart: element.scrollLeft <= 48,
|
|
1692
|
+
atEnd: remaining <= 48,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Emits the mounted week window and its requestable date range.
|
|
1698
|
+
*
|
|
1699
|
+
* @param {ReturnType<typeof calculateVirtualWindow>} window Virtual week window.
|
|
1700
|
+
* @returns {void}
|
|
1701
|
+
*/
|
|
1702
|
+
function emitRenderWindowChange(window) {
|
|
1703
|
+
const firstWeek = allWeeks.value[window.startIndex] || null;
|
|
1704
|
+
const lastWeek = allWeeks.value[Math.max(window.startIndex, window.endIndex - 1)] || null;
|
|
1705
|
+
const firstCell = firstWeek?.days?.[0] || null;
|
|
1706
|
+
const lastCell = lastWeek?.days?.[lastWeek.days.length - 1] || null;
|
|
1707
|
+
emit('render-window-change', {
|
|
1708
|
+
startIndex: window.startIndex,
|
|
1709
|
+
endIndex: window.endIndex,
|
|
1710
|
+
firstVisibleIndex: window.firstVisibleIndex,
|
|
1711
|
+
lastVisibleIndex: window.lastVisibleIndex,
|
|
1712
|
+
renderedWeeks: window.renderedItems,
|
|
1713
|
+
totalWeeks: window.totalItems,
|
|
1714
|
+
start: firstCell?.value || '',
|
|
1715
|
+
end: lastCell?.value || '',
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Schedules the configured initial date to be revealed after rendering.
|
|
1721
|
+
*
|
|
1722
|
+
* @returns {void}
|
|
1723
|
+
*/
|
|
1724
|
+
function scheduleInitialScroll() {
|
|
1725
|
+
if (!props.initialScrollDate || typeof window === 'undefined') return;
|
|
1726
|
+
nextTick(() => {
|
|
1727
|
+
window.requestAnimationFrame(() => {
|
|
1728
|
+
scrollToDate(props.initialScrollDate, { align: 'center' });
|
|
1729
|
+
});
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Scrolls a loaded date into the horizontal viewport.
|
|
1735
|
+
*
|
|
1736
|
+
* @param {string|Date} value Date key or local Date.
|
|
1737
|
+
* @param {{ align?: 'start'|'center'|'end', behavior?: ScrollBehavior }} [options] Scroll alignment.
|
|
1738
|
+
* @returns {boolean} Whether the requested date belongs to the loaded range.
|
|
1739
|
+
*/
|
|
1740
|
+
function scrollToDate(value, options = {}) {
|
|
1741
|
+
const element = scrollViewport.value;
|
|
1742
|
+
const date = value instanceof Date ? value : parseDate(value);
|
|
1743
|
+
if (!element || !date || !loadedRange.value.startDate || !loadedRange.value.endDate) return false;
|
|
1744
|
+
if (date < loadedRange.value.startDate || date > loadedRange.value.endDate) return false;
|
|
1745
|
+
|
|
1746
|
+
const dayIndex = differenceInCalendarDays(date, loadedRange.value.startDate);
|
|
1747
|
+
const dayWidth = clampNumber(props.dayWidth, 144, 480, 192);
|
|
1748
|
+
const railWidth = clampNumber(props.timeRailWidth, 56, 160, 72);
|
|
1749
|
+
const availableWidth = Math.max(dayWidth, element.clientWidth - railWidth);
|
|
1750
|
+
const dayOffset = dayIndex * dayWidth;
|
|
1751
|
+
const align = options.align || 'center';
|
|
1752
|
+
let left = dayOffset;
|
|
1753
|
+
|
|
1754
|
+
if (align === 'center') left -= (availableWidth - dayWidth) / 2;
|
|
1755
|
+
if (align === 'end') left -= availableWidth - dayWidth;
|
|
1756
|
+
|
|
1757
|
+
element.scrollTo({
|
|
1758
|
+
left: Math.max(0, left),
|
|
1759
|
+
top: element.scrollTop,
|
|
1760
|
+
behavior: options.behavior || 'auto',
|
|
1761
|
+
});
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Scrolls a loaded time into the vertical viewport.
|
|
1767
|
+
*
|
|
1768
|
+
* @param {string|Date} value HH:mm key or Date whose local time should be used.
|
|
1769
|
+
* @param {{ align?: 'start'|'center'|'end', behavior?: ScrollBehavior }} [options] Scroll alignment.
|
|
1770
|
+
* @returns {boolean} Whether the requested time belongs to the configured grid.
|
|
1771
|
+
*/
|
|
1772
|
+
function scrollToTime(value, options = {}) {
|
|
1773
|
+
const element = scrollViewport.value;
|
|
1774
|
+
const timeGrid = element?.querySelector('[data-week-planner-time-grid]');
|
|
1775
|
+
const minutes = value instanceof Date ? minutesForDate(value) : parseTime(value, false);
|
|
1776
|
+
const range = normalizedTimeRange.value;
|
|
1777
|
+
if (!element || !(timeGrid instanceof HTMLElement) || minutes === null) return false;
|
|
1778
|
+
if (minutes < range.startMinutes || minutes > range.endMinutes) return false;
|
|
1779
|
+
|
|
1780
|
+
const pixelsPerMinute = range.slotHeight / range.stepMinutes;
|
|
1781
|
+
const timeOffset = timeGrid.offsetTop + ((minutes - range.startMinutes) * pixelsPerMinute);
|
|
1782
|
+
const align = options.align || 'center';
|
|
1783
|
+
let top = timeOffset;
|
|
1784
|
+
|
|
1785
|
+
if (align === 'center') top -= element.clientHeight / 2;
|
|
1786
|
+
if (align === 'end') top -= element.clientHeight;
|
|
1787
|
+
|
|
1788
|
+
element.scrollTo({
|
|
1789
|
+
left: element.scrollLeft,
|
|
1790
|
+
top: Math.max(0, top),
|
|
1791
|
+
behavior: options.behavior || 'auto',
|
|
1792
|
+
});
|
|
1793
|
+
return true;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* Returns the planner to today's date and current local time.
|
|
1798
|
+
*
|
|
1799
|
+
* @param {{ align?: 'start'|'center'|'end', behavior?: ScrollBehavior }} [options] Scroll alignment.
|
|
1800
|
+
* @returns {boolean} Whether today belongs to the loaded date range.
|
|
1801
|
+
*/
|
|
1802
|
+
function scrollToToday(options = {}) {
|
|
1803
|
+
const date = new Date();
|
|
1804
|
+
const found = scrollToDate(date, options);
|
|
1805
|
+
if (found && isCurrentTimeVisible()) scrollToTime(date, options);
|
|
1806
|
+
return found;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Returns minutes after midnight for a local Date.
|
|
1811
|
+
*
|
|
1812
|
+
* @param {Date} date Local date and time.
|
|
1813
|
+
* @returns {number} Fractional minutes after midnight.
|
|
1814
|
+
*/
|
|
1815
|
+
function minutesForDate(date) {
|
|
1816
|
+
return (date.getHours() * 60) + date.getMinutes() + (date.getSeconds() / 60);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* Calculates whole local calendar days between two dates without DST drift.
|
|
1821
|
+
*
|
|
1822
|
+
* @param {Date} date Later date.
|
|
1823
|
+
* @param {Date} origin Earlier date.
|
|
1824
|
+
* @returns {number} Signed whole-day distance.
|
|
1825
|
+
*/
|
|
1826
|
+
function differenceInCalendarDays(date, origin) {
|
|
1827
|
+
const dateUtc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1828
|
+
const originUtc = Date.UTC(origin.getFullYear(), origin.getMonth(), origin.getDate());
|
|
1829
|
+
return Math.round((dateUtc - originUtc) / 86400000);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* Builds the initial date from initialYear and initialMonth.
|
|
1834
|
+
*
|
|
1835
|
+
* @returns {Date|null} First day of the configured month or null.
|
|
1836
|
+
*/
|
|
1837
|
+
function initialDateFromProps() {
|
|
1838
|
+
const year = Number(props.initialYear);
|
|
1839
|
+
const month = Number(props.initialMonth);
|
|
1840
|
+
if (!Number.isFinite(year) || !Number.isFinite(month)) return null;
|
|
1841
|
+
return new Date(year, clampNumber(month, 1, 12, 1) - 1, 1);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/**
|
|
1845
|
+
* Parses a YYYY-MM-DD string into a validated local Date.
|
|
1846
|
+
*
|
|
1847
|
+
* @param {string} value Candidate date key.
|
|
1848
|
+
* @returns {Date|null} Parsed date or null.
|
|
1849
|
+
*/
|
|
1850
|
+
function parseDate(value) {
|
|
1851
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return null;
|
|
1852
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
1853
|
+
const date = new Date(year, month - 1, day);
|
|
1854
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
|
|
1855
|
+
return date;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Parses an HH:mm time string into minutes after midnight.
|
|
1860
|
+
*
|
|
1861
|
+
* @param {string} value Candidate time.
|
|
1862
|
+
* @param {boolean} allowEndOfDay Whether 24:00 is valid.
|
|
1863
|
+
* @returns {number|null} Minutes after midnight or null.
|
|
1864
|
+
*/
|
|
1865
|
+
function parseTime(value, allowEndOfDay) {
|
|
1866
|
+
const match = /^(\d{2}):(\d{2})$/.exec(value || '');
|
|
1867
|
+
if (!match) return null;
|
|
1868
|
+
const hours = Number(match[1]);
|
|
1869
|
+
const minutes = Number(match[2]);
|
|
1870
|
+
if (allowEndOfDay && hours === 24 && minutes === 0) return 1440;
|
|
1871
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
|
|
1872
|
+
return (hours * 60) + minutes;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Formats minutes after midnight as an HH:mm key.
|
|
1877
|
+
*
|
|
1878
|
+
* @param {number} minutes Minutes after midnight.
|
|
1879
|
+
* @returns {string} HH:mm time key.
|
|
1880
|
+
*/
|
|
1881
|
+
function formatTimeKey(minutes) {
|
|
1882
|
+
if (minutes === 1440) return '24:00';
|
|
1883
|
+
const normalized = Math.max(0, Math.min(1439, Math.round(minutes)));
|
|
1884
|
+
return `${pad(Math.floor(normalized / 60))}:${pad(normalized % 60)}`;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Formats nullable minutes as a time key.
|
|
1889
|
+
*
|
|
1890
|
+
* @param {number|null|undefined} minutes Minutes after midnight.
|
|
1891
|
+
* @returns {string} HH:mm time or an empty string.
|
|
1892
|
+
*/
|
|
1893
|
+
function timeKeyOrEmpty(minutes) {
|
|
1894
|
+
return Number.isFinite(minutes) ? formatTimeKey(minutes) : '';
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* Left-pads a numeric date or time part.
|
|
1899
|
+
*
|
|
1900
|
+
* @param {number} value Numeric part.
|
|
1901
|
+
* @returns {string} Two-digit value.
|
|
1902
|
+
*/
|
|
1903
|
+
function pad(value) {
|
|
1904
|
+
return String(value).padStart(2, '0');
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* Formats a local Date as YYYY-MM-DD.
|
|
1909
|
+
*
|
|
1910
|
+
* @param {Date} date Date to format.
|
|
1911
|
+
* @returns {string} Date key.
|
|
1912
|
+
*/
|
|
1913
|
+
function formatDate(date) {
|
|
1914
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Formats a local Date as YYYY-MM.
|
|
1919
|
+
*
|
|
1920
|
+
* @param {Date} date Date to format.
|
|
1921
|
+
* @returns {string} Month key.
|
|
1922
|
+
*/
|
|
1923
|
+
function monthValue(date) {
|
|
1924
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Returns the calendar-month distance between a date and anchor month.
|
|
1929
|
+
*
|
|
1930
|
+
* @param {Date} date Candidate date.
|
|
1931
|
+
* @param {Date} monthDate First day of the anchor month.
|
|
1932
|
+
* @returns {number} Signed month distance.
|
|
1933
|
+
*/
|
|
1934
|
+
function monthDistance(date, monthDate) {
|
|
1935
|
+
return ((date.getFullYear() - monthDate.getFullYear()) * 12) + date.getMonth() - monthDate.getMonth();
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* Adds whole local calendar days.
|
|
1940
|
+
*
|
|
1941
|
+
* @param {Date} date Starting date.
|
|
1942
|
+
* @param {number} days Number of days to add.
|
|
1943
|
+
* @returns {Date} Offset local date.
|
|
1944
|
+
*/
|
|
1945
|
+
function addDays(date, days) {
|
|
1946
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Finds the first date in the week containing a date.
|
|
1951
|
+
*
|
|
1952
|
+
* @param {Date} date Date inside the week.
|
|
1953
|
+
* @param {number} weekStartsOn First weekday index.
|
|
1954
|
+
* @returns {Date} Local week start.
|
|
1955
|
+
*/
|
|
1956
|
+
function startOfWeek(date, weekStartsOn) {
|
|
1957
|
+
const offset = (date.getDay() - weekStartsOn + 7) % 7;
|
|
1958
|
+
return addDays(date, -offset);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/**
|
|
1962
|
+
* Resolves the ISO week number represented by a week.
|
|
1963
|
+
*
|
|
1964
|
+
* @param {Array<WeekPlannerCell>} days Seven day cells.
|
|
1965
|
+
* @returns {number} ISO-8601 week number.
|
|
1966
|
+
*/
|
|
1967
|
+
function isoWeekNumberForWeek(days) {
|
|
1968
|
+
const thursday = days.find((cell) => cell.date.getDay() === 4)?.date || days[0]?.date || new Date();
|
|
1969
|
+
return isoWeekNumber(thursday);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* Calculates the ISO-8601 week number for a local date.
|
|
1974
|
+
*
|
|
1975
|
+
* @param {Date} date Candidate date.
|
|
1976
|
+
* @returns {number} ISO week number.
|
|
1977
|
+
*/
|
|
1978
|
+
function isoWeekNumber(date) {
|
|
1979
|
+
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1980
|
+
const day = target.getDay() || 7;
|
|
1981
|
+
target.setDate(target.getDate() + 4 - day);
|
|
1982
|
+
const yearStart = new Date(target.getFullYear(), 0, 1);
|
|
1983
|
+
return Math.ceil((((target - yearStart) / 86400000) + 1) / 7);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
/**
|
|
1987
|
+
* Checks whether a value has the YYYY-MM-DD boundary shape.
|
|
1988
|
+
*
|
|
1989
|
+
* @param {string} value Candidate boundary.
|
|
1990
|
+
* @returns {boolean} True when the boundary can be lexically compared.
|
|
1991
|
+
*/
|
|
1992
|
+
function isDateBoundary(value) {
|
|
1993
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value || '');
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* Checks whether a date key is before a configured boundary.
|
|
1998
|
+
*
|
|
1999
|
+
* @param {string} value Date key.
|
|
2000
|
+
* @param {string} boundary Boundary key.
|
|
2001
|
+
* @returns {boolean} True when the date is before the boundary.
|
|
2002
|
+
*/
|
|
2003
|
+
function beforeBoundary(value, boundary) {
|
|
2004
|
+
return isDateBoundary(boundary) && value < boundary;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Checks whether a date key is after a configured boundary.
|
|
2009
|
+
*
|
|
2010
|
+
* @param {string} value Date key.
|
|
2011
|
+
* @param {string} boundary Boundary key.
|
|
2012
|
+
* @returns {boolean} True when the date is after the boundary.
|
|
2013
|
+
*/
|
|
2014
|
+
function afterBoundary(value, boundary) {
|
|
2015
|
+
return isDateBoundary(boundary) && value > boundary;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Checks whether a date key is outside the interactive range.
|
|
2020
|
+
*
|
|
2021
|
+
* @param {string} value Date key.
|
|
2022
|
+
* @returns {boolean} True when the date is before min or after max.
|
|
2023
|
+
*/
|
|
2024
|
+
function outsideRange(value) {
|
|
2025
|
+
return beforeBoundary(value, props.min) || afterBoundary(value, props.max);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Clamps a numeric prop into a supported integer range.
|
|
2030
|
+
*
|
|
2031
|
+
* @param {number} value Candidate number.
|
|
2032
|
+
* @param {number} min Minimum value.
|
|
2033
|
+
* @param {number} max Maximum value.
|
|
2034
|
+
* @param {number} fallback Fallback when the value is not finite.
|
|
2035
|
+
* @returns {number} Rounded and clamped value.
|
|
2036
|
+
*/
|
|
2037
|
+
function clampNumber(value, min, max, fallback) {
|
|
2038
|
+
const number = Number(value);
|
|
2039
|
+
if (!Number.isFinite(number)) return fallback;
|
|
2040
|
+
return Math.min(max, Math.max(min, Math.round(number)));
|
|
2041
|
+
}
|
|
2042
|
+
</script>
|
|
2043
|
+
|
|
2044
|
+
<template>
|
|
2045
|
+
<div class="w-full min-w-0" data-dom-week-planner>
|
|
2046
|
+
<h2 :id="`${rootId}-label`" class="sr-only">{{ rangeLabel }}</h2>
|
|
2047
|
+
|
|
2048
|
+
<div
|
|
2049
|
+
ref="scrollViewport"
|
|
2050
|
+
tabindex="0"
|
|
2051
|
+
class="relative overflow-auto overscroll-contain rounded-2xl border border-border bg-canvas focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
2052
|
+
:style="viewportStyle"
|
|
2053
|
+
:aria-busy="loading ? 'true' : undefined"
|
|
2054
|
+
:aria-label="`Week planner, ${rangeLabel}`"
|
|
2055
|
+
@scroll.passive="onViewportScroll"
|
|
2056
|
+
>
|
|
2057
|
+
<div
|
|
2058
|
+
v-if="loading"
|
|
2059
|
+
class="pointer-events-none sticky top-3 left-full z-50 h-0 w-0"
|
|
2060
|
+
data-week-planner-loading
|
|
2061
|
+
>
|
|
2062
|
+
<div class="absolute top-0 right-3 flex w-max items-center gap-2 rounded-full border border-border bg-canvas/95 px-3 py-1.5 text-[10px] font-semibold text-muted-fg shadow-sm backdrop-blur">
|
|
2063
|
+
<span class="size-2 animate-pulse rounded-full bg-primary"></span>
|
|
2064
|
+
Loading dates
|
|
2065
|
+
</div>
|
|
2066
|
+
</div>
|
|
2067
|
+
|
|
2068
|
+
<div class="w-max min-w-full">
|
|
2069
|
+
<div
|
|
2070
|
+
role="row"
|
|
2071
|
+
class="sticky top-0 z-30 grid border-b border-border bg-canvas shadow-sm"
|
|
2072
|
+
:style="plannerGridStyle"
|
|
2073
|
+
>
|
|
2074
|
+
<div
|
|
2075
|
+
role="columnheader"
|
|
2076
|
+
class="sticky left-0 z-40 flex min-w-0 items-end border-r border-border bg-secondary px-3 py-3"
|
|
2077
|
+
>
|
|
2078
|
+
<slot name="corner" :range="visibleRange">
|
|
2079
|
+
<span class="truncate text-[10px] font-semibold uppercase tracking-wide text-muted-fg">{{ rangeLabel }}</span>
|
|
2080
|
+
</slot>
|
|
2081
|
+
</div>
|
|
2082
|
+
|
|
2083
|
+
<div
|
|
2084
|
+
v-if="weekRenderWindow.beforeSpacerSize"
|
|
2085
|
+
aria-hidden="true"
|
|
2086
|
+
class="h-18 border-r border-border bg-secondary/15"
|
|
2087
|
+
></div>
|
|
2088
|
+
|
|
2089
|
+
<component
|
|
2090
|
+
:is="clickable ? 'button' : 'div'"
|
|
2091
|
+
v-for="cell in visibleDays"
|
|
2092
|
+
:key="`header-${cell.value}`"
|
|
2093
|
+
:type="clickable ? 'button' : undefined"
|
|
2094
|
+
role="columnheader"
|
|
2095
|
+
:disabled="clickable && cell.disabled ? true : undefined"
|
|
2096
|
+
:aria-label="cell.label"
|
|
2097
|
+
:aria-current="cell.today ? 'date' : undefined"
|
|
2098
|
+
:data-date="cell.value"
|
|
2099
|
+
:data-week-start="cell.column === 0 ? weekForCell(cell).value : undefined"
|
|
2100
|
+
class="relative flex h-18 min-w-0 items-center justify-between gap-2 border-l border-border px-3 py-2 text-left outline-none first:border-l-0"
|
|
2101
|
+
:class="[
|
|
2102
|
+
cell.column === 0 && cell.row > 0 && 'border-l-2 border-l-muted',
|
|
2103
|
+
cell.today ? 'bg-primary/10' : 'bg-secondary/30',
|
|
2104
|
+
cell.muted && 'text-muted-fg/55',
|
|
2105
|
+
clickable && !cell.disabled && 'cursor-pointer hover:bg-secondary/60 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50',
|
|
2106
|
+
clickable && cell.disabled && 'cursor-not-allowed opacity-55',
|
|
2107
|
+
]"
|
|
2108
|
+
@click="onUntimedDayClick(cell, $event)"
|
|
2109
|
+
>
|
|
2110
|
+
<slot name="day-header" :cell="cell" :month="monthForCell(cell)" :week="weekForCell(cell)">
|
|
2111
|
+
<div class="min-w-0">
|
|
2112
|
+
<span class="block truncate text-[10px] font-semibold uppercase tracking-wide text-muted-fg">{{ cell.weekday }}</span>
|
|
2113
|
+
<span class="mt-0.5 block truncate text-xs font-semibold text-canvas-fg">{{ monthForCell(cell).label }}</span>
|
|
2114
|
+
</div>
|
|
2115
|
+
<span
|
|
2116
|
+
class="flex size-8 shrink-0 items-center justify-center rounded-full text-sm font-bold tabular-nums"
|
|
2117
|
+
:class="cell.today ? 'bg-primary text-primary-fg' : 'bg-canvas text-canvas-fg'"
|
|
2118
|
+
>
|
|
2119
|
+
{{ cell.day }}
|
|
2120
|
+
</span>
|
|
2121
|
+
<span v-if="showWeekNumbers && cell.column === 0" class="block text-[9px] font-bold text-muted-fg">
|
|
2122
|
+
W{{ cell.weekNumber }}
|
|
2123
|
+
</span>
|
|
2124
|
+
</slot>
|
|
2125
|
+
</component>
|
|
2126
|
+
|
|
2127
|
+
<div
|
|
2128
|
+
v-if="weekRenderWindow.afterSpacerSize"
|
|
2129
|
+
aria-hidden="true"
|
|
2130
|
+
class="h-18 border-l border-border bg-secondary/15"
|
|
2131
|
+
></div>
|
|
2132
|
+
</div>
|
|
2133
|
+
|
|
2134
|
+
<div
|
|
2135
|
+
v-if="showAllDayRow"
|
|
2136
|
+
role="row"
|
|
2137
|
+
class="sticky top-18 z-20 grid border-b border-border bg-canvas shadow-sm"
|
|
2138
|
+
:style="plannerGridStyle"
|
|
2139
|
+
>
|
|
2140
|
+
<div
|
|
2141
|
+
role="rowheader"
|
|
2142
|
+
class="sticky left-0 z-30 flex min-h-14 items-center justify-end border-r border-border bg-canvas px-3 text-[10px] font-semibold uppercase tracking-wide text-muted-fg"
|
|
2143
|
+
>
|
|
2144
|
+
All day
|
|
2145
|
+
</div>
|
|
2146
|
+
|
|
2147
|
+
<div
|
|
2148
|
+
v-if="weekRenderWindow.beforeSpacerSize"
|
|
2149
|
+
aria-hidden="true"
|
|
2150
|
+
class="min-h-14 border-r border-border bg-secondary/10"
|
|
2151
|
+
></div>
|
|
2152
|
+
|
|
2153
|
+
<component
|
|
2154
|
+
:is="clickable ? 'button' : 'div'"
|
|
2155
|
+
v-for="cell in visibleDays"
|
|
2156
|
+
:key="`all-day-${cell.value}`"
|
|
2157
|
+
:type="clickable ? 'button' : undefined"
|
|
2158
|
+
role="gridcell"
|
|
2159
|
+
:disabled="clickable && cell.disabled ? true : undefined"
|
|
2160
|
+
:aria-label="`All day, ${cell.label}`"
|
|
2161
|
+
class="min-h-14 min-w-0 border-l border-border p-1.5 text-left outline-none"
|
|
2162
|
+
:class="[
|
|
2163
|
+
cell.column === 0 && cell.row > 0 && 'border-l-2 border-l-muted',
|
|
2164
|
+
cell.muted && 'bg-secondary/20 text-muted-fg/55',
|
|
2165
|
+
clickable && !cell.disabled && 'cursor-pointer hover:bg-secondary/50 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50',
|
|
2166
|
+
clickable && cell.disabled && 'cursor-not-allowed opacity-55',
|
|
2167
|
+
dragAndDrop && dragState.overCell?.value === cell.value && dragState.overMinutes === null && 'bg-primary/10 ring-2 ring-inset ring-ring/60',
|
|
2168
|
+
]"
|
|
2169
|
+
@click="onUntimedDayClick(cell, $event)"
|
|
2170
|
+
@dragenter="onAllDayDragEnter(cell, $event)"
|
|
2171
|
+
@dragover="onAllDayDragOver(cell, $event)"
|
|
2172
|
+
@dragleave="onDayDragLeave(cell, $event)"
|
|
2173
|
+
@drop="onAllDayDrop(cell, $event)"
|
|
2174
|
+
>
|
|
2175
|
+
<slot
|
|
2176
|
+
name="all-day"
|
|
2177
|
+
:cell="cell"
|
|
2178
|
+
:month="monthForCell(cell)"
|
|
2179
|
+
:week="weekForCell(cell)"
|
|
2180
|
+
:drag="dragStateFor(cell)"
|
|
2181
|
+
:dragAttrs="dragAttrsFor(cell)"
|
|
2182
|
+
/>
|
|
2183
|
+
</component>
|
|
2184
|
+
|
|
2185
|
+
<div
|
|
2186
|
+
v-if="weekRenderWindow.afterSpacerSize"
|
|
2187
|
+
aria-hidden="true"
|
|
2188
|
+
class="min-h-14 border-l border-border bg-secondary/10"
|
|
2189
|
+
></div>
|
|
2190
|
+
</div>
|
|
2191
|
+
|
|
2192
|
+
<div
|
|
2193
|
+
role="grid"
|
|
2194
|
+
:aria-labelledby="`${rootId}-label`"
|
|
2195
|
+
class="grid"
|
|
2196
|
+
:style="plannerGridStyle"
|
|
2197
|
+
data-week-planner-time-grid
|
|
2198
|
+
>
|
|
2199
|
+
<div
|
|
2200
|
+
role="rowheader"
|
|
2201
|
+
class="sticky left-0 z-20 border-r border-border bg-canvas"
|
|
2202
|
+
:style="{ height: `${plannerHeight}px` }"
|
|
2203
|
+
>
|
|
2204
|
+
<div
|
|
2205
|
+
v-for="slot in timeSlots"
|
|
2206
|
+
:key="`time-${slot.time}`"
|
|
2207
|
+
class="relative border-b border-border"
|
|
2208
|
+
:style="timeSlotStyle(slot)"
|
|
2209
|
+
>
|
|
2210
|
+
<div class="absolute -top-2.5 right-2 bg-canvas px-1 text-[10px] font-medium tabular-nums text-muted-fg">
|
|
2211
|
+
<slot name="time-label" :slot="slot">{{ slot.label }}</slot>
|
|
2212
|
+
</div>
|
|
2213
|
+
</div>
|
|
2214
|
+
</div>
|
|
2215
|
+
|
|
2216
|
+
<div
|
|
2217
|
+
v-if="weekRenderWindow.beforeSpacerSize"
|
|
2218
|
+
aria-hidden="true"
|
|
2219
|
+
class="border-r border-border bg-secondary/5"
|
|
2220
|
+
:style="{ height: `${plannerHeight}px` }"
|
|
2221
|
+
></div>
|
|
2222
|
+
|
|
2223
|
+
<div
|
|
2224
|
+
v-for="cell in visibleDays"
|
|
2225
|
+
:key="`lane-${cell.value}`"
|
|
2226
|
+
class="relative min-w-0 border-l border-border"
|
|
2227
|
+
:class="[
|
|
2228
|
+
cell.column === 0 && cell.row > 0 && 'border-l-2 border-l-muted',
|
|
2229
|
+
cell.muted && 'bg-secondary/20 text-muted-fg/55',
|
|
2230
|
+
cell.disabled && 'opacity-60',
|
|
2231
|
+
rangeSelection && !cell.disabled && 'cursor-crosshair select-none',
|
|
2232
|
+
dragAndDrop && dragState.active && !cell.disabled && dragState.overCell?.value !== cell.value && 'bg-primary/5',
|
|
2233
|
+
]"
|
|
2234
|
+
:style="[dayStyleFor(cell), { height: `${plannerHeight}px` }]"
|
|
2235
|
+
:data-date="cell.value"
|
|
2236
|
+
:data-today="cell.today ? '' : undefined"
|
|
2237
|
+
:data-muted="cell.muted ? '' : undefined"
|
|
2238
|
+
:data-drag-source="dragState.sourceCell?.value === cell.value ? '' : undefined"
|
|
2239
|
+
:data-drop-target="dragState.overCell?.value === cell.value ? '' : undefined"
|
|
2240
|
+
@pointerdown="onTimeSelectionStart(cell, $event)"
|
|
2241
|
+
@dragenter="onTimedDragEnter(cell, $event)"
|
|
2242
|
+
@dragover="onTimedDragOver(cell, $event)"
|
|
2243
|
+
@dragleave="onDayDragLeave(cell, $event)"
|
|
2244
|
+
@drop="onTimedDrop(cell, $event)"
|
|
2245
|
+
>
|
|
2246
|
+
<component
|
|
2247
|
+
:is="clickable ? 'button' : 'div'"
|
|
2248
|
+
v-for="slot in timeSlots"
|
|
2249
|
+
:key="`${cell.value}-${slot.time}`"
|
|
2250
|
+
:type="clickable ? 'button' : undefined"
|
|
2251
|
+
role="gridcell"
|
|
2252
|
+
:disabled="clickable && cell.disabled ? true : undefined"
|
|
2253
|
+
:aria-label="`${cell.label}, ${slot.label}`"
|
|
2254
|
+
class="block w-full border-b border-border text-left outline-none"
|
|
2255
|
+
:class="[
|
|
2256
|
+
dayClassFor(cell),
|
|
2257
|
+
clickable && !cell.disabled && 'cursor-pointer hover:bg-secondary/40 focus-visible:relative focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50',
|
|
2258
|
+
clickable && cell.disabled && 'cursor-not-allowed',
|
|
2259
|
+
dragAndDrop && dragState.overCell?.value === cell.value && dragState.overMinutes === slot.minutes && 'bg-primary/10 ring-2 ring-inset ring-ring/60',
|
|
2260
|
+
]"
|
|
2261
|
+
:style="timeSlotStyle(slot)"
|
|
2262
|
+
@click="onTimeSlotClick(cell, slot, $event)"
|
|
2263
|
+
/>
|
|
2264
|
+
|
|
2265
|
+
<div
|
|
2266
|
+
v-if="mounted && showCurrentTime && cell.today && currentTimeVisible"
|
|
2267
|
+
class="pointer-events-none absolute inset-x-0 z-20 h-0.5 bg-destructive shadow-sm"
|
|
2268
|
+
:style="currentTimeStyle"
|
|
2269
|
+
:aria-label="`Current time ${formatTimeKey(minutesForDate(now))}`"
|
|
2270
|
+
data-current-time
|
|
2271
|
+
>
|
|
2272
|
+
<span class="absolute -top-1 -left-1 size-2.5 rounded-full bg-destructive ring-2 ring-canvas"></span>
|
|
2273
|
+
</div>
|
|
2274
|
+
|
|
2275
|
+
<div
|
|
2276
|
+
v-if="rangeSelection && timeSelection.active && timeSelection.cell?.value === cell.value"
|
|
2277
|
+
class="pointer-events-none absolute inset-x-1.5 z-30 overflow-hidden rounded-lg border-2 border-primary bg-primary/15 px-2 py-1 text-[10px] font-bold tabular-nums text-primary shadow-sm"
|
|
2278
|
+
:style="timeSelectionStyle"
|
|
2279
|
+
:aria-label="`Selected ${formatTimeKey(timeSelection.startMinutes)} to ${formatTimeKey(timeSelection.endMinutes)}`"
|
|
2280
|
+
data-time-range-selection
|
|
2281
|
+
>
|
|
2282
|
+
{{ formatTimeKey(timeSelection.startMinutes) }}–{{ formatTimeKey(timeSelection.endMinutes) }}
|
|
2283
|
+
</div>
|
|
2284
|
+
|
|
2285
|
+
<div class="pointer-events-none absolute inset-0 z-10">
|
|
2286
|
+
<slot
|
|
2287
|
+
:cell="cell"
|
|
2288
|
+
:month="monthForCell(cell)"
|
|
2289
|
+
:week="weekForCell(cell)"
|
|
2290
|
+
:timeSlots="timeSlots"
|
|
2291
|
+
:drag="dragStateFor(cell)"
|
|
2292
|
+
:dragAttrs="dragAttrsFor(cell)"
|
|
2293
|
+
:rangeStyle="rangeStyle"
|
|
2294
|
+
/>
|
|
2295
|
+
</div>
|
|
2296
|
+
</div>
|
|
2297
|
+
|
|
2298
|
+
<div
|
|
2299
|
+
v-if="weekRenderWindow.afterSpacerSize"
|
|
2300
|
+
aria-hidden="true"
|
|
2301
|
+
class="border-l border-border bg-secondary/5"
|
|
2302
|
+
:style="{ height: `${plannerHeight}px` }"
|
|
2303
|
+
></div>
|
|
2304
|
+
</div>
|
|
2305
|
+
</div>
|
|
2306
|
+
</div>
|
|
2307
|
+
</div>
|
|
2308
|
+
</template>
|