@zeyos/zx 2.1.0 → 2.2.1
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/CHANGELOG.md +51 -0
- package/README.md +15 -0
- package/dist/types/compat/base.d.ts +156 -0
- package/dist/types/compat/globals.d.ts +20 -0
- package/dist/types/compat/index.d.ts +128 -0
- package/dist/types/compat/map/basic.d.ts +175 -0
- package/dist/types/compat/map/data.d.ts +102 -0
- package/dist/types/compat/map/date-time.d.ts +80 -0
- package/dist/types/compat/map/factory.d.ts +9 -0
- package/dist/types/compat/map/form.d.ts +98 -0
- package/dist/types/compat/map/helpers.d.ts +12 -0
- package/dist/types/compat/map/http.d.ts +36 -0
- package/dist/types/compat/map/layout.d.ts +106 -0
- package/dist/types/compat/map/options.d.ts +80 -0
- package/dist/types/compat/map/select.d.ts +95 -0
- package/dist/types/compat/map/stubs.d.ts +32 -0
- package/dist/types/compat/map/utilities.d.ts +37 -0
- package/dist/types/compat-entry.d.ts +5 -0
- package/dist/types/components/badge/badge.d.ts +46 -0
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
- package/dist/types/components/button/button.d.ts +42 -0
- package/dist/types/components/check-button/check-button.d.ts +98 -0
- package/dist/types/components/checklist/checklist.d.ts +143 -0
- package/dist/types/components/checklist/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +130 -0
- package/dist/types/components/copy/copy-button.d.ts +48 -0
- package/dist/types/components/copy/copy-input.d.ts +91 -0
- package/dist/types/components/data-filter/data-filter.d.ts +250 -0
- package/dist/types/components/data-filter/filter-core.d.ts +52 -0
- package/dist/types/components/date-picker/date-picker.d.ts +137 -0
- package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
- package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
- package/dist/types/components/date-picker/month-picker.d.ts +75 -0
- package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
- package/dist/types/components/date-picker/time-picker.d.ts +127 -0
- package/dist/types/components/datebox/date-range-box.d.ts +218 -0
- package/dist/types/components/datebox/datebox.d.ts +160 -0
- package/dist/types/components/datebox/field-adapter.d.ts +5 -0
- package/dist/types/components/dialog/dialog.d.ts +284 -0
- package/dist/types/components/dropdown/dropdown.d.ts +118 -0
- package/dist/types/components/empty-state/empty-state.d.ts +38 -0
- package/dist/types/components/field/field.d.ts +231 -0
- package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
- package/dist/types/components/field-upload/field-upload.d.ts +213 -0
- package/dist/types/components/fieldset/fieldset.d.ts +125 -0
- package/dist/types/components/finder/finder.d.ts +252 -0
- package/dist/types/components/form/form.d.ts +194 -0
- package/dist/types/components/groupbox/groupbox.d.ts +89 -0
- package/dist/types/components/layout/layout.d.ts +132 -0
- package/dist/types/components/loading/inline-loading.d.ts +86 -0
- package/dist/types/components/loading/progress-bar.d.ts +168 -0
- package/dist/types/components/loading/spinner.d.ts +34 -0
- package/dist/types/components/master-panel/master-panel.d.ts +112 -0
- package/dist/types/components/menu-button/menu-button.d.ts +142 -0
- package/dist/types/components/message/message.d.ts +212 -0
- package/dist/types/components/modal/modal.d.ts +105 -0
- package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
- package/dist/types/components/number-field/field-adapter.d.ts +5 -0
- package/dist/types/components/number-field/number-field.d.ts +292 -0
- package/dist/types/components/pagination/pagination.d.ts +267 -0
- package/dist/types/components/panel/panel.d.ts +194 -0
- package/dist/types/components/permission/permission.d.ts +93 -0
- package/dist/types/components/rating/field-adapter.d.ts +5 -0
- package/dist/types/components/rating/rating.d.ts +213 -0
- package/dist/types/components/search/search.d.ts +98 -0
- package/dist/types/components/select/field-adapter.d.ts +5 -0
- package/dist/types/components/select/filter.d.ts +11 -0
- package/dist/types/components/select/select.d.ts +235 -0
- package/dist/types/components/skeleton/skeleton.d.ts +97 -0
- package/dist/types/components/slider/field-adapter.d.ts +5 -0
- package/dist/types/components/slider/slider.d.ts +216 -0
- package/dist/types/components/split-view/split-view.d.ts +335 -0
- package/dist/types/components/stepper/stepper.d.ts +314 -0
- package/dist/types/components/stepper/steps.d.ts +131 -0
- package/dist/types/components/tabbox/tabbox.d.ts +230 -0
- package/dist/types/components/table/sort.d.ts +49 -0
- package/dist/types/components/table/table.d.ts +872 -0
- package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
- package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
- package/dist/types/components/timebox/field-adapter.d.ts +5 -0
- package/dist/types/components/timebox/timebox.d.ts +121 -0
- package/dist/types/components/toggle/field-adapter.d.ts +5 -0
- package/dist/types/components/toggle/toggle.d.ts +94 -0
- package/dist/types/components/toolbar/toolbar.d.ts +230 -0
- package/dist/types/components/tooltip/tooltip.d.ts +183 -0
- package/dist/types/components/tree/hierarchy.d.ts +113 -0
- package/dist/types/components/tree/tree.d.ts +383 -0
- package/dist/types/components/truncate/truncate.d.ts +61 -0
- package/dist/types/components/value-list/field-adapter.d.ts +5 -0
- package/dist/types/components/value-list/value-list.d.ts +143 -0
- package/dist/types/core/breakpoint.d.ts +68 -0
- package/dist/types/core/component.d.ts +115 -0
- package/dist/types/core/date.d.ts +50 -0
- package/dist/types/core/dom.d.ts +86 -0
- package/dist/types/core/export.d.ts +70 -0
- package/dist/types/core/fontawesome.d.ts +107 -0
- package/dist/types/core/format.d.ts +107 -0
- package/dist/types/core/http.d.ts +122 -0
- package/dist/types/core/i18n.d.ts +35 -0
- package/dist/types/core/icons.d.ts +137 -0
- package/dist/types/core/keyboard.d.ts +72 -0
- package/dist/types/core/position.d.ts +45 -0
- package/dist/types/core/storage.d.ts +46 -0
- package/dist/types/core/util.d.ts +117 -0
- package/dist/types/elements/define.d.ts +73 -0
- package/dist/types/elements/reflect.d.ts +27 -0
- package/dist/types/index.d.ts +79 -0
- package/dist/types/zeyos/connect.d.ts +65 -0
- package/dist/types/zeyos/form.d.ts +140 -0
- package/dist/types/zeyos/icons.d.ts +104 -0
- package/dist/types/zeyos/index.d.ts +12 -0
- package/dist/types/zeyos/modules.d.ts +110 -0
- package/dist/types/zeyos/query.d.ts +157 -0
- package/dist/types/zeyos/schema.d.ts +79 -0
- package/dist/types/zeyos/select.d.ts +167 -0
- package/dist/types/zeyos/table.d.ts +205 -0
- package/dist/zx-compat.esm.js +375 -39
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +372 -73
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +355 -49
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +68 -6
- package/dist/zx.esm.js +359 -73
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +359 -73
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +48 -2
- package/docs/llms.md +31 -0
- package/package.json +37 -9
- package/src/compat/base.js +291 -0
- package/src/compat/globals.js +165 -0
- package/src/compat/index.js +84 -0
- package/src/compat/map/basic.js +464 -0
- package/src/compat/map/data.js +232 -0
- package/src/compat/map/date-time.js +177 -0
- package/src/compat/map/factory.js +45 -0
- package/src/compat/map/form.js +330 -0
- package/src/compat/map/helpers.js +46 -0
- package/src/compat/map/http.js +117 -0
- package/src/compat/map/layout.js +256 -0
- package/src/compat/map/options.js +267 -0
- package/src/compat/map/select.js +198 -0
- package/src/compat/map/stubs.js +26 -0
- package/src/compat/map/utilities.js +113 -0
- package/src/compat-entry.js +11 -0
- package/src/components/badge/badge.css +147 -0
- package/src/components/badge/badge.js +92 -0
- package/src/components/breadcrumb/breadcrumb.css +105 -0
- package/src/components/breadcrumb/breadcrumb.js +331 -0
- package/src/components/button/button.css +104 -0
- package/src/components/button/button.js +76 -0
- package/src/components/check-button/check-button.css +66 -0
- package/src/components/check-button/check-button.js +167 -0
- package/src/components/checklist/checklist.css +65 -0
- package/src/components/checklist/checklist.js +282 -0
- package/src/components/checklist/field-adapter.js +46 -0
- package/src/components/context-menu/context-menu.css +54 -0
- package/src/components/context-menu/context-menu.js +334 -0
- package/src/components/copy/copy-button.css +27 -0
- package/src/components/copy/copy-button.js +78 -0
- package/src/components/copy/copy-input.css +59 -0
- package/src/components/copy/copy-input.js +123 -0
- package/src/components/data-filter/data-filter.css +53 -0
- package/src/components/data-filter/data-filter.js +400 -0
- package/src/components/data-filter/filter-core.js +128 -0
- package/src/components/date-picker/date-picker.css +181 -0
- package/src/components/date-picker/date-picker.js +500 -0
- package/src/components/date-picker/date-range-picker.css +264 -0
- package/src/components/date-picker/date-range-picker.js +990 -0
- package/src/components/date-picker/field-adapter.js +36 -0
- package/src/components/date-picker/month-picker.js +223 -0
- package/src/components/date-picker/range-field-adapter.js +31 -0
- package/src/components/date-picker/time-picker.css +191 -0
- package/src/components/date-picker/time-picker.js +406 -0
- package/src/components/datebox/date-range-box.css +128 -0
- package/src/components/datebox/date-range-box.js +492 -0
- package/src/components/datebox/datebox.css +108 -0
- package/src/components/datebox/datebox.js +358 -0
- package/src/components/datebox/field-adapter.js +34 -0
- package/src/components/dialog/dialog.css +74 -0
- package/src/components/dialog/dialog.js +450 -0
- package/src/components/dropdown/dropdown.css +24 -0
- package/src/components/dropdown/dropdown.js +288 -0
- package/src/components/empty-state/empty-state.css +80 -0
- package/src/components/empty-state/empty-state.js +92 -0
- package/src/components/field/field.css +127 -0
- package/src/components/field/field.js +605 -0
- package/src/components/field-upload/field-adapter.js +21 -0
- package/src/components/field-upload/field-upload.css +108 -0
- package/src/components/field-upload/field-upload.js +475 -0
- package/src/components/fieldset/fieldset.css +37 -0
- package/src/components/fieldset/fieldset.js +307 -0
- package/src/components/finder/finder.css +98 -0
- package/src/components/finder/finder.js +509 -0
- package/src/components/form/form.css +32 -0
- package/src/components/form/form.js +302 -0
- package/src/components/groupbox/groupbox.css +51 -0
- package/src/components/groupbox/groupbox.js +199 -0
- package/src/components/layout/layout.css +117 -0
- package/src/components/layout/layout.js +136 -0
- package/src/components/loading/inline-loading.css +34 -0
- package/src/components/loading/inline-loading.js +140 -0
- package/src/components/loading/progress-bar.css +115 -0
- package/src/components/loading/progress-bar.js +259 -0
- package/src/components/loading/spinner.css +58 -0
- package/src/components/loading/spinner.js +57 -0
- package/src/components/master-panel/master-panel.css +51 -0
- package/src/components/master-panel/master-panel.js +203 -0
- package/src/components/menu-button/menu-button.css +49 -0
- package/src/components/menu-button/menu-button.js +294 -0
- package/src/components/message/message.css +122 -0
- package/src/components/message/message.js +499 -0
- package/src/components/modal/modal.css +47 -0
- package/src/components/modal/modal.js +203 -0
- package/src/components/multi-value-editor/field-adapter.js +44 -0
- package/src/components/multi-value-editor/multi-value-editor.css +56 -0
- package/src/components/multi-value-editor/multi-value-editor.js +170 -0
- package/src/components/navigation-bar/navigation-bar.css +130 -0
- package/src/components/navigation-bar/navigation-bar.js +450 -0
- package/src/components/number-field/field-adapter.js +40 -0
- package/src/components/number-field/number-field.css +77 -0
- package/src/components/number-field/number-field.js +545 -0
- package/src/components/pagination/pagination.css +95 -0
- package/src/components/pagination/pagination.js +481 -0
- package/src/components/panel/panel.css +123 -0
- package/src/components/panel/panel.js +336 -0
- package/src/components/permission/permission.css +47 -0
- package/src/components/permission/permission.js +198 -0
- package/src/components/rating/field-adapter.js +34 -0
- package/src/components/rating/rating.css +97 -0
- package/src/components/rating/rating.js +395 -0
- package/src/components/search/search.css +70 -0
- package/src/components/search/search.js +193 -0
- package/src/components/select/field-adapter.js +24 -0
- package/src/components/select/filter.js +39 -0
- package/src/components/select/select.css +180 -0
- package/src/components/select/select.js +663 -0
- package/src/components/skeleton/skeleton.css +76 -0
- package/src/components/skeleton/skeleton.js +139 -0
- package/src/components/slider/field-adapter.js +36 -0
- package/src/components/slider/slider.css +181 -0
- package/src/components/slider/slider.js +399 -0
- package/src/components/split-view/split-view.css +139 -0
- package/src/components/split-view/split-view.js +806 -0
- package/src/components/stepper/stepper.css +234 -0
- package/src/components/stepper/stepper.js +459 -0
- package/src/components/stepper/steps.js +131 -0
- package/src/components/tabbox/tabbox.css +281 -0
- package/src/components/tabbox/tabbox.js +484 -0
- package/src/components/table/sort.js +85 -0
- package/src/components/table/table.css +364 -0
- package/src/components/table/table.js +1711 -0
- package/src/components/tag-picker/field-adapter.js +35 -0
- package/src/components/tag-picker/tag-picker.css +169 -0
- package/src/components/tag-picker/tag-picker.js +837 -0
- package/src/components/timebox/field-adapter.js +24 -0
- package/src/components/timebox/timebox.css +51 -0
- package/src/components/timebox/timebox.js +276 -0
- package/src/components/toggle/field-adapter.js +24 -0
- package/src/components/toggle/toggle.css +66 -0
- package/src/components/toggle/toggle.js +150 -0
- package/src/components/toolbar/toolbar.css +81 -0
- package/src/components/toolbar/toolbar.js +511 -0
- package/src/components/tooltip/tooltip.css +36 -0
- package/src/components/tooltip/tooltip.js +420 -0
- package/src/components/tree/hierarchy.js +151 -0
- package/src/components/tree/tree.css +99 -0
- package/src/components/tree/tree.js +784 -0
- package/src/components/truncate/truncate.css +20 -0
- package/src/components/truncate/truncate.js +92 -0
- package/src/components/value-list/field-adapter.js +24 -0
- package/src/components/value-list/value-list.css +108 -0
- package/src/components/value-list/value-list.js +336 -0
- package/src/core/breakpoint.js +150 -0
- package/src/core/component.js +292 -0
- package/src/core/date.js +216 -0
- package/src/core/dom.js +260 -0
- package/src/core/export.js +204 -0
- package/src/core/fontawesome.js +177 -0
- package/src/core/format.js +283 -0
- package/src/core/http.js +246 -0
- package/src/core/i18n.js +83 -0
- package/src/core/icons.js +283 -0
- package/src/core/keyboard.js +240 -0
- package/src/core/position.js +177 -0
- package/src/core/storage.js +175 -0
- package/src/core/util.js +336 -0
- package/src/elements/define.js +715 -0
- package/src/elements/reflect.js +79 -0
- package/src/index.js +131 -0
- package/src/zeyos/connect.js +142 -0
- package/src/zeyos/form.js +254 -0
- package/src/zeyos/icons.js +92 -0
- package/src/zeyos/index.js +21 -0
- package/src/zeyos/modules.js +269 -0
- package/src/zeyos/query.js +199 -0
- package/src/zeyos/schema.js +474 -0
- package/src/zeyos/select.js +175 -0
- package/src/zeyos/table.js +272 -0
- package/styles/base.css +124 -0
- package/styles/icons.css +34 -0
- package/styles/tokens/dark.css +84 -0
- package/styles/tokens/density.css +9 -0
- package/styles/tokens/global.css +80 -0
- package/styles/tokens/modules.css +109 -0
- package/styles/tokens/semantic.css +49 -0
- package/styles/zx.css +61 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Named width thresholds, and a way to react to one being crossed.
|
|
4
|
+
*
|
|
5
|
+
* The scale lives here rather than in the token layer because a CSS custom property cannot be
|
|
6
|
+
* used inside a media query, so a `--zx-bp-*` token would be a value no stylesheet could read.
|
|
7
|
+
* Zx's own layout utilities avoid the problem entirely — `.zx-grid` reflows on intrinsic track
|
|
8
|
+
* widths, not on breakpoints — which leaves this module for the cases only script can decide:
|
|
9
|
+
* rendering a Table as cards below a width, collapsing a MasterPanel, choosing a chart density.
|
|
10
|
+
* @module zx/core/breakpoint
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isElement } from './util.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {'xs'|'sm'|'md'|'lg'|'xl'} BreakpointName */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lower bound of each named breakpoint, in pixels. A width below `sm` is `xs`.
|
|
19
|
+
* @type {Readonly<Record<Exclude<BreakpointName, 'xs'>, number>>}
|
|
20
|
+
*/
|
|
21
|
+
export const breakpoints = Object.freeze({ sm: 480, md: 768, lg: 1024, xl: 1280 });
|
|
22
|
+
|
|
23
|
+
/** Names from widest to narrowest, which is the order a lookup has to test them in. */
|
|
24
|
+
const ORDER = /** @type {const} */ (['xl', 'lg', 'md', 'sm']);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The breakpoint a width falls into.
|
|
28
|
+
* @param {number} width Width in pixels.
|
|
29
|
+
* @returns {BreakpointName}
|
|
30
|
+
*/
|
|
31
|
+
export function breakpointOf(width) {
|
|
32
|
+
const value = Number(width);
|
|
33
|
+
if (!Number.isFinite(value)) return 'xs';
|
|
34
|
+
for (const name of ORDER) {
|
|
35
|
+
if (value >= breakpoints[name]) return name;
|
|
36
|
+
}
|
|
37
|
+
return 'xs';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a width has reached a named breakpoint. `matchBreakpoint('md', 900)` is the script
|
|
42
|
+
* equivalent of `@media (min-width: 768px)`.
|
|
43
|
+
* @param {BreakpointName} name Breakpoint to test against.
|
|
44
|
+
* @param {number} width Width in pixels.
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function matchBreakpoint(name, width) {
|
|
48
|
+
if (name === 'xs') return true;
|
|
49
|
+
const min = breakpoints[name];
|
|
50
|
+
if (min === undefined) throw new RangeError(`Unknown breakpoint "${name}"`);
|
|
51
|
+
return Number(width) >= min;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} BreakpointOptions
|
|
56
|
+
* @property {Element|Window} [target=window] What to measure. An Element is watched with a
|
|
57
|
+
* `ResizeObserver`, which is what a component inside a split pane or a modal needs — the
|
|
58
|
+
* viewport width says nothing about the space it actually has.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} BreakpointController
|
|
63
|
+
* @property {() => BreakpointName} current The breakpoint as last measured.
|
|
64
|
+
* @property {() => void} destroy Stops observing.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calls back whenever the observed width crosses into another breakpoint, and once immediately
|
|
69
|
+
* with the current one.
|
|
70
|
+
* @param {(name: BreakpointName, width: number) => void} handler Called on every change.
|
|
71
|
+
* @param {BreakpointOptions} [options={}] What to observe.
|
|
72
|
+
* @returns {BreakpointController}
|
|
73
|
+
*/
|
|
74
|
+
export function onBreakpoint(handler, options = {}) {
|
|
75
|
+
if (typeof handler !== 'function') throw new TypeError('onBreakpoint requires a handler');
|
|
76
|
+
/*
|
|
77
|
+
* An explicitly passed target that is not an element is a mistake worth shouting about. Falling
|
|
78
|
+
* back to the window looks like it works — the callback fires, the values are plausible — while
|
|
79
|
+
* measuring something entirely different from what the caller asked for. Table hit exactly this:
|
|
80
|
+
* it passed `this.el` before the base constructor had assigned it, and quietly watched the
|
|
81
|
+
* viewport instead of itself.
|
|
82
|
+
*/
|
|
83
|
+
if ('target' in options && !isElement(options.target)
|
|
84
|
+
&& /** @type {unknown} */ (options.target) !== /** @type {unknown} */ (globalThis)) {
|
|
85
|
+
throw new TypeError('onBreakpoint target must be an Element or the window');
|
|
86
|
+
}
|
|
87
|
+
const target = options.target ?? globalThis;
|
|
88
|
+
const element = isElement(target) ? /** @type {Element} */ (target) : null;
|
|
89
|
+
/** Null until the first real measurement — see `update()` on why 0 is not a band. */
|
|
90
|
+
let name = /** @type {BreakpointName|null} */ (null);
|
|
91
|
+
let destroyed = false;
|
|
92
|
+
let observer = null;
|
|
93
|
+
|
|
94
|
+
const measure = () => (element
|
|
95
|
+
? element.getBoundingClientRect().width
|
|
96
|
+
: Number(globalThis.innerWidth) || 0);
|
|
97
|
+
|
|
98
|
+
const update = () => {
|
|
99
|
+
if (destroyed) return;
|
|
100
|
+
const width = measure();
|
|
101
|
+
/*
|
|
102
|
+
* A zero width is not the `xs` band — it is no measurement at all, which is what an element
|
|
103
|
+
* that has not been laid out yet reports. Recording it as a band silently disarms the whole
|
|
104
|
+
* watcher: the next real width lands in the same band and never notifies. Table hit exactly
|
|
105
|
+
* this, rendering before it was inserted, measuring 0, and then sitting at 154px inside a
|
|
106
|
+
* narrow pane without ever being told it should stack.
|
|
107
|
+
*/
|
|
108
|
+
if (element && width === 0) return;
|
|
109
|
+
const next = breakpointOf(width);
|
|
110
|
+
if (next === name) return;
|
|
111
|
+
name = next;
|
|
112
|
+
handler(name, width);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
update();
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* Resize notifications are answered on the next frame rather than inside the observer callback.
|
|
119
|
+
* A handler that reacts to a width by changing layout — which is the entire point of this
|
|
120
|
+
* module; Table restyles itself — resizes the very element being observed, and the browser
|
|
121
|
+
* reports that as "ResizeObserver loop completed with undelivered notifications". Deferring
|
|
122
|
+
* breaks the cycle, and coalesces a burst of notifications into one.
|
|
123
|
+
*/
|
|
124
|
+
let frame = 0;
|
|
125
|
+
const schedule = () => {
|
|
126
|
+
if (frame || destroyed) return;
|
|
127
|
+
frame = requestAnimationFrame(() => {
|
|
128
|
+
frame = 0;
|
|
129
|
+
update();
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (element && typeof ResizeObserver === 'function') {
|
|
134
|
+
observer = new ResizeObserver(schedule);
|
|
135
|
+
observer.observe(element);
|
|
136
|
+
} else {
|
|
137
|
+
globalThis.addEventListener?.('resize', update, { passive: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
current: () => name ?? breakpointOf(measure()),
|
|
142
|
+
destroy: () => {
|
|
143
|
+
if (destroyed) return;
|
|
144
|
+
destroyed = true;
|
|
145
|
+
if (frame) cancelAnimationFrame(frame);
|
|
146
|
+
observer?.disconnect();
|
|
147
|
+
globalThis.removeEventListener?.('resize', update);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { h, resolveElement } from './dom.js';
|
|
3
|
+
import { getLanguage, printf, translate } from './i18n.js';
|
|
4
|
+
import { deepMerge, isElement } from './util.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {Record<string, any>} ComponentOptions */
|
|
7
|
+
/** @typedef {(event: Event & {detail?: Record<string, unknown>}) => void} ComponentListener */
|
|
8
|
+
|
|
9
|
+
/** @type {WeakMap<Element, Component>} */
|
|
10
|
+
const registry = new WeakMap();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base class for lifecycle-safe Zx components.
|
|
14
|
+
*
|
|
15
|
+
* Generic over its own options type so a subclass can bind its `XOptions` typedef with
|
|
16
|
+
* `@extends {Component<XOptions>}`. That is what types `new Select(target, { … })` for editors and
|
|
17
|
+
* for TypeScript consumers: a subclass declares no constructor of its own, so it inherits this
|
|
18
|
+
* signature, and without the binding every component would accept any object at all.
|
|
19
|
+
*
|
|
20
|
+
* @template {ComponentOptions} [TOptions=ComponentOptions]
|
|
21
|
+
* @fires Component#event
|
|
22
|
+
*/
|
|
23
|
+
export class Component extends EventTarget {
|
|
24
|
+
/** @type {ComponentOptions} */
|
|
25
|
+
static defaults = {};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* BEM block a subclass wants on its root, without the `zx-` prefix. Empty on the base, which is
|
|
29
|
+
* the same falsy answer the undeclared property gave — declared so the statics are visible to
|
|
30
|
+
* `this.constructor` and to the generated declarations.
|
|
31
|
+
* @type {string}
|
|
32
|
+
*/
|
|
33
|
+
static cssName = '';
|
|
34
|
+
|
|
35
|
+
/** @type {Element} Root element. */
|
|
36
|
+
el;
|
|
37
|
+
|
|
38
|
+
/** @type {Record<string, Element>} Elements captured by `h()` ref properties. */
|
|
39
|
+
refs;
|
|
40
|
+
|
|
41
|
+
/** @type {Readonly<TOptions>} Merged component options. */
|
|
42
|
+
options;
|
|
43
|
+
|
|
44
|
+
#abort = new AbortController();
|
|
45
|
+
#created = false;
|
|
46
|
+
#destroyed = false;
|
|
47
|
+
#addedRootClass = false;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a component around an existing target, or asks `render()` to create its root.
|
|
51
|
+
* @param {Element|string|null} target Existing element, selector, or null.
|
|
52
|
+
* @param {TOptions} [options={}] Component options.
|
|
53
|
+
*/
|
|
54
|
+
constructor(target, options = /** @type {TOptions} */ ({})) {
|
|
55
|
+
super();
|
|
56
|
+
/** `this.constructor` is typed `Function`, which hides the statics this class declares. */
|
|
57
|
+
const self = /** @type {typeof Component} */ (this.constructor);
|
|
58
|
+
this.#created = target === null;
|
|
59
|
+
const resolved = resolveElement(target);
|
|
60
|
+
if (target !== null && !resolved) throw new TypeError('Component target could not be resolved');
|
|
61
|
+
this.el = /** @type {Element} */ (resolved);
|
|
62
|
+
this.refs = {};
|
|
63
|
+
|
|
64
|
+
const merged = /** @type {TOptions} */ (mergeOptions(self, options));
|
|
65
|
+
const eventOptions = [];
|
|
66
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
67
|
+
if (/^on[a-z]/.test(key) && typeof value === 'function') {
|
|
68
|
+
eventOptions.push([key.slice(2), value]);
|
|
69
|
+
delete merged[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (merged.msg && typeof merged.msg === 'object') Object.freeze(merged.msg);
|
|
73
|
+
this.options = Object.freeze(merged);
|
|
74
|
+
for (const [type, listener] of eventOptions) this.on(type, listener);
|
|
75
|
+
|
|
76
|
+
let rendered;
|
|
77
|
+
try {
|
|
78
|
+
rendered = h.scope(this.refs, () => this.render());
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.#abort.abort();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
if (this.#created) {
|
|
84
|
+
const root = isElement(rendered) ? rendered : this.el;
|
|
85
|
+
if (!isElement(root)) {
|
|
86
|
+
this.#abort.abort();
|
|
87
|
+
throw new TypeError('render() must return an Element (or assign this.el) when target is null');
|
|
88
|
+
}
|
|
89
|
+
this.el = root;
|
|
90
|
+
}
|
|
91
|
+
if (!isElement(this.el)) {
|
|
92
|
+
this.#abort.abort();
|
|
93
|
+
throw new TypeError('Component requires a root Element');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cssName = self.cssName;
|
|
97
|
+
if (typeof cssName === 'string' && cssName) {
|
|
98
|
+
const className = `zx-${cssName}`;
|
|
99
|
+
this.#addedRootClass = !this.el.classList.contains(className);
|
|
100
|
+
this.el.classList.add(className);
|
|
101
|
+
}
|
|
102
|
+
registry.set(this.el, this);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Renders or enhances the component root. Subclasses creating a root must return it.
|
|
107
|
+
* @returns {Element|null}
|
|
108
|
+
*/
|
|
109
|
+
render() {
|
|
110
|
+
return this.el ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Subscribes to a component-level event for this component's lifetime.
|
|
115
|
+
* @param {string} type Lowercase event name.
|
|
116
|
+
* @param {ComponentListener|EventListenerObject} fn Listener.
|
|
117
|
+
* @returns {this}
|
|
118
|
+
*/
|
|
119
|
+
on(type, fn) {
|
|
120
|
+
this.addEventListener(type, fn, { signal: this.#abort.signal });
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Removes a component-level event listener.
|
|
126
|
+
* @param {string} type Event name.
|
|
127
|
+
* @param {ComponentListener|EventListenerObject} fn Listener.
|
|
128
|
+
* @returns {this}
|
|
129
|
+
*/
|
|
130
|
+
off(type, fn) {
|
|
131
|
+
this.removeEventListener(type, fn);
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Subscribes to one component-level event occurrence.
|
|
137
|
+
* @param {string} type Event name.
|
|
138
|
+
* @param {ComponentListener|EventListenerObject} fn Listener.
|
|
139
|
+
* @returns {this}
|
|
140
|
+
*/
|
|
141
|
+
once(type, fn) {
|
|
142
|
+
this.addEventListener(type, fn, { once: true, signal: this.#abort.signal });
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* MooTools-compatible alias of `on()`.
|
|
148
|
+
* @param {string} type Event name.
|
|
149
|
+
* @param {ComponentListener|EventListenerObject} fn Listener.
|
|
150
|
+
* @returns {this}
|
|
151
|
+
*/
|
|
152
|
+
addEvent(type, fn) {
|
|
153
|
+
return this.on(type, fn);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Dispatches a component event and a bubbling, composed `zx-*` DOM event.
|
|
158
|
+
* @param {string} type Lowercase event name.
|
|
159
|
+
* @param {Record<string, unknown>} [detail={}] Single event detail object.
|
|
160
|
+
* @returns {Event & {detail: Record<string, unknown>}}
|
|
161
|
+
* @fires Component#event
|
|
162
|
+
*/
|
|
163
|
+
emit(type, detail = {}) {
|
|
164
|
+
if (detail === null || typeof detail !== 'object' || Array.isArray(detail)) {
|
|
165
|
+
throw new TypeError('Event detail must be an object');
|
|
166
|
+
}
|
|
167
|
+
const componentEvent = createCustomEvent(type, detail, { cancelable: true });
|
|
168
|
+
this.dispatchEvent(componentEvent);
|
|
169
|
+
if (this.el) {
|
|
170
|
+
this.el.dispatchEvent(createCustomEvent(`zx-${type}`, detail, {
|
|
171
|
+
bubbles: true,
|
|
172
|
+
composed: true,
|
|
173
|
+
cancelable: true
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
return componentEvent;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Adds an automatically cleaned-up DOM event listener.
|
|
181
|
+
* @param {EventTarget} element DOM event target.
|
|
182
|
+
* @param {string} type Event name.
|
|
183
|
+
* @param {EventListenerOrEventListenerObject} fn Listener.
|
|
184
|
+
* @param {boolean|AddEventListenerOptions} [opts={}] Listener options.
|
|
185
|
+
* @returns {this}
|
|
186
|
+
*/
|
|
187
|
+
listen(element, type, fn, opts = {}) {
|
|
188
|
+
const options = typeof opts === 'boolean' ? { capture: opts } : { ...opts };
|
|
189
|
+
element.addEventListener(type, fn, { ...options, signal: this.#abort.signal });
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Returns the component's root element.
|
|
195
|
+
* @returns {Element}
|
|
196
|
+
*/
|
|
197
|
+
toElement() {
|
|
198
|
+
return this.el;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolves and interpolates a component-local or global translation.
|
|
203
|
+
* @param {string} key Message key.
|
|
204
|
+
* @param {...unknown} args Positional interpolation values.
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
msg(key, ...args) {
|
|
208
|
+
const messages = this.options.msg;
|
|
209
|
+
const lang = this.options.lang ?? this.options.language ?? getLanguage();
|
|
210
|
+
const localized = messages?.[lang]?.[key];
|
|
211
|
+
if (localized != null) return printf(String(localized), args);
|
|
212
|
+
const direct = messages?.[key];
|
|
213
|
+
if (direct != null) return printf(String(direct), args);
|
|
214
|
+
return translate(key, args);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Aborts listeners, unregisters the component, and removes roots created by the component.
|
|
219
|
+
* Safe to call repeatedly.
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
destroy() {
|
|
223
|
+
if (this.#destroyed) return;
|
|
224
|
+
this.#destroyed = true;
|
|
225
|
+
this.#abort.abort();
|
|
226
|
+
if (registry.get(this.el) === this) registry.delete(this.el);
|
|
227
|
+
const cssName = /** @type {typeof Component} */ (this.constructor).cssName;
|
|
228
|
+
if (this.#addedRootClass && typeof cssName === 'string' && this.el) {
|
|
229
|
+
this.el.classList.remove(`zx-${cssName}`);
|
|
230
|
+
}
|
|
231
|
+
if (this.#created && this.el) this.el.remove();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Retrieves the component currently registered for an element.
|
|
236
|
+
* @param {Element} element Root element.
|
|
237
|
+
* @returns {Component|null}
|
|
238
|
+
*/
|
|
239
|
+
static from(element) {
|
|
240
|
+
return registry.get(element) ?? null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generic component event emitted under the name passed to `emit()`.
|
|
246
|
+
* @event Component#event
|
|
247
|
+
* @type {CustomEvent<Record<string, unknown>>}
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
/** @param {typeof Component} constructor @param {ComponentOptions} options @returns {ComponentOptions} */
|
|
251
|
+
function mergeOptions(constructor, options) {
|
|
252
|
+
const chain = [];
|
|
253
|
+
let current = constructor;
|
|
254
|
+
while (current && current !== Function.prototype) {
|
|
255
|
+
if (Object.prototype.hasOwnProperty.call(current, 'defaults')) chain.unshift(current.defaults);
|
|
256
|
+
current = Object.getPrototypeOf(current);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = {};
|
|
260
|
+
let messages = {};
|
|
261
|
+
let hasMessages = false;
|
|
262
|
+
for (const defaults of chain) {
|
|
263
|
+
if (!defaults || typeof defaults !== 'object') continue;
|
|
264
|
+
Object.assign(result, defaults);
|
|
265
|
+
if (defaults.msg && typeof defaults.msg === 'object') {
|
|
266
|
+
hasMessages = true;
|
|
267
|
+
messages = deepMerge(messages, defaults.msg);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
Object.assign(result, options && typeof options === 'object' ? options : {});
|
|
271
|
+
if (options?.msg && typeof options.msg === 'object') {
|
|
272
|
+
hasMessages = true;
|
|
273
|
+
messages = deepMerge(messages, options.msg);
|
|
274
|
+
}
|
|
275
|
+
if (hasMessages) result.msg = messages;
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {string} type
|
|
281
|
+
* @param {Record<string, unknown>} detail
|
|
282
|
+
* @param {CustomEventInit<Record<string, unknown>>} init
|
|
283
|
+
* @returns {Event & {detail: Record<string, unknown>}}
|
|
284
|
+
*/
|
|
285
|
+
function createCustomEvent(type, detail, init) {
|
|
286
|
+
if (typeof CustomEvent === 'function') {
|
|
287
|
+
return new CustomEvent(type, { ...init, detail });
|
|
288
|
+
}
|
|
289
|
+
const event = new Event(type, init);
|
|
290
|
+
Object.defineProperty(event, 'detail', { configurable: true, enumerable: true, value: detail });
|
|
291
|
+
return /** @type {Event & {detail: Record<string, unknown>}} */ (event);
|
|
292
|
+
}
|
package/src/core/date.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { getLanguage, translate } from './i18n.js';
|
|
3
|
+
|
|
4
|
+
const MONTHS = [
|
|
5
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
6
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
7
|
+
];
|
|
8
|
+
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
9
|
+
const TOKEN_PATTERN = /%[demYyHMSaAbBs]/g;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Formats a local Date using strftime-style kernel tokens.
|
|
13
|
+
* @param {Date} date Date to format.
|
|
14
|
+
* @param {string} fmt Format containing `%d %e %m %Y %y %H %M %S %a %A %b %B %s` tokens.
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function formatDate(date, fmt) {
|
|
18
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
|
|
19
|
+
return String(fmt).replace(TOKEN_PATTERN, (token) => {
|
|
20
|
+
switch (token) {
|
|
21
|
+
case '%d': return pad(date.getDate());
|
|
22
|
+
case '%e': return String(date.getDate());
|
|
23
|
+
case '%m': return pad(date.getMonth() + 1);
|
|
24
|
+
case '%Y': return String(date.getFullYear()).padStart(4, '0');
|
|
25
|
+
case '%y': return pad(date.getFullYear() % 100);
|
|
26
|
+
case '%H': return pad(date.getHours());
|
|
27
|
+
case '%M': return pad(date.getMinutes());
|
|
28
|
+
case '%S': return pad(date.getSeconds());
|
|
29
|
+
case '%a': return localizedShortName('weekday', date.getDay(), WEEKDAYS[date.getDay()]);
|
|
30
|
+
case '%A': return localizedName('weekday', date.getDay(), WEEKDAYS[date.getDay()]);
|
|
31
|
+
case '%b': return localizedShortName('month', date.getMonth(), MONTHS[date.getMonth()]);
|
|
32
|
+
case '%B': return localizedName('month', date.getMonth(), MONTHS[date.getMonth()]);
|
|
33
|
+
case '%s': return String(Math.floor(date.getTime() / 1000));
|
|
34
|
+
default: return token;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parses numeric date tokens from a string, returning null for mismatches or invalid dates.
|
|
41
|
+
* Unspecified fields default to 1970-01-01 00:00:00 local time.
|
|
42
|
+
* @param {string} str Date text.
|
|
43
|
+
* @param {string} fmt Matching numeric format.
|
|
44
|
+
* @returns {Date|null}
|
|
45
|
+
*/
|
|
46
|
+
export function parseDate(str, fmt) {
|
|
47
|
+
const fields = [];
|
|
48
|
+
let pattern = '^';
|
|
49
|
+
let cursor = 0;
|
|
50
|
+
|
|
51
|
+
for (const match of String(fmt).matchAll(TOKEN_PATTERN)) {
|
|
52
|
+
pattern += escapeRegExp(String(fmt).slice(cursor, match.index));
|
|
53
|
+
const token = match[0];
|
|
54
|
+
const tokenPattern = numericPattern(token);
|
|
55
|
+
if (tokenPattern === null) return null;
|
|
56
|
+
pattern += `(${tokenPattern})`;
|
|
57
|
+
fields.push(token);
|
|
58
|
+
cursor = match.index + token.length;
|
|
59
|
+
}
|
|
60
|
+
pattern += escapeRegExp(String(fmt).slice(cursor)) + '$';
|
|
61
|
+
|
|
62
|
+
const matched = new RegExp(pattern).exec(String(str));
|
|
63
|
+
if (!matched) return null;
|
|
64
|
+
const values = Object.create(null);
|
|
65
|
+
fields.forEach((field, index) => {
|
|
66
|
+
values[field] = Number(matched[index + 1]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (values['%s'] !== undefined) {
|
|
70
|
+
const timestamp = values['%s'] * 1000;
|
|
71
|
+
const parsed = new Date(timestamp);
|
|
72
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const year = values['%Y'] ?? yearFromTwoDigits(values['%y']) ?? 1970;
|
|
76
|
+
const month = (values['%m'] ?? 1) - 1;
|
|
77
|
+
const day = values['%d'] ?? values['%e'] ?? 1;
|
|
78
|
+
const hour = values['%H'] ?? 0;
|
|
79
|
+
const minute = values['%M'] ?? 0;
|
|
80
|
+
const second = values['%S'] ?? 0;
|
|
81
|
+
const parsed = new Date(0);
|
|
82
|
+
parsed.setHours(0, 0, 0, 0);
|
|
83
|
+
parsed.setFullYear(year, month, day);
|
|
84
|
+
parsed.setHours(hour, minute, second, 0);
|
|
85
|
+
|
|
86
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
87
|
+
if (values['%Y'] !== undefined && parsed.getFullYear() !== values['%Y']) return null;
|
|
88
|
+
if (values['%y'] !== undefined && parsed.getFullYear() % 100 !== values['%y']) return null;
|
|
89
|
+
if (values['%m'] !== undefined && parsed.getMonth() !== month) return null;
|
|
90
|
+
if (values['%d'] !== undefined && parsed.getDate() !== day) return null;
|
|
91
|
+
if (values['%e'] !== undefined && parsed.getDate() !== day) return null;
|
|
92
|
+
if (values['%H'] !== undefined && parsed.getHours() !== hour) return null;
|
|
93
|
+
if (values['%M'] !== undefined && parsed.getMinutes() !== minute) return null;
|
|
94
|
+
if (values['%S'] !== undefined && parsed.getSeconds() !== second) return null;
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clamps a date to optional inclusive bounds and returns a new Date.
|
|
100
|
+
* @param {Date} date Date to clamp.
|
|
101
|
+
* @param {Date|null|undefined} min Minimum date.
|
|
102
|
+
* @param {Date|null|undefined} max Maximum date.
|
|
103
|
+
* @returns {Date}
|
|
104
|
+
*/
|
|
105
|
+
export function clampDate(date, min, max) {
|
|
106
|
+
const value = date.getTime();
|
|
107
|
+
const minimum = min instanceof Date ? min.getTime() : -Infinity;
|
|
108
|
+
const maximum = max instanceof Date ? max.getTime() : Infinity;
|
|
109
|
+
return new Date(Math.min(maximum, Math.max(minimum, value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reports whether two dates share the same local calendar day.
|
|
114
|
+
* @param {Date} a First date.
|
|
115
|
+
* @param {Date} b Second date.
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
export function isSameDay(a, b) {
|
|
119
|
+
return a.getFullYear() === b.getFullYear() &&
|
|
120
|
+
a.getMonth() === b.getMonth() &&
|
|
121
|
+
a.getDate() === b.getDate();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Adds local calendar days without mutating the input.
|
|
126
|
+
* @param {Date} date Base date.
|
|
127
|
+
* @param {number} n Number of days.
|
|
128
|
+
* @returns {Date}
|
|
129
|
+
*/
|
|
130
|
+
export function addDays(date, n) {
|
|
131
|
+
const result = new Date(date.getTime());
|
|
132
|
+
result.setDate(result.getDate() + n);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Adds local calendar months without mutating the input, clamping to the target month's end.
|
|
138
|
+
* @param {Date} date Base date.
|
|
139
|
+
* @param {number} n Number of months.
|
|
140
|
+
* @returns {Date}
|
|
141
|
+
*/
|
|
142
|
+
export function addMonths(date, n) {
|
|
143
|
+
const result = new Date(date.getTime());
|
|
144
|
+
const day = result.getDate();
|
|
145
|
+
result.setDate(1);
|
|
146
|
+
result.setMonth(result.getMonth() + n);
|
|
147
|
+
const lastDay = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate();
|
|
148
|
+
result.setDate(Math.min(day, lastDay));
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns the locale's first weekday (`0` Sunday through `6` Saturday), Monday by default.
|
|
154
|
+
* @param {string} [lang=getLanguage()] BCP 47 language tag.
|
|
155
|
+
* @returns {number}
|
|
156
|
+
*/
|
|
157
|
+
export function getWeekStart(lang = getLanguage()) {
|
|
158
|
+
if (!lang) return 1;
|
|
159
|
+
if (lang.toLowerCase() === 'en') return 1;
|
|
160
|
+
try {
|
|
161
|
+
/** `getWeekInfo()` and `weekInfo` are newer Intl surface than the bundled lib types know. */
|
|
162
|
+
const locale = /** @type {Intl.Locale & {getWeekInfo?: () => {firstDay?: number}, weekInfo?: {firstDay?: number}}} */
|
|
163
|
+
(new Intl.Locale(lang));
|
|
164
|
+
const weekInfo = locale.getWeekInfo?.() ?? locale.weekInfo;
|
|
165
|
+
if (weekInfo?.firstDay) return weekInfo.firstDay % 7;
|
|
166
|
+
} catch {
|
|
167
|
+
// Invalid or unsupported locale data uses the APG-friendly Monday default.
|
|
168
|
+
}
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {number} value @returns {string} */
|
|
173
|
+
function pad(value) {
|
|
174
|
+
return String(value).padStart(2, '0');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @param {'month'|'weekday'} type @param {number} index @param {string} fallback @returns {string} */
|
|
178
|
+
function localizedName(type, index, fallback) {
|
|
179
|
+
const key = `date.${type}.${index}`;
|
|
180
|
+
const value = translate(key);
|
|
181
|
+
return value === key ? fallback : value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @param {'month'|'weekday'} type @param {number} index @param {string} fallback @returns {string} */
|
|
185
|
+
function localizedShortName(type, index, fallback) {
|
|
186
|
+
const shortKey = `date.${type}.short.${index}`;
|
|
187
|
+
const shortValue = translate(shortKey);
|
|
188
|
+
if (shortValue !== shortKey) return shortValue;
|
|
189
|
+
return localizedName(type, index, fallback).slice(0, 3);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @param {string} token @returns {string|null} */
|
|
193
|
+
function numericPattern(token) {
|
|
194
|
+
return {
|
|
195
|
+
'%d': '\\d{1,2}',
|
|
196
|
+
'%e': '\\d{1,2}',
|
|
197
|
+
'%m': '\\d{1,2}',
|
|
198
|
+
'%Y': '\\d{4}',
|
|
199
|
+
'%y': '\\d{2}',
|
|
200
|
+
'%H': '\\d{1,2}',
|
|
201
|
+
'%M': '\\d{1,2}',
|
|
202
|
+
'%S': '\\d{1,2}',
|
|
203
|
+
'%s': '-?\\d+'
|
|
204
|
+
}[token] ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @param {number|undefined} year @returns {number|undefined} */
|
|
208
|
+
function yearFromTwoDigits(year) {
|
|
209
|
+
if (year === undefined) return undefined;
|
|
210
|
+
return year >= 69 ? 1900 + year : 2000 + year;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {string} value @returns {string} */
|
|
214
|
+
function escapeRegExp(value) {
|
|
215
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
216
|
+
}
|