@ketvietlab/design-system 0.1.25 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -5
- package/dist/atlas/island-runtime.d.mts +2 -0
- package/dist/atlas/island-runtime.d.mts.map +1 -0
- package/dist/atlas/island-runtime.mjs +2 -0
- package/dist/atlas/island-runtime.mjs.map +1 -0
- package/dist/atlas/relation-select-hydration.d.ts +2 -0
- package/dist/atlas/relation-select-hydration.d.ts.map +1 -0
- package/dist/atlas/relation-select-hydration.js +40 -0
- package/dist/atlas/relation-select-hydration.js.map +1 -0
- package/dist/atlas-cli.d.ts.map +1 -1
- package/dist/atlas-cli.js +28 -10
- package/dist/atlas-cli.js.map +1 -1
- package/dist/catalogue/date-pickers.d.ts +6 -0
- package/dist/catalogue/date-pickers.d.ts.map +1 -0
- package/dist/catalogue/date-pickers.js +16 -0
- package/dist/catalogue/date-pickers.js.map +1 -0
- package/dist/catalogue/index.d.ts +7 -2
- package/dist/catalogue/index.d.ts.map +1 -1
- package/dist/catalogue/index.js +20 -4
- package/dist/catalogue/index.js.map +1 -1
- package/dist/catalogue/inventory-policy.json +49 -0
- package/dist/catalogue/inventory.generated.js +1 -1
- package/dist/catalogue/inventory.generated.js.map +1 -1
- package/dist/catalogue/inventory.generated.json +2815 -1108
- package/dist/catalogue/layering.d.ts +10 -0
- package/dist/catalogue/layering.d.ts.map +1 -0
- package/dist/catalogue/layering.js +67 -0
- package/dist/catalogue/layering.js.map +1 -0
- package/dist/catalogue/primitives.css +77 -0
- package/dist/catalogue/primitives.d.ts +34 -0
- package/dist/catalogue/primitives.d.ts.map +1 -0
- package/dist/catalogue/primitives.js +149 -0
- package/dist/catalogue/primitives.js.map +1 -0
- package/dist/catalogue/registry.d.ts.map +1 -1
- package/dist/catalogue/registry.js +18 -0
- package/dist/catalogue/registry.js.map +1 -1
- package/dist/catalogue/specimens.d.ts.map +1 -1
- package/dist/catalogue/specimens.js +142 -22
- package/dist/catalogue/specimens.js.map +1 -1
- package/dist/catalogue/styles.css +1 -0
- package/dist/contract/index.d.ts +2 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +11 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/layout-audit.d.ts +32 -0
- package/dist/contract/layout-audit.d.ts.map +1 -0
- package/dist/contract/layout-audit.js +204 -0
- package/dist/contract/layout-audit.js.map +1 -0
- package/dist/forms/date-time/date-math.d.ts +23 -0
- package/dist/forms/date-time/date-math.d.ts.map +1 -0
- package/dist/forms/date-time/date-math.js +83 -0
- package/dist/forms/date-time/date-math.js.map +1 -0
- package/dist/forms/date-time/index.d.ts +37 -6
- package/dist/forms/date-time/index.d.ts.map +1 -1
- package/dist/forms/date-time/index.js +140 -6
- package/dist/forms/date-time/index.js.map +1 -1
- package/dist/forms/date-time/runtime.d.ts +4 -0
- package/dist/forms/date-time/runtime.d.ts.map +1 -0
- package/dist/forms/date-time/runtime.js +637 -0
- package/dist/forms/date-time/runtime.js.map +1 -0
- package/dist/forms/date-time/styles.css +251 -12
- package/dist/foundations/tokens.css +48 -28
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/interactions/ket-table/demo.d.ts +4 -0
- package/dist/interactions/ket-table/demo.d.ts.map +1 -0
- package/dist/interactions/ket-table/demo.js +147 -0
- package/dist/interactions/ket-table/demo.js.map +1 -0
- package/dist/interactions/ket-table/index.d.ts +199 -0
- package/dist/interactions/ket-table/index.d.ts.map +1 -0
- package/dist/interactions/ket-table/index.js +376 -0
- package/dist/interactions/ket-table/index.js.map +1 -0
- package/dist/interactions/ket-table/styles.css +282 -0
- package/dist/interactions/lightbox/demo.d.ts +3 -0
- package/dist/interactions/lightbox/demo.d.ts.map +1 -0
- package/dist/interactions/lightbox/demo.js +21 -0
- package/dist/interactions/lightbox/demo.js.map +1 -0
- package/dist/interactions/lightbox/index.d.ts +58 -0
- package/dist/interactions/lightbox/index.d.ts.map +1 -0
- package/dist/interactions/lightbox/index.js +220 -0
- package/dist/interactions/lightbox/index.js.map +1 -0
- package/dist/interactions/lightbox/styles.css +153 -0
- package/dist/interactions/relation-select/demo.d.ts +3 -0
- package/dist/interactions/relation-select/demo.d.ts.map +1 -0
- package/dist/interactions/relation-select/demo.js +36 -0
- package/dist/interactions/relation-select/demo.js.map +1 -0
- package/dist/interactions/relation-select/index.d.ts +87 -0
- package/dist/interactions/relation-select/index.d.ts.map +1 -0
- package/dist/interactions/relation-select/index.js +307 -0
- package/dist/interactions/relation-select/index.js.map +1 -0
- package/dist/interactions/relation-select/styles.css +299 -0
- package/dist/interactions/reorder-list/index.d.ts +23 -0
- package/dist/interactions/reorder-list/index.d.ts.map +1 -0
- package/dist/interactions/reorder-list/index.js +15 -0
- package/dist/interactions/reorder-list/index.js.map +1 -0
- package/dist/interactions/reorder-list/runtime.d.ts +5 -0
- package/dist/interactions/reorder-list/runtime.d.ts.map +1 -0
- package/dist/interactions/reorder-list/runtime.js +85 -0
- package/dist/interactions/reorder-list/runtime.js.map +1 -0
- package/dist/interactions/reorder-list/styles.css +33 -0
- package/dist/interactions/search-filter/demo.d.ts +3 -0
- package/dist/interactions/search-filter/demo.d.ts.map +1 -0
- package/dist/interactions/search-filter/demo.js +74 -0
- package/dist/interactions/search-filter/demo.js.map +1 -0
- package/dist/interactions/search-filter/index.d.ts +153 -0
- package/dist/interactions/search-filter/index.d.ts.map +1 -0
- package/dist/interactions/search-filter/index.js +824 -0
- package/dist/interactions/search-filter/index.js.map +1 -0
- package/dist/interactions/search-filter/styles.css +568 -0
- package/dist/layout-audit-cli.d.ts +7 -0
- package/dist/layout-audit-cli.d.ts.map +1 -0
- package/dist/layout-audit-cli.js +63 -0
- package/dist/layout-audit-cli.js.map +1 -0
- package/dist/layouts/app-navigation/index.d.ts +11 -5
- package/dist/layouts/app-navigation/index.d.ts.map +1 -1
- package/dist/layouts/app-navigation/index.js +5 -3
- package/dist/layouts/app-navigation/index.js.map +1 -1
- package/dist/layouts/app-navigation/styles.css +47 -0
- package/dist/layouts/grouped/styles.css +5 -36
- package/dist/layouts/layering/styles.css +159 -0
- package/dist/layouts/layout/index.d.ts +6 -0
- package/dist/layouts/layout/index.d.ts.map +1 -1
- package/dist/layouts/layout/index.js +6 -1
- package/dist/layouts/layout/index.js.map +1 -1
- package/dist/layouts/layout/styles.css +15 -6
- package/dist/layouts/page-identity/index.d.ts +10 -7
- package/dist/layouts/page-identity/index.d.ts.map +1 -1
- package/dist/layouts/page-identity/index.js +9 -4
- package/dist/layouts/page-identity/index.js.map +1 -1
- package/dist/layouts/shell/index.d.ts +34 -1
- package/dist/layouts/shell/index.d.ts.map +1 -1
- package/dist/layouts/shell/index.js +26 -3
- package/dist/layouts/shell/index.js.map +1 -1
- package/dist/layouts/shell/styles.css +284 -2
- package/dist/patterns/board-page/index.d.ts +1 -1
- package/dist/patterns/board-page/index.d.ts.map +1 -1
- package/dist/patterns/board-page/index.js +0 -1
- package/dist/patterns/board-page/index.js.map +1 -1
- package/dist/patterns/board-page/styles.css +0 -12
- package/dist/patterns/dashboard-page/index.d.ts +1 -1
- package/dist/patterns/dashboard-page/index.d.ts.map +1 -1
- package/dist/patterns/dashboard-page/index.js +0 -1
- package/dist/patterns/dashboard-page/index.js.map +1 -1
- package/dist/patterns/dashboard-page/styles.css +0 -13
- package/dist/patterns/data-table/styles.css +1 -1
- package/dist/patterns/form-page/index.d.ts +1 -1
- package/dist/patterns/form-page/index.d.ts.map +1 -1
- package/dist/patterns/form-page/index.js +0 -1
- package/dist/patterns/form-page/index.js.map +1 -1
- package/dist/patterns/form-page/styles.css +0 -12
- package/dist/patterns/list-page/index.d.ts +10 -4
- package/dist/patterns/list-page/index.d.ts.map +1 -1
- package/dist/patterns/list-page/index.js +10 -8
- package/dist/patterns/list-page/index.js.map +1 -1
- package/dist/patterns/list-page/responsive.css +1 -13
- package/dist/patterns/list-page/styles.css +20 -20
- package/dist/patterns/modal-sheet/index.d.ts +2 -0
- package/dist/patterns/modal-sheet/index.d.ts.map +1 -1
- package/dist/patterns/modal-sheet/index.js +3 -1
- package/dist/patterns/modal-sheet/index.js.map +1 -1
- package/dist/patterns/modal-sheet/styles.css +1 -1
- package/dist/patterns/page-shell/styles.css +0 -12
- package/dist/patterns/record-form/styles.css +34 -0
- package/dist/patterns/record-page/index.d.ts +1 -2
- package/dist/patterns/record-page/index.d.ts.map +1 -1
- package/dist/patterns/record-page/index.js +2 -3
- package/dist/patterns/record-page/index.js.map +1 -1
- package/dist/patterns/record-page/styles.css +0 -12
- package/dist/patterns/workspace-page/index.d.ts +1 -2
- package/dist/patterns/workspace-page/index.d.ts.map +1 -1
- package/dist/patterns/workspace-page/index.js +0 -1
- package/dist/patterns/workspace-page/index.js.map +1 -1
- package/dist/primitives/actions/index.d.ts +4 -0
- package/dist/primitives/actions/index.d.ts.map +1 -1
- package/dist/primitives/actions/index.js +2 -2
- package/dist/primitives/actions/index.js.map +1 -1
- package/dist/primitives/actions/styles.css +3 -1
- package/dist/primitives/feedback/styles.css +11 -6
- package/dist/primitives/field/index.d.ts +5 -0
- package/dist/primitives/field/index.d.ts.map +1 -1
- package/dist/primitives/field/index.js +1 -1
- package/dist/primitives/field/index.js.map +1 -1
- package/dist/primitives/field/native-control.d.ts.map +1 -1
- package/dist/primitives/field/native-control.js +3 -3
- package/dist/primitives/field/native-control.js.map +1 -1
- package/dist/primitives/field/styles.css +124 -2
- package/dist/primitives/progress/styles.css +3 -0
- package/dist/primitives/status/index.d.ts +12 -1
- package/dist/primitives/status/index.d.ts.map +1 -1
- package/dist/primitives/status/index.js +16 -1
- package/dist/primitives/status/index.js.map +1 -1
- package/dist/primitives/status/styles.css +34 -2
- package/dist/record/display/index.d.ts +6 -0
- package/dist/record/display/index.d.ts.map +1 -1
- package/dist/record/display/index.js +6 -1
- package/dist/record/display/index.js.map +1 -1
- package/dist/record/display/styles.css +25 -0
- package/dist/record/formatted-values/index.d.ts +2 -1
- package/dist/record/formatted-values/index.d.ts.map +1 -1
- package/dist/record/formatted-values/index.js +2 -1
- package/dist/record/formatted-values/index.js.map +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +132 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import {
|
|
3
|
+
addDays,
|
|
4
|
+
formatRange,
|
|
5
|
+
monthEnd,
|
|
6
|
+
monthStart,
|
|
7
|
+
moveMonth,
|
|
8
|
+
orderedRange,
|
|
9
|
+
parseDate,
|
|
10
|
+
presetRange,
|
|
11
|
+
withinBounds,
|
|
12
|
+
} from './date-math.js'
|
|
13
|
+
|
|
14
|
+
/** An already validated calendar day. @param {string} value */
|
|
15
|
+
const civil = (value) => {
|
|
16
|
+
const date = parseDate(value)
|
|
17
|
+
if (!date) throw new RangeError(`Invalid civil date: ${value}`)
|
|
18
|
+
return date
|
|
19
|
+
}
|
|
20
|
+
/** @param {HTMLInputElement} input @param {string} value */
|
|
21
|
+
const allowed = (input, value) => {
|
|
22
|
+
if (!withinBounds(value, input.min, input.max)) return false
|
|
23
|
+
const step = Number(input.step || 1)
|
|
24
|
+
if (input.step === 'any' || !Number.isFinite(step) || step <= 0) return true
|
|
25
|
+
const base = parseDate(input.min) ?? parseDate(input.getAttribute('value') ?? '') ?? civil('1970-01-01')
|
|
26
|
+
return Math.abs(((civil(value).getTime() - base.getTime()) / 86400000 / step) % 1) < 0.000001
|
|
27
|
+
}
|
|
28
|
+
/** @type {WeakMap<HTMLElement, { users: number, prior: boolean }>} */
|
|
29
|
+
const inertLayers = new WeakMap()
|
|
30
|
+
let scrollLocks = 0
|
|
31
|
+
let priorOverflow = ''
|
|
32
|
+
let priorOverflowPriority = ''
|
|
33
|
+
/** Make the fullscreen calendar modal while preserving any outer dialog's locks.
|
|
34
|
+
* @param {HTMLElement} popup */
|
|
35
|
+
const lockBackground = (popup) => {
|
|
36
|
+
/** @type {HTMLElement[]} */
|
|
37
|
+
const siblings = []
|
|
38
|
+
let branch = popup
|
|
39
|
+
while (branch.parentElement) {
|
|
40
|
+
const parent = branch.parentElement
|
|
41
|
+
for (const sibling of parent.children) {
|
|
42
|
+
if (!(sibling instanceof HTMLElement) || sibling === branch) continue
|
|
43
|
+
const state = inertLayers.get(sibling) ?? { users: 0, prior: sibling.inert }
|
|
44
|
+
state.users++
|
|
45
|
+
inertLayers.set(sibling, state)
|
|
46
|
+
sibling.inert = true
|
|
47
|
+
siblings.push(sibling)
|
|
48
|
+
}
|
|
49
|
+
if (parent === document.body) break
|
|
50
|
+
branch = parent
|
|
51
|
+
}
|
|
52
|
+
const style = document.documentElement.style
|
|
53
|
+
if (scrollLocks++ === 0) {
|
|
54
|
+
priorOverflow = style.overflow
|
|
55
|
+
priorOverflowPriority = style.getPropertyPriority('overflow')
|
|
56
|
+
style.setProperty('overflow', 'hidden')
|
|
57
|
+
}
|
|
58
|
+
return () => {
|
|
59
|
+
for (const sibling of siblings) {
|
|
60
|
+
const state = inertLayers.get(sibling)
|
|
61
|
+
if (state && --state.users === 0) {
|
|
62
|
+
sibling.inert = state.prior
|
|
63
|
+
inertLayers.delete(sibling)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (--scrollLocks === 0) {
|
|
67
|
+
if (priorOverflow) style.setProperty('overflow', priorOverflow, priorOverflowPriority)
|
|
68
|
+
else style.removeProperty('overflow')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** @param {HTMLElement} picker */
|
|
73
|
+
const enhance = (picker) => {
|
|
74
|
+
const panel = picker.querySelector('[data-ui="date-calendar"]')
|
|
75
|
+
const trigger = picker.querySelector('[name="kv-date-open"]')
|
|
76
|
+
const inputs = /** @type {HTMLInputElement[]} */ ([
|
|
77
|
+
...picker.querySelectorAll('[data-ui="date-inputs"] input[type="date"], [data-ui="date-boundary"]'),
|
|
78
|
+
])
|
|
79
|
+
if (
|
|
80
|
+
!(panel instanceof HTMLElement) ||
|
|
81
|
+
!(trigger instanceof HTMLButtonElement) ||
|
|
82
|
+
!inputs.length ||
|
|
83
|
+
!('showPopover' in panel)
|
|
84
|
+
)
|
|
85
|
+
return () => {}
|
|
86
|
+
const popup = panel
|
|
87
|
+
const opener = trigger
|
|
88
|
+
const range = picker.dataset.dateMode === 'range'
|
|
89
|
+
const locale = picker.dataset.dateLocale === 'en' ? 'en-GB' : 'vi-VN'
|
|
90
|
+
const weekStart = Number(picker.dataset.dateWeekStart ?? 1)
|
|
91
|
+
const labels = /** @type {import('./index.tsx').DatePickerLabels} */ (
|
|
92
|
+
JSON.parse(picker.dataset.dateLabels ?? '{}')
|
|
93
|
+
)
|
|
94
|
+
const months = /** @type {HTMLElement} */ (popup.querySelector('[data-ui="date-calendar-months"]'))
|
|
95
|
+
const status = /** @type {HTMLElement} */ (popup.querySelector('[data-ui="date-calendar-status"]'))
|
|
96
|
+
const apply = /** @type {HTMLButtonElement} */ (popup.querySelector('[value="apply"]'))
|
|
97
|
+
const message = picker.querySelector('[data-ui="date-range-error"]')
|
|
98
|
+
const end = inputs[1]
|
|
99
|
+
const defaults = inputs.map((input) => input.value)
|
|
100
|
+
const display = picker.querySelector('input[type="text"]')
|
|
101
|
+
const preset = picker.querySelector('select')
|
|
102
|
+
const field = display instanceof HTMLInputElement ? display : end
|
|
103
|
+
const originalInvalid = field?.getAttribute('aria-invalid')
|
|
104
|
+
const originalDescription = field?.getAttribute('aria-describedby')
|
|
105
|
+
let returnFocus = /** @type {HTMLElement} */ (opener)
|
|
106
|
+
const readToday = () => {
|
|
107
|
+
const local = new Date()
|
|
108
|
+
const fallback = `${local.getFullYear()}-${String(local.getMonth() + 1).padStart(2, '0')}-${String(local.getDate()).padStart(2, '0')}`
|
|
109
|
+
return parseDate(picker.dataset.dateToday ?? '') ? (picker.dataset.dateToday ?? fallback) : fallback
|
|
110
|
+
}
|
|
111
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
112
|
+
day: 'numeric',
|
|
113
|
+
month: 'long',
|
|
114
|
+
year: 'numeric',
|
|
115
|
+
timeZone: 'UTC',
|
|
116
|
+
})
|
|
117
|
+
const short = new Intl.DateTimeFormat(locale, {
|
|
118
|
+
day: '2-digit',
|
|
119
|
+
month: '2-digit',
|
|
120
|
+
year: 'numeric',
|
|
121
|
+
timeZone: 'UTC',
|
|
122
|
+
})
|
|
123
|
+
const monthFormat = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' })
|
|
124
|
+
const weekdayFormat = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' })
|
|
125
|
+
let today = readToday()
|
|
126
|
+
let month = ''
|
|
127
|
+
let focusDate = ''
|
|
128
|
+
let preferredPreset = ''
|
|
129
|
+
/** @type {string[]} */
|
|
130
|
+
let draft = []
|
|
131
|
+
const isOpen = () => popup.matches(':popover-open')
|
|
132
|
+
const isMobile = () => window.innerWidth < 768
|
|
133
|
+
/** @type {(() => void) | null} */
|
|
134
|
+
let unlock = null
|
|
135
|
+
const syncPresentation = () => {
|
|
136
|
+
if (isOpen() && isMobile()) {
|
|
137
|
+
unlock ??= lockBackground(popup)
|
|
138
|
+
popup.setAttribute('aria-modal', 'true')
|
|
139
|
+
} else {
|
|
140
|
+
unlock?.()
|
|
141
|
+
unlock = null
|
|
142
|
+
popup.removeAttribute('aria-modal')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const monthCount = () => (range ? (isMobile() ? 12 : 2) : 1)
|
|
146
|
+
/** @param {string} value */
|
|
147
|
+
const calendarStart = (value) => {
|
|
148
|
+
const start = monthStart(value)
|
|
149
|
+
return range && isMobile() ? moveMonth(start, -2) || start : start
|
|
150
|
+
}
|
|
151
|
+
const visibleMonth = () => {
|
|
152
|
+
const top = months.getBoundingClientRect().top
|
|
153
|
+
const section = [...months.children].find((child) => child.getBoundingClientRect().bottom > top + 1)
|
|
154
|
+
return section instanceof HTMLElement ? (section.dataset.dateMonth ?? month) : month
|
|
155
|
+
}
|
|
156
|
+
const revealMonth = () => {
|
|
157
|
+
if (!range || !isMobile()) return
|
|
158
|
+
const section = months.querySelector(`[data-date-month="${monthStart(focusDate)}"]`)
|
|
159
|
+
if (section instanceof HTMLElement) {
|
|
160
|
+
months.scrollTop +=
|
|
161
|
+
section.getBoundingClientRect().top -
|
|
162
|
+
months.getBoundingClientRect().top -
|
|
163
|
+
parseFloat(getComputedStyle(months).paddingTop)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** @param {HTMLElement} day */
|
|
167
|
+
const revealDay = (day) => {
|
|
168
|
+
if (!isMobile()) return
|
|
169
|
+
const box = day.getBoundingClientRect()
|
|
170
|
+
const viewport = months.getBoundingClientRect()
|
|
171
|
+
if (box.top < viewport.top) months.scrollTop += box.top - viewport.top
|
|
172
|
+
else if (box.bottom > viewport.bottom) months.scrollTop += box.bottom - viewport.bottom
|
|
173
|
+
}
|
|
174
|
+
const enabled = () => inputs.every((input) => !input.disabled && !input.readOnly)
|
|
175
|
+
const valid = () =>
|
|
176
|
+
!!draft[0] &&
|
|
177
|
+
allowed(inputs[0], draft[0]) &&
|
|
178
|
+
(!range || (!!draft[1] && draft[0] <= draft[1] && allowed(end, draft[1])))
|
|
179
|
+
const validate = () => {
|
|
180
|
+
if (!end || !field) return true
|
|
181
|
+
const startValue = inputs[0].value
|
|
182
|
+
const endValue = end.value
|
|
183
|
+
const empty = !startValue && !endValue
|
|
184
|
+
const invalid =
|
|
185
|
+
!inputs[0].disabled &&
|
|
186
|
+
!end.disabled &&
|
|
187
|
+
(empty
|
|
188
|
+
? inputs.some((input) => input.required)
|
|
189
|
+
: !allowed(inputs[0], startValue) || !allowed(end, endValue) || startValue > endValue)
|
|
190
|
+
if (display instanceof HTMLInputElement) display.value = formatRange(startValue, endValue)
|
|
191
|
+
if (message instanceof HTMLElement) {
|
|
192
|
+
message.hidden = !invalid
|
|
193
|
+
field.setAttribute(
|
|
194
|
+
'aria-describedby',
|
|
195
|
+
[originalDescription, invalid ? message.id : ''].filter(Boolean).join(' '),
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
if (invalid) field.setAttribute('aria-invalid', 'true')
|
|
199
|
+
else if (originalInvalid) field.setAttribute('aria-invalid', originalInvalid)
|
|
200
|
+
else field.removeAttribute('aria-invalid')
|
|
201
|
+
if (preset instanceof HTMLSelectElement) {
|
|
202
|
+
preset.disabled = !enabled()
|
|
203
|
+
const matching = [...preset.options].filter((option) => {
|
|
204
|
+
const period = presetRange(option.value, today)
|
|
205
|
+
option.disabled = !!period && (!allowed(inputs[0], period[0]) || !allowed(end, period[1]))
|
|
206
|
+
return period && period[0] === startValue && period[1] === endValue
|
|
207
|
+
})
|
|
208
|
+
preset.value =
|
|
209
|
+
(matching.find((option) => option.value === preferredPreset) ?? matching[0])?.value ?? 'custom'
|
|
210
|
+
}
|
|
211
|
+
return !invalid
|
|
212
|
+
}
|
|
213
|
+
/** @param {string[]} values */
|
|
214
|
+
const commit = (values) => {
|
|
215
|
+
const changed = inputs.filter((input, index) => input.value !== values[index])
|
|
216
|
+
inputs.forEach((input, index) => {
|
|
217
|
+
input.value = values[index]
|
|
218
|
+
})
|
|
219
|
+
validate()
|
|
220
|
+
for (const input of [
|
|
221
|
+
...changed,
|
|
222
|
+
...(changed.length && display instanceof HTMLInputElement ? [display] : []),
|
|
223
|
+
]) {
|
|
224
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
225
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** @param {string} [preview] */
|
|
229
|
+
const paint = (preview = '') => {
|
|
230
|
+
const [from, to] =
|
|
231
|
+
range && draft[0] && !draft[1] && preview
|
|
232
|
+
? orderedRange(draft[0], preview)
|
|
233
|
+
: [draft[0], draft[1] ?? draft[0]]
|
|
234
|
+
for (const node of months.querySelectorAll('button[data-date]')) {
|
|
235
|
+
const button = /** @type {HTMLButtonElement} */ (node)
|
|
236
|
+
const day = button.dataset.date ?? ''
|
|
237
|
+
const cell = button.parentElement
|
|
238
|
+
const selected = day === draft[0] || day === draft[1]
|
|
239
|
+
button.dataset.selected = String(selected)
|
|
240
|
+
if (cell) {
|
|
241
|
+
cell.dataset.inRange = String(range && !!from && !!to && day >= from && day <= to)
|
|
242
|
+
cell.setAttribute(
|
|
243
|
+
'aria-selected',
|
|
244
|
+
String(selected || (range && !!from && !!to && day >= from && day <= to)),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const position = () => {
|
|
250
|
+
if (!isOpen() || isMobile()) return
|
|
251
|
+
const rect = opener.getBoundingClientRect()
|
|
252
|
+
const inset = parseFloat(getComputedStyle(popup).scrollMarginTop)
|
|
253
|
+
const gap = inset
|
|
254
|
+
popup.style.setProperty('--kv-date-max-height', `${Math.max(0, window.innerHeight - inset * 2)}px`)
|
|
255
|
+
const box = popup.getBoundingClientRect()
|
|
256
|
+
const left = Math.max(
|
|
257
|
+
inset,
|
|
258
|
+
Math.min(
|
|
259
|
+
rect.left + box.width <= window.innerWidth - inset ? rect.left : rect.right - box.width,
|
|
260
|
+
window.innerWidth - box.width - inset,
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
const below = rect.bottom + gap
|
|
264
|
+
const top =
|
|
265
|
+
below + box.height <= window.innerHeight - inset ? below : Math.max(inset, rect.top - box.height - gap)
|
|
266
|
+
popup.style.setProperty('--kv-date-left', `${left}px`)
|
|
267
|
+
popup.style.setProperty('--kv-date-top', `${top}px`)
|
|
268
|
+
}
|
|
269
|
+
/** @param {boolean} [focus] */
|
|
270
|
+
const render = (focus = false) => {
|
|
271
|
+
const scrollTop = months.scrollTop
|
|
272
|
+
months.replaceChildren()
|
|
273
|
+
popup.dataset.months = String(monthCount())
|
|
274
|
+
for (let index = 0; index < monthCount(); index++) {
|
|
275
|
+
const current = moveMonth(month, index)
|
|
276
|
+
if (!current) continue
|
|
277
|
+
const section = document.createElement('section')
|
|
278
|
+
section.dataset.ui = 'date-month'
|
|
279
|
+
section.dataset.dateMonth = current
|
|
280
|
+
const title = document.createElement('h4')
|
|
281
|
+
title.dataset.ui = 'date-month-title'
|
|
282
|
+
title.id = `${popup.id}-month-${index}`
|
|
283
|
+
title.textContent = monthFormat.format(civil(current))
|
|
284
|
+
if (!isMobile()) title.setAttribute('aria-live', 'polite')
|
|
285
|
+
section.append(title)
|
|
286
|
+
const table = document.createElement('table')
|
|
287
|
+
table.dataset.ui = 'date-grid'
|
|
288
|
+
table.setAttribute('role', 'grid')
|
|
289
|
+
table.setAttribute('aria-labelledby', title.id)
|
|
290
|
+
if (range) table.setAttribute('aria-multiselectable', 'true')
|
|
291
|
+
const head = table.createTHead().insertRow()
|
|
292
|
+
for (let column = 0; column < 7; column++) {
|
|
293
|
+
const th = document.createElement('th')
|
|
294
|
+
th.dataset.ui = 'date-weekday'
|
|
295
|
+
th.scope = 'col'
|
|
296
|
+
th.textContent = weekdayFormat.format(civil(addDays('2026-09-06', (column + weekStart) % 7)))
|
|
297
|
+
head.append(th)
|
|
298
|
+
}
|
|
299
|
+
const body = table.createTBody()
|
|
300
|
+
const offset = (civil(current).getUTCDay() - weekStart + 7) % 7
|
|
301
|
+
const days = civil(monthEnd(current)).getUTCDate()
|
|
302
|
+
const weeks = isMobile() ? Math.ceil((offset + days) / 7) : 6
|
|
303
|
+
for (let row = 0; row < weeks; row++) {
|
|
304
|
+
const tr = body.insertRow()
|
|
305
|
+
for (let column = 0; column < 7; column++) {
|
|
306
|
+
const cell = tr.insertCell()
|
|
307
|
+
cell.dataset.ui = 'date-cell'
|
|
308
|
+
const number = row * 7 + column - offset + 1
|
|
309
|
+
if (number < 1 || number > days) {
|
|
310
|
+
cell.setAttribute('aria-hidden', 'true')
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
const value = addDays(current, number - 1)
|
|
314
|
+
const button = document.createElement('button')
|
|
315
|
+
button.type = 'button'
|
|
316
|
+
button.dataset.ui = 'date-day'
|
|
317
|
+
button.dataset.date = value
|
|
318
|
+
button.textContent = String(number)
|
|
319
|
+
button.setAttribute('aria-label', formatter.format(civil(value)))
|
|
320
|
+
if (value === today) button.setAttribute('aria-current', 'date')
|
|
321
|
+
if (range && draft[0] && !draft[1]) {
|
|
322
|
+
const [start, finish] = orderedRange(draft[0], value)
|
|
323
|
+
button.disabled = !allowed(inputs[0], start) || !allowed(end, finish)
|
|
324
|
+
} else button.disabled = !allowed(inputs[0], value)
|
|
325
|
+
button.tabIndex = value === focusDate && !button.disabled ? 0 : -1
|
|
326
|
+
cell.append(button)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
section.append(table)
|
|
330
|
+
months.append(section)
|
|
331
|
+
}
|
|
332
|
+
months.scrollTop = scrollTop
|
|
333
|
+
const focusable =
|
|
334
|
+
months.querySelector('button[tabindex="0"]') ?? months.querySelector('button:not(:disabled)')
|
|
335
|
+
if (focusable instanceof HTMLButtonElement) {
|
|
336
|
+
focusable.tabIndex = 0
|
|
337
|
+
focusDate = focusable.dataset.date ?? focusDate
|
|
338
|
+
if (focus) {
|
|
339
|
+
focusable.focus({ preventScroll: true })
|
|
340
|
+
revealDay(focusable)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const command of ['previous', 'next']) {
|
|
344
|
+
const button = /** @type {HTMLButtonElement} */ (popup.querySelector(`[value="${command}"]`))
|
|
345
|
+
button.disabled = !moveMonth(
|
|
346
|
+
range && isMobile() ? monthStart(focusDate) : month,
|
|
347
|
+
command === 'previous' ? -1 : range && isMobile() ? 1 : monthCount(),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
apply.disabled = !valid()
|
|
351
|
+
status.textContent = draft[0]
|
|
352
|
+
? range && !draft[1]
|
|
353
|
+
? `${short.format(civil(draft[0]))} · ${labels.chooseEnd}`
|
|
354
|
+
: `${short.format(civil(draft[0]))}${range ? ` — ${short.format(civil(draft[1]))}` : ''}`
|
|
355
|
+
: range
|
|
356
|
+
? labels.chooseStart
|
|
357
|
+
: labels.chooseDate
|
|
358
|
+
paint(focus && range && draft[0] && !draft[1] ? focusDate : '')
|
|
359
|
+
position()
|
|
360
|
+
}
|
|
361
|
+
/** @param {HTMLElement} [source] */
|
|
362
|
+
const open = (source = opener) => {
|
|
363
|
+
if (!enabled()) return
|
|
364
|
+
returnFocus = source
|
|
365
|
+
today = readToday()
|
|
366
|
+
draft = inputs.map((input) => (parseDate(input.value) ? input.value : ''))
|
|
367
|
+
focusDate = draft[0] || today
|
|
368
|
+
if (parseDate(inputs[0].min) && focusDate < inputs[0].min) focusDate = inputs[0].min
|
|
369
|
+
if (parseDate(inputs[0].max) && focusDate > inputs[0].max) focusDate = inputs[0].max
|
|
370
|
+
month = calendarStart(focusDate)
|
|
371
|
+
render()
|
|
372
|
+
popup.showPopover()
|
|
373
|
+
syncPresentation()
|
|
374
|
+
opener.setAttribute('aria-expanded', 'true')
|
|
375
|
+
position()
|
|
376
|
+
revealMonth()
|
|
377
|
+
const first = months.querySelector('button[tabindex="0"]')
|
|
378
|
+
if (first instanceof HTMLElement) {
|
|
379
|
+
first.focus({ preventScroll: true })
|
|
380
|
+
revealDay(first)
|
|
381
|
+
} else {
|
|
382
|
+
const cancel = popup.querySelector('[value="cancel"]')
|
|
383
|
+
if (cancel instanceof HTMLElement) cancel.focus()
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** @param {boolean} [restore] */
|
|
387
|
+
const close = (restore = false) => {
|
|
388
|
+
if (isOpen()) popup.hidePopover()
|
|
389
|
+
syncPresentation()
|
|
390
|
+
opener.setAttribute('aria-expanded', 'false')
|
|
391
|
+
if (restore && returnFocus.isConnected) returnFocus.focus({ preventScroll: true })
|
|
392
|
+
}
|
|
393
|
+
/** @param {Event} event */
|
|
394
|
+
const click = (event) => {
|
|
395
|
+
if (event.target === display && display instanceof HTMLElement) {
|
|
396
|
+
isOpen() ? close(true) : open(display)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
const target = event.target instanceof Element ? event.target.closest('button') : null
|
|
400
|
+
if (!(target instanceof HTMLButtonElement) || target.disabled) return
|
|
401
|
+
if (target === opener) {
|
|
402
|
+
isOpen() ? close(true) : open()
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
if (!popup.contains(target) || !enabled()) return
|
|
406
|
+
if (target.dataset.date) {
|
|
407
|
+
const value = target.dataset.date
|
|
408
|
+
preferredPreset = ''
|
|
409
|
+
draft = range ? (draft[0] && !draft[1] ? orderedRange(draft[0], value) : [value, '']) : [value]
|
|
410
|
+
focusDate = value
|
|
411
|
+
render(true)
|
|
412
|
+
} else if (target.name === 'kv-date-command') {
|
|
413
|
+
if (target.value === 'cancel') close(true)
|
|
414
|
+
else if (target.value === 'apply' && valid()) {
|
|
415
|
+
const values = [...draft]
|
|
416
|
+
close(true)
|
|
417
|
+
commit(values)
|
|
418
|
+
} else if (target.value === 'previous' || target.value === 'next') {
|
|
419
|
+
const next = moveMonth(
|
|
420
|
+
range && isMobile() ? visibleMonth() : month,
|
|
421
|
+
target.value === 'previous' ? -1 : 1,
|
|
422
|
+
)
|
|
423
|
+
if (!next) return
|
|
424
|
+
focusDate = next
|
|
425
|
+
month = range && isMobile() ? calendarStart(next) : next
|
|
426
|
+
render()
|
|
427
|
+
revealMonth()
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** @param {KeyboardEvent} event */
|
|
432
|
+
const keydown = (event) => {
|
|
433
|
+
if (
|
|
434
|
+
!isOpen() &&
|
|
435
|
+
event.target === display &&
|
|
436
|
+
display instanceof HTMLElement &&
|
|
437
|
+
['Enter', ' ', 'ArrowDown'].includes(event.key)
|
|
438
|
+
) {
|
|
439
|
+
event.preventDefault()
|
|
440
|
+
open(display)
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
if (!isOpen()) return
|
|
444
|
+
if (event.key === 'Escape') {
|
|
445
|
+
event.preventDefault()
|
|
446
|
+
event.stopImmediatePropagation()
|
|
447
|
+
close(true)
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (event.key === 'Tab' && isMobile()) {
|
|
451
|
+
const items = [
|
|
452
|
+
...popup.querySelectorAll('button:not(:disabled):not([tabindex="-1"]), select:not(:disabled)'),
|
|
453
|
+
]
|
|
454
|
+
const first = items[0]
|
|
455
|
+
const last = items.at(-1)
|
|
456
|
+
if (event.shiftKey && document.activeElement === first && last instanceof HTMLElement) {
|
|
457
|
+
event.preventDefault()
|
|
458
|
+
last.focus()
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
if (!event.shiftKey && document.activeElement === last && first instanceof HTMLElement) {
|
|
462
|
+
event.preventDefault()
|
|
463
|
+
first.focus()
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const target = event.target
|
|
468
|
+
if (!(target instanceof HTMLButtonElement) || !target.dataset.date) return
|
|
469
|
+
const value = target.dataset.date
|
|
470
|
+
const weekday = (civil(value).getUTCDay() - weekStart + 7) % 7
|
|
471
|
+
const offsets = {
|
|
472
|
+
ArrowLeft: -1,
|
|
473
|
+
ArrowRight: 1,
|
|
474
|
+
ArrowUp: -7,
|
|
475
|
+
ArrowDown: 7,
|
|
476
|
+
Home: -weekday,
|
|
477
|
+
End: 6 - weekday,
|
|
478
|
+
}
|
|
479
|
+
let next =
|
|
480
|
+
event.key in offsets
|
|
481
|
+
? addDays(value, offsets[/** @type {keyof typeof offsets} */ (event.key)])
|
|
482
|
+
: event.key === 'PageUp' || event.key === 'PageDown'
|
|
483
|
+
? moveMonth(value, (event.key === 'PageUp' ? -1 : 1) * (event.shiftKey ? 12 : 1))
|
|
484
|
+
: ''
|
|
485
|
+
if (!next) return
|
|
486
|
+
event.preventDefault()
|
|
487
|
+
for (let attempt = 0; attempt < 366 && next; attempt++) {
|
|
488
|
+
const period = range && draft[0] && !draft[1] ? orderedRange(draft[0], next) : [next]
|
|
489
|
+
if (allowed(inputs[0], period[0]) && (!period[1] || allowed(end, period[1]))) break
|
|
490
|
+
next = addDays(next, next < value ? -1 : 1)
|
|
491
|
+
}
|
|
492
|
+
const period = range && draft[0] && !draft[1] ? orderedRange(draft[0], next) : [next]
|
|
493
|
+
if (!next || !allowed(inputs[0], period[0]) || (period[1] && !allowed(end, period[1]))) return
|
|
494
|
+
focusDate = next
|
|
495
|
+
if (next < month || next > monthEnd(moveMonth(month, monthCount() - 1))) month = monthStart(next)
|
|
496
|
+
render(true)
|
|
497
|
+
}
|
|
498
|
+
/** @param {PointerEvent} event */
|
|
499
|
+
const hover = (event) => {
|
|
500
|
+
const target = event.target
|
|
501
|
+
if (range && draft[0] && !draft[1] && target instanceof HTMLButtonElement && !target.disabled)
|
|
502
|
+
paint(target.dataset.date)
|
|
503
|
+
}
|
|
504
|
+
const leave = () => paint()
|
|
505
|
+
const toggle = () => {
|
|
506
|
+
syncPresentation()
|
|
507
|
+
opener.setAttribute('aria-expanded', String(isOpen()))
|
|
508
|
+
if (!isOpen()) draft = []
|
|
509
|
+
}
|
|
510
|
+
/** @param {FocusEvent} event */
|
|
511
|
+
const focusout = (event) => {
|
|
512
|
+
if (isOpen() && event.target instanceof Node && !popup.contains(event.target) && event.target !== opener)
|
|
513
|
+
close()
|
|
514
|
+
}
|
|
515
|
+
let mobileLayout = isMobile()
|
|
516
|
+
const resize = () => {
|
|
517
|
+
if (isOpen()) {
|
|
518
|
+
const changed = mobileLayout !== isMobile()
|
|
519
|
+
if (changed) month = calendarStart(focusDate)
|
|
520
|
+
syncPresentation()
|
|
521
|
+
render(months.contains(document.activeElement))
|
|
522
|
+
if (changed) revealMonth()
|
|
523
|
+
}
|
|
524
|
+
mobileLayout = isMobile()
|
|
525
|
+
}
|
|
526
|
+
/** @param {Event} event */
|
|
527
|
+
const change = (event) => {
|
|
528
|
+
if (event.target === preset && preset instanceof HTMLSelectElement && enabled()) {
|
|
529
|
+
today = readToday()
|
|
530
|
+
const period = presetRange(preset.value, today)
|
|
531
|
+
if (period && allowed(inputs[0], period[0]) && allowed(end, period[1])) {
|
|
532
|
+
preferredPreset = preset.value
|
|
533
|
+
close(true)
|
|
534
|
+
commit(period)
|
|
535
|
+
} else {
|
|
536
|
+
validate()
|
|
537
|
+
open(preset)
|
|
538
|
+
}
|
|
539
|
+
} else validate()
|
|
540
|
+
}
|
|
541
|
+
/** @param {SubmitEvent} event */
|
|
542
|
+
const submit = (event) => {
|
|
543
|
+
if (
|
|
544
|
+
!validate() &&
|
|
545
|
+
!inputs[0].form?.noValidate &&
|
|
546
|
+
!(event.submitter instanceof HTMLButtonElement && event.submitter.formNoValidate)
|
|
547
|
+
) {
|
|
548
|
+
event.preventDefault()
|
|
549
|
+
field?.focus()
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const reset = () => {
|
|
553
|
+
close()
|
|
554
|
+
setTimeout(() => {
|
|
555
|
+
if (range)
|
|
556
|
+
inputs.forEach((input, index) => {
|
|
557
|
+
input.value = defaults[index]
|
|
558
|
+
})
|
|
559
|
+
preferredPreset = ''
|
|
560
|
+
validate()
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
picker.dataset.dateEnhanced = 'true'
|
|
564
|
+
validate()
|
|
565
|
+
picker.addEventListener('click', click)
|
|
566
|
+
picker.addEventListener('keydown', keydown)
|
|
567
|
+
picker.addEventListener('change', change)
|
|
568
|
+
popup.addEventListener('toggle', toggle)
|
|
569
|
+
months.addEventListener('pointerover', hover)
|
|
570
|
+
months.addEventListener('pointerleave', leave)
|
|
571
|
+
document.addEventListener('focusin', focusout)
|
|
572
|
+
window.addEventListener('resize', resize)
|
|
573
|
+
window.addEventListener('scroll', position, true)
|
|
574
|
+
inputs[0].form?.addEventListener('reset', reset)
|
|
575
|
+
inputs[0].form?.addEventListener('submit', submit)
|
|
576
|
+
return () => {
|
|
577
|
+
close()
|
|
578
|
+
delete picker.dataset.dateEnhanced
|
|
579
|
+
picker.removeEventListener('click', click)
|
|
580
|
+
picker.removeEventListener('keydown', keydown)
|
|
581
|
+
picker.removeEventListener('change', change)
|
|
582
|
+
popup.removeEventListener('toggle', toggle)
|
|
583
|
+
months.removeEventListener('pointerover', hover)
|
|
584
|
+
months.removeEventListener('pointerleave', leave)
|
|
585
|
+
document.removeEventListener('focusin', focusout)
|
|
586
|
+
window.removeEventListener('resize', resize)
|
|
587
|
+
window.removeEventListener('scroll', position, true)
|
|
588
|
+
inputs[0].form?.removeEventListener('reset', reset)
|
|
589
|
+
inputs[0].form?.removeEventListener('submit', submit)
|
|
590
|
+
if (preset instanceof HTMLSelectElement) preset.disabled = true
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/** Shared ownership prevents duplicate listeners when document and island roots overlap.
|
|
594
|
+
* @type {WeakMap<HTMLElement, { users: number, cleanup: () => void }>} */
|
|
595
|
+
const instances = new WeakMap()
|
|
596
|
+
/** @param {HTMLElement} node */
|
|
597
|
+
const acquire = (node) => {
|
|
598
|
+
let instance = instances.get(node)
|
|
599
|
+
if (!instance) {
|
|
600
|
+
instance = { users: 0, cleanup: enhance(node) }
|
|
601
|
+
instances.set(node, instance)
|
|
602
|
+
}
|
|
603
|
+
const owned = instance
|
|
604
|
+
owned.users++
|
|
605
|
+
return () => {
|
|
606
|
+
if (--owned.users === 0) {
|
|
607
|
+
owned.cleanup()
|
|
608
|
+
instances.delete(node)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/** Enhances current and subsequently inserted fields; cleanup also releases removed islands.
|
|
613
|
+
* @param {ParentNode} root */
|
|
614
|
+
export const attachDatePickers = (root) => {
|
|
615
|
+
if (typeof HTMLElement === 'undefined' || typeof Node === 'undefined') return () => {}
|
|
616
|
+
/** @type {Map<HTMLElement, () => void>} */
|
|
617
|
+
const attached = new Map()
|
|
618
|
+
const sync = () => {
|
|
619
|
+
for (const [element, cleanup] of attached)
|
|
620
|
+
if (element !== root && !root.contains(element)) {
|
|
621
|
+
cleanup()
|
|
622
|
+
attached.delete(element)
|
|
623
|
+
}
|
|
624
|
+
const candidates = [...root.querySelectorAll('[data-date-mode]')]
|
|
625
|
+
if (root instanceof HTMLElement && root.matches('[data-date-mode]')) candidates.unshift(root)
|
|
626
|
+
for (const node of candidates)
|
|
627
|
+
if (node instanceof HTMLElement && !attached.has(node)) attached.set(node, acquire(node))
|
|
628
|
+
}
|
|
629
|
+
sync()
|
|
630
|
+
const observer = typeof MutationObserver === 'undefined' ? null : new MutationObserver(sync)
|
|
631
|
+
if (root instanceof Node) observer?.observe(root, { childList: true, subtree: true })
|
|
632
|
+
return () => {
|
|
633
|
+
observer?.disconnect()
|
|
634
|
+
for (const cleanup of attached.values()) cleanup()
|
|
635
|
+
attached.clear()
|
|
636
|
+
}
|
|
637
|
+
}
|