@fusion.dev/helix-core 0.147.0 → 0.149.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/custom-elements.json +16160 -12084
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
- package/dist/controllers/children-controller/children-controller.d.ts +28 -0
- package/dist/controllers/children-controller/children-controller.js +18 -0
- package/dist/controllers/countdown-controller/countdown-controller.d.ts +15 -1
- package/dist/controllers/countdown-controller/countdown-controller.js +27 -2
- package/dist/controllers/floating-controller/floating-controller.d.ts +12 -1
- package/dist/controllers/floating-controller/floating-controller.js +2 -1
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +25 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +8 -0
- package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
- package/dist/controllers/machine-controller/machine-controller.js +78 -0
- package/dist/controllers/machine-controller/machine.d.ts +93 -0
- package/dist/controllers/machine-controller/machine.js +20 -0
- package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +10 -0
- package/dist/controllers/open-phase-controller/open-phase-controller.js +7 -4
- package/dist/controllers/option-window-controller/option-window-controller.d.ts +96 -0
- package/dist/controllers/option-window-controller/option-window-controller.js +81 -0
- package/dist/controllers/popover-controller/popover-controller.js +1 -1
- package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
- package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
- package/dist/controllers/source-controller/source-controller.js +1 -1
- package/dist/controllers/virtualize-controller/heights.js +1 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
- package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
- package/dist/events/before-reorder.d.ts +14 -0
- package/dist/events/before-reorder.js +11 -0
- package/dist/events/details.d.ts +28 -0
- package/dist/events/events.d.ts +4 -2
- package/dist/events/events.js +3 -1
- package/dist/events/names.js +1 -1
- package/dist/events/reorder.d.ts +18 -12
- package/dist/events/reordered.d.ts +4 -4
- package/dist/helix-element/helix-element.d.ts +14 -5
- package/dist/helix-element/helix-element.js +6 -3
- package/dist/helix-element/react-audit-off.d.ts +11 -0
- package/dist/helix-element/react-audit-off.js +1 -0
- package/dist/helix-element/react-audit.d.ts +20 -0
- package/dist/helix-element/react-audit.js +56 -0
- package/dist/helix-element/react-bridge.d.ts +2 -22
- package/dist/helix-element/react-bridge.js +1 -56
- package/dist/index.d.ts +34 -14
- package/dist/index.js +32 -12
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +6 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
- package/dist/mixins/mixin-field/mixin-field.js +1 -1
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
- package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
- package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
- package/dist/mixins/mixin-viewport/mixin-viewport.js +23 -41
- package/dist/utils/assign-slots/assign-slots.d.ts +18 -0
- package/dist/utils/assign-slots/assign-slots.js +14 -0
- package/dist/utils/autofill-select/autofill-select.d.ts +40 -0
- package/dist/utils/autofill-select/autofill-select.js +14 -0
- package/dist/utils/calendar/calendar.d.ts +27 -0
- package/dist/utils/calendar/calendar.js +17 -0
- package/dist/utils/center-in-view/center-in-view.d.ts +18 -0
- package/dist/utils/center-in-view/center-in-view.js +14 -0
- package/dist/utils/chord/key-names.d.ts +30 -0
- package/dist/utils/chord/key-names.js +52 -0
- package/dist/utils/column-step/column-step.d.ts +13 -0
- package/dist/utils/column-step/column-step.js +9 -0
- package/dist/utils/count-label/count-label.d.ts +7 -4
- package/dist/utils/count-label/count-label.js +2 -5
- package/dist/utils/data-options/data-options.d.ts +60 -0
- package/dist/utils/data-options/data-options.js +46 -0
- package/dist/utils/date-segments/date-segments.d.ts +2 -1
- package/dist/utils/date-segments/date-segments.js +10 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +7 -2
- package/dist/utils/draw-tooltip/draw-tooltip.js +1 -1
- package/dist/utils/draw-tooltip/spoken-reading.d.ts +3 -2
- package/dist/utils/draw-tooltip/spoken-reading.js +3 -2
- package/dist/utils/export-parts/export-parts.d.ts +14 -0
- package/dist/utils/export-parts/export-parts.js +1 -0
- package/dist/utils/fill-template/fill-template.d.ts +9 -0
- package/dist/utils/fill-template/fill-template.js +3 -0
- package/dist/utils/filter-marks/filter-marks.d.ts +36 -0
- package/dist/utils/filter-marks/filter-marks.js +41 -0
- package/dist/utils/focus-target/focus-target.d.ts +12 -0
- package/dist/utils/focus-target/focus-target.js +9 -0
- package/dist/utils/focusable-elements/focusable-elements.d.ts +17 -0
- package/dist/utils/focusable-elements/focusable-elements.js +8 -0
- package/dist/utils/grid-move/grid-move.d.ts +56 -0
- package/dist/utils/grid-move/grid-move.js +28 -0
- package/dist/utils/locale-of/locale-of.d.ts +5 -0
- package/dist/utils/locale-of/locale-of.js +8 -2
- package/dist/utils/number-format/number-format.d.ts +7 -0
- package/dist/utils/number-format/number-format.js +1 -0
- package/dist/utils/reorder/drag-preview.d.ts +46 -0
- package/dist/utils/reorder/drag-preview.js +58 -0
- package/dist/utils/reorder/reorder.d.ts +178 -0
- package/dist/utils/reorder/reorder.js +112 -0
- package/dist/utils/report-runtime/report-runtime.d.ts +14 -5
- package/dist/utils/report-values/report-values.d.ts +16 -4
- package/dist/utils/report-values/report-values.js +2 -2
- package/dist/utils/request-change/request-change.d.ts +11 -0
- package/dist/utils/request-change/request-change.js +10 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +17 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +1 -0
- package/dist/utils/slot-filled/slot-filled.d.ts +8 -0
- package/dist/utils/slot-filled/slot-filled.js +1 -0
- package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
- package/dist/utils/slotted-words/slotted-words.js +3 -1
- package/dist/utils/toolbar-around/toolbar-around.d.ts +18 -0
- package/dist/utils/toolbar-around/toolbar-around.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/words/index.d.ts +3 -0
- package/dist/words/index.js +3 -0
- package/dist/words/message.d.ts +47 -0
- package/dist/words/message.js +152 -0
- package/dist/words/registry.d.ts +89 -0
- package/dist/words/registry.js +101 -0
- package/dist/words/words-locale.d.ts +34 -0
- package/dist/words/words-locale.js +30 -0
- package/dist/words/words.d.ts +52 -0
- package/dist/words/words.js +100 -0
- package/package.json +40 -25
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { formatNumber } from '../utils/number-format/number-format.js';
|
|
2
|
+
const parsed = new Map();
|
|
3
|
+
const WORD = /\s*([^\s,{}]+)\s*/y;
|
|
4
|
+
const SPACE = /\s*/y;
|
|
5
|
+
const parse = (source) => {
|
|
6
|
+
let at = 0;
|
|
7
|
+
const fail = () => {
|
|
8
|
+
throw at;
|
|
9
|
+
};
|
|
10
|
+
const match = (pattern) => {
|
|
11
|
+
pattern.lastIndex = at;
|
|
12
|
+
const found = pattern.exec(source) ?? fail();
|
|
13
|
+
at = pattern.lastIndex;
|
|
14
|
+
return found[1];
|
|
15
|
+
};
|
|
16
|
+
const take = (char) => {
|
|
17
|
+
if (source[at++] !== char) {
|
|
18
|
+
fail();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const text = (plural) => {
|
|
22
|
+
const parts = [];
|
|
23
|
+
let written = '';
|
|
24
|
+
const flush = () => {
|
|
25
|
+
if (written) {
|
|
26
|
+
parts.push(written);
|
|
27
|
+
}
|
|
28
|
+
written = '';
|
|
29
|
+
};
|
|
30
|
+
for (let char = source[at]; char && char !== '}'; char = source[at]) {
|
|
31
|
+
const next = source[++at];
|
|
32
|
+
if (char === "'" && next === "'") {
|
|
33
|
+
written += char;
|
|
34
|
+
at++;
|
|
35
|
+
}
|
|
36
|
+
else if (char === "'" && (next === '{' || next === '}' || (plural && next === '#'))) {
|
|
37
|
+
while (at < source.length) {
|
|
38
|
+
const quoted = source[at++];
|
|
39
|
+
if (quoted === "'") {
|
|
40
|
+
if (source[at] !== "'") {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
at++;
|
|
44
|
+
}
|
|
45
|
+
written += quoted;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (char === '#' && plural) {
|
|
49
|
+
flush();
|
|
50
|
+
parts.push(0);
|
|
51
|
+
}
|
|
52
|
+
else if (char === '{') {
|
|
53
|
+
flush();
|
|
54
|
+
parts.push(argument(plural));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
written += char;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
flush();
|
|
61
|
+
return parts;
|
|
62
|
+
};
|
|
63
|
+
const argument = (plural) => {
|
|
64
|
+
const found = { n: match(WORD) };
|
|
65
|
+
if (source[at] !== '}') {
|
|
66
|
+
take(',');
|
|
67
|
+
const type = match(WORD);
|
|
68
|
+
found.t = type;
|
|
69
|
+
if (source[at] !== '}') {
|
|
70
|
+
take(',');
|
|
71
|
+
if (type !== 'plural' && type !== 'selectordinal' && type !== 'select') {
|
|
72
|
+
const end = source.indexOf('}', at);
|
|
73
|
+
found.s = source.slice(at, end < 0 ? fail() : end).trim();
|
|
74
|
+
at = end;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const branches = {};
|
|
78
|
+
found.b = branches;
|
|
79
|
+
while (source[at] !== '}') {
|
|
80
|
+
let key = match(WORD);
|
|
81
|
+
if (key.startsWith('offset:')) {
|
|
82
|
+
found.o = Number(key.slice(7) || match(WORD));
|
|
83
|
+
key = match(WORD);
|
|
84
|
+
}
|
|
85
|
+
take('{');
|
|
86
|
+
branches[key] = text(type !== 'select' || plural);
|
|
87
|
+
take('}');
|
|
88
|
+
match(SPACE);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
at++;
|
|
94
|
+
return found;
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const parts = text(false);
|
|
98
|
+
return at < source.length ? fail() : parts;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [source];
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const rules = new Map();
|
|
105
|
+
const STYLES = {
|
|
106
|
+
integer: { maximumFractionDigits: 0 },
|
|
107
|
+
percent: { style: 'percent' },
|
|
108
|
+
};
|
|
109
|
+
export const formatMessage = (message, values, locale, options = {}) => {
|
|
110
|
+
let parts = parsed.get(message);
|
|
111
|
+
if (!parts) {
|
|
112
|
+
parts = parse(message);
|
|
113
|
+
parsed.set(message, parts);
|
|
114
|
+
}
|
|
115
|
+
const plurals = options.plurals ?? locale;
|
|
116
|
+
const isolated = (text) => (options.isolate ? `\u2068${text}\u2069` : text);
|
|
117
|
+
const run = (from, count) => from
|
|
118
|
+
.map((part) => {
|
|
119
|
+
if (typeof part === 'string') {
|
|
120
|
+
return part;
|
|
121
|
+
}
|
|
122
|
+
if (part === 0) {
|
|
123
|
+
return formatNumber(count, locale);
|
|
124
|
+
}
|
|
125
|
+
const { b: branches, n: name, t: type } = part;
|
|
126
|
+
const value = values[name];
|
|
127
|
+
if (value === undefined) {
|
|
128
|
+
return `{${name}}`;
|
|
129
|
+
}
|
|
130
|
+
if (type === 'number' || (!type && typeof value === 'number')) {
|
|
131
|
+
return isolated(formatNumber(+value, locale, STYLES[part.s]));
|
|
132
|
+
}
|
|
133
|
+
if (!branches) {
|
|
134
|
+
return isolated(String(value));
|
|
135
|
+
}
|
|
136
|
+
if (type === 'select') {
|
|
137
|
+
return run(branches[value] ?? branches.other ?? [], count);
|
|
138
|
+
}
|
|
139
|
+
const shifted = +value - (part.o ?? 0);
|
|
140
|
+
const key = `${plurals}|${type}`;
|
|
141
|
+
let rule = rules.get(key);
|
|
142
|
+
if (!rule) {
|
|
143
|
+
rule = new Intl.PluralRules(plurals || undefined, {
|
|
144
|
+
type: type === 'plural' ? 'cardinal' : 'ordinal',
|
|
145
|
+
});
|
|
146
|
+
rules.set(key, rule);
|
|
147
|
+
}
|
|
148
|
+
return run(branches[`=${value}`] ?? branches[rule.select(shifted)] ?? branches.other ?? [], shifted);
|
|
149
|
+
})
|
|
150
|
+
.join('');
|
|
151
|
+
return run(parts, 0);
|
|
152
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Message, MessageTable } from './message.js';
|
|
2
|
+
/**
|
|
3
|
+
* The words of every family, keyed by the family's styled tag, each with its
|
|
4
|
+
* word properties.
|
|
5
|
+
*
|
|
6
|
+
* Empty here. The styled and headless packages fill it from their manifests,
|
|
7
|
+
* in `types/words.d.ts`, and an element of your own adds its tag the same way:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* declare module '@fusion.dev/helix-core' {
|
|
11
|
+
* interface HelixWords {
|
|
12
|
+
* 'my-uploader': { dropLabel: Message }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface HelixWords {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Words for one locale, by tag and property. A tag that `HelixWords` names is
|
|
21
|
+
* checked against it, and any other tag takes any words.
|
|
22
|
+
*/
|
|
23
|
+
export type Words = {
|
|
24
|
+
readonly [T in keyof HelixWords]?: Readonly<Partial<HelixWords[T]>>;
|
|
25
|
+
} & {
|
|
26
|
+
readonly [tag: string]: Readonly<Record<string, Message | MessageTable>> | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Adds words for a locale, merged over the words already registered for it.
|
|
30
|
+
* Elements on the page update once, in a microtask.
|
|
31
|
+
*
|
|
32
|
+
* `locale` is a BCP 47 tag. Name a pack by its full tag where a language has
|
|
33
|
+
* regional forms, such as `pt-BR`, `pt-PT` and `es-419`. A word table, such
|
|
34
|
+
* as Color Picker's `colorWords`, merges entry by entry.
|
|
35
|
+
*
|
|
36
|
+
* ```js
|
|
37
|
+
* registerWords('de', { 'helix-combobox': { emptyText: 'Keine Treffer' } })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare const registerWords: (locale: string, ...words: Words[]) => void;
|
|
41
|
+
/** Removes a locale's words, or every locale's where no locale is given. */
|
|
42
|
+
export declare const clearWords: (locale?: string) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Sets the locale an element speaks on a server, and in its first render
|
|
45
|
+
* while it hydrates, where neither its `locale` nor its own `lang` says.
|
|
46
|
+
* Defaults to `en`.
|
|
47
|
+
*
|
|
48
|
+
* **Process-wide**, so it suits a server that renders one language. A server
|
|
49
|
+
* that renders several sets `lang` or `locale` on the elements.
|
|
50
|
+
*/
|
|
51
|
+
export declare const setDefaultWordsLocale: (locale: string) => void;
|
|
52
|
+
/** The locale set with `setDefaultWordsLocale`. */
|
|
53
|
+
export declare const defaultWordsLocale: () => string;
|
|
54
|
+
/**
|
|
55
|
+
* Asks every element that reads words to update, after an ancestor's `lang`
|
|
56
|
+
* changes. A change to the document's own `lang` is watched for you.
|
|
57
|
+
*/
|
|
58
|
+
export declare const refreshWords: () => void;
|
|
59
|
+
/**
|
|
60
|
+
* The locales a word is looked for in, most specific first, ending in `en`.
|
|
61
|
+
*
|
|
62
|
+
* **The script comes before the bare language**, so Hong Kong (`zh-HK`)
|
|
63
|
+
* reaches a Traditional Chinese pack (`zh-Hant`) before a `zh` pack, which is
|
|
64
|
+
* usually Simplified. Unicode extensions are dropped, and a tag `Intl` refuses
|
|
65
|
+
* falls straight to `en`.
|
|
66
|
+
*
|
|
67
|
+
* `de-CH` tries `de-ch`, `de-latn-ch`, `de-latn`, `de`, then `en`.
|
|
68
|
+
*/
|
|
69
|
+
export declare const localeChain: (locale: string) => string[];
|
|
70
|
+
/** Whether any words are registered, for a read that can skip the lookup. */
|
|
71
|
+
export declare const wordsRegistered: () => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* A registered word for a tag's property in a locale, with the locale it was
|
|
74
|
+
* found in, or `undefined`.
|
|
75
|
+
*
|
|
76
|
+
* **Per key, along the chain.** A `de-CH` pack that holds only the Swiss
|
|
77
|
+
* spellings falls through to `de` for every other word. A word table merges
|
|
78
|
+
* along the chain, the more specific entry winning.
|
|
79
|
+
*/
|
|
80
|
+
export declare const lookupWord: (locale: string, tag: string, name: string) => [Message | MessageTable, string] | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Calls `watcher` once in the microtask after words are registered or
|
|
83
|
+
* cleared, or the document's `lang` changes. Returns the function that stops
|
|
84
|
+
* it.
|
|
85
|
+
*
|
|
86
|
+
* The first watcher in a browser starts watching the document's `lang`, once
|
|
87
|
+
* for the page.
|
|
88
|
+
*/
|
|
89
|
+
export declare const watchWords: (watcher: () => void) => (() => void);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const held = (globalThis[Symbol.for('helix.words')] ??= {
|
|
2
|
+
fallback: 'en',
|
|
3
|
+
packs: new Map(),
|
|
4
|
+
watchers: new Set(),
|
|
5
|
+
});
|
|
6
|
+
const changed = () => {
|
|
7
|
+
if (!held.queued) {
|
|
8
|
+
held.queued = true;
|
|
9
|
+
queueMicrotask(() => {
|
|
10
|
+
held.queued = false;
|
|
11
|
+
for (const watcher of held.watchers) {
|
|
12
|
+
watcher();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export const registerWords = (locale, ...words) => {
|
|
18
|
+
const key = locale.toLowerCase();
|
|
19
|
+
const pack = held.packs.get(key) ?? {};
|
|
20
|
+
held.packs.set(key, pack);
|
|
21
|
+
for (const one of words) {
|
|
22
|
+
for (const tag in one) {
|
|
23
|
+
const into = (pack[tag] ??= {});
|
|
24
|
+
for (const [name, value] of Object.entries(one[tag] ?? {})) {
|
|
25
|
+
const before = into[name];
|
|
26
|
+
into[name] =
|
|
27
|
+
typeof value === 'object' && typeof before === 'object' ? { ...before, ...value } : value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
changed();
|
|
32
|
+
};
|
|
33
|
+
export const clearWords = (locale) => {
|
|
34
|
+
if (locale === undefined) {
|
|
35
|
+
held.packs.clear();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
held.packs.delete(locale.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
changed();
|
|
41
|
+
};
|
|
42
|
+
export const setDefaultWordsLocale = (locale) => {
|
|
43
|
+
held.fallback = locale;
|
|
44
|
+
changed();
|
|
45
|
+
};
|
|
46
|
+
export const defaultWordsLocale = () => held.fallback;
|
|
47
|
+
export const refreshWords = () => changed();
|
|
48
|
+
const chains = new Map();
|
|
49
|
+
export const localeChain = (locale) => {
|
|
50
|
+
let found = chains.get(locale);
|
|
51
|
+
if (found) {
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
let tags = [];
|
|
55
|
+
try {
|
|
56
|
+
const asked = new Intl.Locale(locale);
|
|
57
|
+
const full = asked.maximize?.();
|
|
58
|
+
const script = `${full?.language}-${full?.script}`;
|
|
59
|
+
tags = [
|
|
60
|
+
asked.baseName,
|
|
61
|
+
`${script}-${full?.region}`,
|
|
62
|
+
script,
|
|
63
|
+
`${asked.language}-${asked.region}`,
|
|
64
|
+
asked.language,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
}
|
|
69
|
+
found = [
|
|
70
|
+
...new Set([...tags, 'en']
|
|
71
|
+
.filter((tag) => tag && !tag.includes('undefined'))
|
|
72
|
+
.map((tag) => tag.toLowerCase())),
|
|
73
|
+
];
|
|
74
|
+
chains.set(locale, found);
|
|
75
|
+
return found;
|
|
76
|
+
};
|
|
77
|
+
export const wordsRegistered = () => held.packs.size > 0;
|
|
78
|
+
export const lookupWord = (locale, tag, name) => {
|
|
79
|
+
let found;
|
|
80
|
+
for (const one of localeChain(locale)) {
|
|
81
|
+
const word = held.packs.get(one)?.[tag]?.[name];
|
|
82
|
+
if (word === undefined) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof word === 'string') {
|
|
86
|
+
return found ?? [word, one];
|
|
87
|
+
}
|
|
88
|
+
found = found ? [{ ...word, ...found[0] }, found[1]] : [word, one];
|
|
89
|
+
}
|
|
90
|
+
return found;
|
|
91
|
+
};
|
|
92
|
+
export const watchWords = (watcher) => {
|
|
93
|
+
held.watchers.add(watcher);
|
|
94
|
+
if (!held.observed && typeof MutationObserver === 'function' && globalThis.document) {
|
|
95
|
+
held.observed = true;
|
|
96
|
+
new MutationObserver(changed).observe(document.documentElement, {
|
|
97
|
+
attributeFilter: ['lang'],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return () => held.watchers.delete(watcher);
|
|
101
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale an element speaks: the one its words, its plural forms, and the
|
|
3
|
+
* numbers in its sentences are in.
|
|
4
|
+
*
|
|
5
|
+
* 1. The element's `locale` property, where it has one and it is set.
|
|
6
|
+
* 2. On a server, and in the first render while the element hydrates, its own
|
|
7
|
+
* `lang` attribute, then the locale set with `setDefaultWordsLocale`, which
|
|
8
|
+
* is `en` until you set it. A server knows the element's attributes and
|
|
9
|
+
* nothing around it, so a hydrating render reads the same inputs and draws
|
|
10
|
+
* what the server drew.
|
|
11
|
+
* 3. Otherwise, the nearest `lang`, read out through shadow roots, then the
|
|
12
|
+
* runtime's own locale.
|
|
13
|
+
*
|
|
14
|
+
* **One rule for every family**, so one sentence never mixes the words of one
|
|
15
|
+
* language with the plural rules or digits of another. A `lang` that names a
|
|
16
|
+
* bare language the runtime already speaks, such as `en` in a British
|
|
17
|
+
* browser, is the runtime's locale, as `localeOf` reads it for dates.
|
|
18
|
+
*
|
|
19
|
+
* Always a tag `Intl` takes.
|
|
20
|
+
*/
|
|
21
|
+
export declare const wordsLocale: (element: Element & {
|
|
22
|
+
locale?: string | undefined;
|
|
23
|
+
}) => string;
|
|
24
|
+
/**
|
|
25
|
+
* The locale an element hands a child that reads only its own `lang` or
|
|
26
|
+
* `locale`, such as the list inside a combobox: the element's `locale`, or
|
|
27
|
+
* the nearest `lang` where it names a locale the runtime does not already
|
|
28
|
+
* speak. Empty where nothing says, so the child keeps the runtime's locale and
|
|
29
|
+
* gains no attribute, and empty while the element hydrates, as a server could
|
|
30
|
+
* not have known it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const handedLocale: (element: Element & {
|
|
33
|
+
locale?: string | undefined;
|
|
34
|
+
}) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { localeOf, runtimeLocale } from '../utils/locale-of/locale-of.js';
|
|
2
|
+
import { defaultWordsLocale } from './registry.js';
|
|
3
|
+
const checked = new Map();
|
|
4
|
+
const tagOf = (written) => {
|
|
5
|
+
let found = checked.get(written);
|
|
6
|
+
if (found === undefined) {
|
|
7
|
+
found = '';
|
|
8
|
+
try {
|
|
9
|
+
found = Intl.getCanonicalLocales(written)[0] ?? '';
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
}
|
|
13
|
+
checked.set(written, found);
|
|
14
|
+
}
|
|
15
|
+
return found;
|
|
16
|
+
};
|
|
17
|
+
export const wordsLocale = (element) => {
|
|
18
|
+
const own = tagOf(element.locale || '');
|
|
19
|
+
if (own) {
|
|
20
|
+
return own;
|
|
21
|
+
}
|
|
22
|
+
if (typeof element.closest !== 'function' || element.hydrating) {
|
|
23
|
+
return tagOf(element.getAttribute?.('lang') ?? '') || defaultWordsLocale();
|
|
24
|
+
}
|
|
25
|
+
return localeOf(element) || runtimeLocale();
|
|
26
|
+
};
|
|
27
|
+
export const handedLocale = (element) => {
|
|
28
|
+
const own = typeof element.locale === 'string' ? element.locale : '';
|
|
29
|
+
return element.hydrating ? own : localeOf(element, own);
|
|
30
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactiveElement } from 'lit';
|
|
2
|
+
import { type MessageValues } from './message.js';
|
|
3
|
+
/** An element that reads words: a Lit element, perhaps with a `locale`. */
|
|
4
|
+
type Host = ReactiveElement & {
|
|
5
|
+
locale?: string | undefined;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Makes a property a word property: one a page can set, register for a
|
|
9
|
+
* locale with `registerWords`, or leave in English. Written above
|
|
10
|
+
* `@property`, which declares its attribute:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* static words = 'helix-combobox'
|
|
14
|
+
*
|
|
15
|
+
* @words
|
|
16
|
+
* @property({ attribute: 'empty-text' })
|
|
17
|
+
* emptyText = 'No matches'
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **Reading the property gives the word in the element's locale**: a value
|
|
21
|
+
* you set, then the words registered for the element's locale, then the
|
|
22
|
+
* initializer, which is the English. Setting any value, the English included,
|
|
23
|
+
* makes it yours. Setting `null` or `undefined`, or removing the attribute,
|
|
24
|
+
* gives the registry's word back. So does an empty validation message, a
|
|
25
|
+
* property whose name ends in `Message`: a field never reports a failed check
|
|
26
|
+
* with no sentence, and a template that renders an empty variable into
|
|
27
|
+
* `range-overflow-message=""` gets the built-in sentence.
|
|
28
|
+
*
|
|
29
|
+
* **A subclass or a family that overrides a word's English writes `@words`
|
|
30
|
+
* above the override**, and its initializer is the English: a family's
|
|
31
|
+
* `Remove filter` over a mixin's `Dismiss`. Every `@words` declaration has an
|
|
32
|
+
* initializer, so an element knows how many of its sets are English.
|
|
33
|
+
*
|
|
34
|
+
* The class's static `words` names the key its words are registered under, the
|
|
35
|
+
* family's styled tag, so the headless and styled tiers share one key. A mixin
|
|
36
|
+
* that declares words writes `static words = ''`, which its host's key
|
|
37
|
+
* replaces; a host with no key of its own reads its tag name. An element whose
|
|
38
|
+
* words change updates once, with each changed property named.
|
|
39
|
+
*/
|
|
40
|
+
export declare const words: (proto: object, name: string) => void;
|
|
41
|
+
/**
|
|
42
|
+
* A word property of an element, formatted: its values put in, its numbers
|
|
43
|
+
* written in the element's locale, and its plural forms picked by the rules
|
|
44
|
+
* of the language its words are in. A property no `@words` declares is
|
|
45
|
+
* formatted as it reads, with the element's own plural rules.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* formatWords(this, 'resultsLabel', { count: 3 }) // '3 results'
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const formatWords: (host: Host, name: string, values?: MessageValues) => string;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { formatMessage } from './message.js';
|
|
2
|
+
import { lookupWord, watchWords, wordsRegistered } from './registry.js';
|
|
3
|
+
import { wordsLocale } from './words-locale.js';
|
|
4
|
+
const HELD = Symbol();
|
|
5
|
+
const heldBy = (host) => host[HELD] ?? {};
|
|
6
|
+
const declared = new WeakMap();
|
|
7
|
+
const initializers = (host, name) => {
|
|
8
|
+
let count = 0;
|
|
9
|
+
for (let proto = Object.getPrototypeOf(host); proto; proto = Object.getPrototypeOf(proto)) {
|
|
10
|
+
count += declared.get(proto)?.has(name) ? 1 : 0;
|
|
11
|
+
}
|
|
12
|
+
return count;
|
|
13
|
+
};
|
|
14
|
+
const resolve = (host, name) => {
|
|
15
|
+
const word = heldBy(host)[name];
|
|
16
|
+
if (!word) {
|
|
17
|
+
return [host[name], ''];
|
|
18
|
+
}
|
|
19
|
+
const { e: english, o: own } = word;
|
|
20
|
+
const table = typeof english === 'object' && english;
|
|
21
|
+
let found = [own, ''];
|
|
22
|
+
if (own == null || table) {
|
|
23
|
+
const registered = wordsRegistered()
|
|
24
|
+
? lookupWord(wordsLocale(host), host.constructor.words || host.localName, name)
|
|
25
|
+
: undefined;
|
|
26
|
+
found = table
|
|
27
|
+
? [{ ...table, ...registered?.[0], ...own }, '']
|
|
28
|
+
: (registered ?? [english, 'en']);
|
|
29
|
+
}
|
|
30
|
+
word.l = found[0];
|
|
31
|
+
return found;
|
|
32
|
+
};
|
|
33
|
+
const same = (one, two) => one === two || JSON.stringify(one) === JSON.stringify(two);
|
|
34
|
+
const recheck = (host) => {
|
|
35
|
+
for (const [name, word] of Object.entries(heldBy(host))) {
|
|
36
|
+
const before = word.l;
|
|
37
|
+
if ('l' in word && !same(before, resolve(host, name)[0])) {
|
|
38
|
+
host.requestUpdate(name, before);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const watcher = (host) => {
|
|
43
|
+
let stop;
|
|
44
|
+
let locale = '';
|
|
45
|
+
return {
|
|
46
|
+
hostConnected: () => {
|
|
47
|
+
locale = wordsLocale(host);
|
|
48
|
+
stop = watchWords(() => {
|
|
49
|
+
recheck(host);
|
|
50
|
+
const now = wordsLocale(host);
|
|
51
|
+
if (now !== locale) {
|
|
52
|
+
locale = now;
|
|
53
|
+
host.requestUpdate();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
hostDisconnected: () => stop?.(),
|
|
58
|
+
hostUpdated: () => {
|
|
59
|
+
if (host.hydrating === true) {
|
|
60
|
+
queueMicrotask(() => recheck(host));
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export const words = (proto, name) => {
|
|
66
|
+
if (!Object.getOwnPropertyDescriptor(proto, name)) {
|
|
67
|
+
;
|
|
68
|
+
proto.constructor.createProperty(name);
|
|
69
|
+
}
|
|
70
|
+
const names = declared.get(proto) ?? new Set();
|
|
71
|
+
declared.set(proto, names.add(name));
|
|
72
|
+
Object.defineProperty(proto, name, {
|
|
73
|
+
configurable: true,
|
|
74
|
+
enumerable: true,
|
|
75
|
+
get() {
|
|
76
|
+
return resolve(this, name)[0];
|
|
77
|
+
},
|
|
78
|
+
set(value) {
|
|
79
|
+
if (!this[HELD]) {
|
|
80
|
+
this[HELD] = {};
|
|
81
|
+
this.addController(watcher(this));
|
|
82
|
+
}
|
|
83
|
+
const word = (this[HELD][name] ??= { i: 0 });
|
|
84
|
+
if (word.i < initializers(this, name)) {
|
|
85
|
+
word.i++;
|
|
86
|
+
word.e = value;
|
|
87
|
+
this.requestUpdate(name, undefined, undefined, true, value);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const before = this[name];
|
|
91
|
+
word.o = value === '' && name.endsWith('Message') ? undefined : value;
|
|
92
|
+
this.requestUpdate(name, before);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
export const formatWords = (host, name, values = {}) => {
|
|
97
|
+
const [message, from] = resolve(host, name);
|
|
98
|
+
const locale = wordsLocale(host);
|
|
99
|
+
return formatMessage(String(message ?? ''), values, locale, { plurals: from || locale });
|
|
100
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.149.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -17,7 +17,29 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"default": "./dist/index.js"
|
|
19
19
|
},
|
|
20
|
-
"./
|
|
20
|
+
"./audits/name": {
|
|
21
|
+
"types": "./dist/utils/audit-name/audit-name.d.ts",
|
|
22
|
+
"development": "./dist/utils/audit-name/audit-name.js",
|
|
23
|
+
"helix-development": "./dist/utils/audit-name/audit-name.js",
|
|
24
|
+
"default": "./dist/utils/audit-name/audit-name-off.js"
|
|
25
|
+
},
|
|
26
|
+
"./audits/react": {
|
|
27
|
+
"types": "./dist/helix-element/react-audit.d.ts",
|
|
28
|
+
"development": "./dist/helix-element/react-audit.js",
|
|
29
|
+
"helix-development": "./dist/helix-element/react-audit.js",
|
|
30
|
+
"default": "./dist/helix-element/react-audit-off.js"
|
|
31
|
+
},
|
|
32
|
+
"./audits/source": {
|
|
33
|
+
"types": "./dist/utils/audit-source/audit-source.d.ts",
|
|
34
|
+
"development": "./dist/utils/audit-source/audit-source.js",
|
|
35
|
+
"helix-development": "./dist/utils/audit-source/audit-source.js",
|
|
36
|
+
"default": "./dist/utils/audit-source/audit-source-off.js"
|
|
37
|
+
},
|
|
38
|
+
"./custom-elements.json": "./custom-elements.json",
|
|
39
|
+
"./words": {
|
|
40
|
+
"types": "./dist/words/index.d.ts",
|
|
41
|
+
"default": "./dist/words/index.js"
|
|
42
|
+
}
|
|
21
43
|
},
|
|
22
44
|
"files": [
|
|
23
45
|
"custom-elements.json",
|
|
@@ -27,44 +49,37 @@
|
|
|
27
49
|
"sideEffects": false,
|
|
28
50
|
"scripts": {
|
|
29
51
|
"analyze": "bun --bun helix-cem",
|
|
30
|
-
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json",
|
|
31
|
-
"test": "bun test src --conditions helix-
|
|
52
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json && bun scripts/development-imports.mjs",
|
|
53
|
+
"test": "bun test src --conditions helix-development",
|
|
32
54
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
33
55
|
},
|
|
34
56
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
35
57
|
"dependencies": {
|
|
36
58
|
"@floating-ui/dom": "^1.8.0",
|
|
37
|
-
"@fusion.dev/helix-icons": "0.
|
|
59
|
+
"@fusion.dev/helix-icons": "0.149.0"
|
|
38
60
|
},
|
|
39
61
|
"devDependencies": {
|
|
40
|
-
"@fusion.dev/helix-cem": "0.
|
|
41
|
-
"@fusion.dev/helix-testing": "0.
|
|
62
|
+
"@fusion.dev/helix-cem": "0.149.0",
|
|
63
|
+
"@fusion.dev/helix-testing": "0.149.0",
|
|
42
64
|
"@lit-labs/ssr": "^4.1.0",
|
|
43
65
|
"lit": "^3.2.1"
|
|
44
66
|
},
|
|
45
67
|
"homepage": "https://fusion.dev",
|
|
46
68
|
"imports": {
|
|
47
69
|
"#audit-name": {
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"default": "./src/utils/audit-name/audit-name-off.ts"
|
|
52
|
-
},
|
|
53
|
-
"types": "./dist/utils/audit-name/audit-name.d.ts",
|
|
54
|
-
"development": "./dist/utils/audit-name/audit-name.js",
|
|
55
|
-
"helix-development": "./dist/utils/audit-name/audit-name.js",
|
|
56
|
-
"default": "./dist/utils/audit-name/audit-name-off.js"
|
|
70
|
+
"development": "./src/utils/audit-name/audit-name.ts",
|
|
71
|
+
"helix-development": "./src/utils/audit-name/audit-name.ts",
|
|
72
|
+
"default": "./src/utils/audit-name/audit-name-off.ts"
|
|
57
73
|
},
|
|
58
74
|
"#audit-source": {
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
"development": "./
|
|
66
|
-
"
|
|
67
|
-
"default": "./dist/utils/audit-source/audit-source-off.js"
|
|
75
|
+
"development": "./src/utils/audit-source/audit-source.ts",
|
|
76
|
+
"helix-development": "./src/utils/audit-source/audit-source.ts",
|
|
77
|
+
"default": "./src/utils/audit-source/audit-source-off.ts"
|
|
78
|
+
},
|
|
79
|
+
"#react-audit": {
|
|
80
|
+
"development": "./src/helix-element/react-audit.ts",
|
|
81
|
+
"helix-development": "./src/helix-element/react-audit.ts",
|
|
82
|
+
"default": "./src/helix-element/react-audit-off.ts"
|
|
68
83
|
}
|
|
69
84
|
},
|
|
70
85
|
"keywords": [
|