@peektravel/app-utilities 0.5.4 → 0.6.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/README.md +4 -0
- package/dist/index.cjs +18 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +362 -35
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +94 -15
- package/dist/ui/index.d.ts +94 -15
- package/dist/ui/index.js +362 -35
- package/dist/ui/index.js.map +1 -1
- package/docs/ui.md +26 -3
- package/llms.txt +5 -1
- package/package.json +1 -1
package/dist/ui/index.cjs
CHANGED
|
@@ -54,9 +54,31 @@ var OdyElement = class extends HTMLElement {
|
|
|
54
54
|
/** Self-pruning re-render hook subscribed to locale changes (set on mount). */
|
|
55
55
|
#localeCb = null;
|
|
56
56
|
connectedCallback() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
this.#upgradeProperties();
|
|
58
|
+
if (this.#slot === null) {
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
if (this.isConnected && this.#slot === null) this.render();
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
this.render();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Re-apply own properties that shadow a prototype accessor (pre-upgrade sets). */
|
|
67
|
+
#upgradeProperties() {
|
|
68
|
+
const self = this;
|
|
69
|
+
for (const key of Object.getOwnPropertyNames(this)) {
|
|
70
|
+
let proto = Object.getPrototypeOf(this);
|
|
71
|
+
while (proto && proto !== HTMLElement.prototype) {
|
|
72
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
73
|
+
if (desc && (desc.get || desc.set)) {
|
|
74
|
+
const value = self[key];
|
|
75
|
+
delete self[key];
|
|
76
|
+
self[key] = value;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
proto = Object.getPrototypeOf(proto);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
60
82
|
}
|
|
61
83
|
attributeChangedCallback() {
|
|
62
84
|
if (this.#slot !== null) this.render();
|
|
@@ -85,6 +107,77 @@ var OdyElement = class extends HTMLElement {
|
|
|
85
107
|
}
|
|
86
108
|
return this.#slot;
|
|
87
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Move slotted content out of a portaled chrome node (dialog/panel/bubble)
|
|
112
|
+
* back into the captured slot fragment, so it survives `disconnectedCallback`
|
|
113
|
+
* tearing that node down and is re-slotted by the re-render on reconnect.
|
|
114
|
+
* Portal components call this before `removePortal(...)`; without it, moving
|
|
115
|
+
* the element in the DOM would permanently lose the consumer's content.
|
|
116
|
+
*/
|
|
117
|
+
reclaimPortaledSlot(node) {
|
|
118
|
+
if (this.#slot === null || !node) return;
|
|
119
|
+
const slot = node.querySelector("[data-ody-slot]");
|
|
120
|
+
if (slot) while (slot.firstChild) this.#slot.appendChild(slot.firstChild);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The element that actually holds slotted (consumer-provided) children right
|
|
124
|
+
* now, or `null` before the first render, for a non-slotting component, or
|
|
125
|
+
* while the slot is portaled out of the host (e.g. an open modal). See the
|
|
126
|
+
* child-mutation overrides below for why this is needed.
|
|
127
|
+
*/
|
|
128
|
+
#slotTarget() {
|
|
129
|
+
return this.#slot !== null ? this.querySelector("[data-ody-slot]") : null;
|
|
130
|
+
}
|
|
131
|
+
// --- Framework-reconciler child mutations -------------------------------
|
|
132
|
+
//
|
|
133
|
+
// `mount()` physically relocates the consumer's light-DOM children into the
|
|
134
|
+
// internal `[data-ody-slot]` node, so a slotted child's real `parentNode` is
|
|
135
|
+
// that slot div, not the host `<ody-*>` element. Framework reconcilers
|
|
136
|
+
// (React, Vue, Angular, Svelte, …) don't track parent pointers — to move or
|
|
137
|
+
// remove a node they call `host.removeChild(child)` /
|
|
138
|
+
// `host.insertBefore(node, ref)` where `host` is the element from their
|
|
139
|
+
// virtual tree. Because the child no longer lives directly under the host,
|
|
140
|
+
// those calls would throw `NotFoundError` (or reorder into the wrong place).
|
|
141
|
+
//
|
|
142
|
+
// These overrides forward the operation to wherever the node actually lives
|
|
143
|
+
// (the slot), so reconciler operations succeed regardless of the relocation.
|
|
144
|
+
// For removal/insertion/replacement we key off the target node's real
|
|
145
|
+
// `parentNode`, which also works while the slot is portaled to `document.body`
|
|
146
|
+
// (open modal/panel/popover). Internal chrome mutations use {@link adopt} to
|
|
147
|
+
// bypass this forwarding; `mount()` itself never routes through here (it uses
|
|
148
|
+
// `this.innerHTML` and operates on the slot/fragment nodes directly).
|
|
149
|
+
appendChild(node) {
|
|
150
|
+
const slot = this.#slotTarget();
|
|
151
|
+
return slot ? slot.appendChild(node) : super.appendChild(node);
|
|
152
|
+
}
|
|
153
|
+
insertBefore(node, child) {
|
|
154
|
+
if (child && child.parentNode && child.parentNode !== this) {
|
|
155
|
+
return child.parentNode.insertBefore(node, child);
|
|
156
|
+
}
|
|
157
|
+
const slot = this.#slotTarget();
|
|
158
|
+
return slot ? slot.insertBefore(node, child) : super.insertBefore(node, child);
|
|
159
|
+
}
|
|
160
|
+
removeChild(child) {
|
|
161
|
+
if (child.parentNode && child.parentNode !== this) {
|
|
162
|
+
return child.parentNode.removeChild(child);
|
|
163
|
+
}
|
|
164
|
+
return super.removeChild(child);
|
|
165
|
+
}
|
|
166
|
+
replaceChild(node, child) {
|
|
167
|
+
if (child.parentNode && child.parentNode !== this) {
|
|
168
|
+
return child.parentNode.replaceChild(node, child);
|
|
169
|
+
}
|
|
170
|
+
return super.replaceChild(node, child);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Append an element the component owns (chrome / portaled nodes) as a direct
|
|
174
|
+
* host child, bypassing the slot-forwarding {@link appendChild} override.
|
|
175
|
+
* Portal-based components use this to re-home a portaled node before a
|
|
176
|
+
* re-render rebuilds the chrome.
|
|
177
|
+
*/
|
|
178
|
+
adopt(node) {
|
|
179
|
+
super.appendChild(node);
|
|
180
|
+
}
|
|
88
181
|
/** Read a string attribute with a fallback. */
|
|
89
182
|
attr(name, fallback = "") {
|
|
90
183
|
return this.getAttribute(name) ?? fallback;
|
|
@@ -156,8 +249,12 @@ function wireLocaleObserver() {
|
|
|
156
249
|
function escapeHtml(value) {
|
|
157
250
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
158
251
|
}
|
|
252
|
+
var SAFE_CSS_COLOR = /^[a-zA-Z0-9#(),.%\s-]+$/;
|
|
253
|
+
function cssColor(value, fallback = "") {
|
|
254
|
+
return value && SAFE_CSS_COLOR.test(value) ? value : fallback;
|
|
255
|
+
}
|
|
159
256
|
function classes(...parts) {
|
|
160
|
-
return parts.filter(Boolean).join(" ");
|
|
257
|
+
return escapeHtml(parts.filter(Boolean).join(" "));
|
|
161
258
|
}
|
|
162
259
|
function reflectControlValue(control, value) {
|
|
163
260
|
if (control && control.value !== value) control.value = value;
|
|
@@ -851,8 +948,8 @@ var OdyCard = class extends OdyElement {
|
|
|
851
948
|
static observedAttributes = ["bar-color", "clickable", "no-bar"];
|
|
852
949
|
render() {
|
|
853
950
|
const noBar = this.flag("no-bar");
|
|
854
|
-
const barColor = this.attr("bar-color");
|
|
855
|
-
const barStyle = barColor ? ` style="background-color:${
|
|
951
|
+
const barColor = cssColor(this.attr("bar-color"));
|
|
952
|
+
const barStyle = barColor ? ` style="background-color:${barColor}"` : "";
|
|
856
953
|
const cls = classes(
|
|
857
954
|
"ody-card__container",
|
|
858
955
|
this.flag("clickable") && "ody-card--clickable",
|
|
@@ -918,7 +1015,7 @@ var OdyLoadingBar = class extends OdyElement {
|
|
|
918
1015
|
render() {
|
|
919
1016
|
const raw = Number.parseFloat(this.attr("value", "0"));
|
|
920
1017
|
const value = Number.isFinite(raw) ? Math.min(100, Math.max(0, raw)) : 0;
|
|
921
|
-
const color = this.attr("color")
|
|
1018
|
+
const color = cssColor(this.attr("color"), "var(--color-interaction-300)");
|
|
922
1019
|
const label = this.attr("label");
|
|
923
1020
|
const text = label ? `<div class="loading-bar__text-container"><span>${this.esc(label)}</span><span>${value}%</span></div>` : "";
|
|
924
1021
|
this.mount(
|
|
@@ -1121,11 +1218,11 @@ var OdyProductIndicator = class extends OdyElement {
|
|
|
1121
1218
|
`ody-product-indicator-container--size-${size}`,
|
|
1122
1219
|
clickable && "ody-product-indicator-clickable"
|
|
1123
1220
|
);
|
|
1124
|
-
const barColor = this.attr("bar-color");
|
|
1125
|
-
const barStyle = barColor ? ` style="background-color:${
|
|
1221
|
+
const barColor = cssColor(this.attr("bar-color"));
|
|
1222
|
+
const barStyle = barColor ? ` style="background-color:${barColor}"` : "";
|
|
1126
1223
|
const bar = `<div class="ody-product-indicator__bar"${barStyle}></div>`;
|
|
1127
|
-
const textColor = this.attr("text-color");
|
|
1128
|
-
const textStyle = textColor ? ` style="color:${
|
|
1224
|
+
const textColor = cssColor(this.attr("text-color"));
|
|
1225
|
+
const textStyle = textColor ? ` style="color:${textColor}"` : "";
|
|
1129
1226
|
const name = this.attr("name");
|
|
1130
1227
|
const nameEl = `<div class="ody-product-indicator__name"${textStyle}>${this.esc(name)}</div>`;
|
|
1131
1228
|
const detail = this.attr("detail");
|
|
@@ -1141,6 +1238,16 @@ define("ody-product-indicator", OdyProductIndicator);
|
|
|
1141
1238
|
// src/ui/components/toggle-button.ts
|
|
1142
1239
|
var OdyToggleButton = class extends OdyElement {
|
|
1143
1240
|
static observedAttributes = ["options", "selected", "size", "disabled"];
|
|
1241
|
+
/** Options set via the JS property; when set it wins over the attribute. */
|
|
1242
|
+
#options = null;
|
|
1243
|
+
/** Options: the JS property wins, else the JSON `options` attribute. */
|
|
1244
|
+
get options() {
|
|
1245
|
+
return this.#options ?? this.#parseOptions();
|
|
1246
|
+
}
|
|
1247
|
+
set options(next) {
|
|
1248
|
+
this.#options = coerceToggleOptions(next);
|
|
1249
|
+
if (this.querySelector(".ody-toggle-button")) this.render();
|
|
1250
|
+
}
|
|
1144
1251
|
connectedCallback() {
|
|
1145
1252
|
super.connectedCallback();
|
|
1146
1253
|
this.addEventListener("click", this.#handleClick);
|
|
@@ -1172,7 +1279,7 @@ var OdyToggleButton = class extends OdyElement {
|
|
|
1172
1279
|
const size = this.attr("size", "base");
|
|
1173
1280
|
const selected = this.attr("selected");
|
|
1174
1281
|
const groupDisabled = this.flag("disabled");
|
|
1175
|
-
const buttons = this
|
|
1282
|
+
const buttons = this.options.map((opt) => {
|
|
1176
1283
|
const isSelected = selected !== "" && selected === opt.value;
|
|
1177
1284
|
const isDisabled = groupDisabled || Boolean(opt.disabled);
|
|
1178
1285
|
const cls = classes(
|
|
@@ -1190,6 +1297,18 @@ var OdyToggleButton = class extends OdyElement {
|
|
|
1190
1297
|
this.mount(`<div class="ody-toggle-button" aria-label="${this.localized("aria-label", "toggleGroup")}">${buttons}</div>`);
|
|
1191
1298
|
}
|
|
1192
1299
|
};
|
|
1300
|
+
function coerceToggleOptions(next) {
|
|
1301
|
+
if (Array.isArray(next)) return next;
|
|
1302
|
+
if (typeof next === "string" && next) {
|
|
1303
|
+
try {
|
|
1304
|
+
const parsed = JSON.parse(next);
|
|
1305
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1306
|
+
} catch {
|
|
1307
|
+
return [];
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return [];
|
|
1311
|
+
}
|
|
1193
1312
|
define("ody-toggle-button", OdyToggleButton);
|
|
1194
1313
|
|
|
1195
1314
|
// src/ui/components/section.ts
|
|
@@ -1762,6 +1881,20 @@ var OdyMoneyInput = class extends OdyElement {
|
|
|
1762
1881
|
get precision() {
|
|
1763
1882
|
return CURRENCY_PRECISION[this.attr("currency", "USD")] ?? DEFAULT_PRECISION;
|
|
1764
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Reflect a `value` change into the live control in place instead of a full
|
|
1886
|
+
* re-render, so a framework-controlled `value` binding never drops focus or
|
|
1887
|
+
* caret while the user is typing. Other observed attributes change the chrome
|
|
1888
|
+
* and still re-render via the base implementation.
|
|
1889
|
+
*/
|
|
1890
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1891
|
+
if (name === "value") {
|
|
1892
|
+
if (oldValue === newValue) return;
|
|
1893
|
+
reflectControlValue(this.querySelector(".ody-input__field"), newValue ?? "");
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
super.attributeChangedCallback();
|
|
1897
|
+
}
|
|
1765
1898
|
render() {
|
|
1766
1899
|
const size = this.attr("size", "base");
|
|
1767
1900
|
const isReadonly = this.flag("readonly");
|
|
@@ -1785,11 +1918,15 @@ var OdyMoneyInput = class extends OdyElement {
|
|
|
1785
1918
|
const input = this.querySelector(".ody-input__field");
|
|
1786
1919
|
input?.addEventListener("input", this.#onInput);
|
|
1787
1920
|
input?.addEventListener("blur", this.#onBlur);
|
|
1921
|
+
input?.addEventListener("change", this.#onNativeChange);
|
|
1788
1922
|
}
|
|
1923
|
+
#onNativeChange = (event) => {
|
|
1924
|
+
event.stopPropagation();
|
|
1925
|
+
};
|
|
1789
1926
|
#onInput = (event) => {
|
|
1790
1927
|
event.stopPropagation();
|
|
1791
1928
|
const value = event.target.value;
|
|
1792
|
-
this
|
|
1929
|
+
this.value = value;
|
|
1793
1930
|
this.dispatchEvent(new CustomEvent("input", { detail: { value }, bubbles: true }));
|
|
1794
1931
|
};
|
|
1795
1932
|
#onBlur = (event) => {
|
|
@@ -1825,6 +1962,20 @@ var OdyPercentageInput = class extends OdyElement {
|
|
|
1825
1962
|
this.#value = next;
|
|
1826
1963
|
this.setAttribute("value", next);
|
|
1827
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Reflect a `value` change into the live control in place instead of a full
|
|
1967
|
+
* re-render, so a framework-controlled `value` binding never drops focus or
|
|
1968
|
+
* caret while the user is typing. Other observed attributes change the chrome
|
|
1969
|
+
* and still re-render via the base implementation.
|
|
1970
|
+
*/
|
|
1971
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1972
|
+
if (name === "value") {
|
|
1973
|
+
if (oldValue === newValue) return;
|
|
1974
|
+
reflectControlValue(this.querySelector(".ody-input__field"), newValue ?? "");
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
super.attributeChangedCallback();
|
|
1978
|
+
}
|
|
1828
1979
|
render() {
|
|
1829
1980
|
const size = this.attr("size", "base");
|
|
1830
1981
|
const isReadonly = this.flag("readonly");
|
|
@@ -1847,7 +1998,11 @@ var OdyPercentageInput = class extends OdyElement {
|
|
|
1847
1998
|
input?.addEventListener("keypress", this.#onKeypress);
|
|
1848
1999
|
input?.addEventListener("input", this.#onInput);
|
|
1849
2000
|
input?.addEventListener("blur", this.#onBlur);
|
|
2001
|
+
input?.addEventListener("change", this.#onNativeChange);
|
|
1850
2002
|
}
|
|
2003
|
+
#onNativeChange = (event) => {
|
|
2004
|
+
event.stopPropagation();
|
|
2005
|
+
};
|
|
1851
2006
|
#onKeypress = (event) => {
|
|
1852
2007
|
if (NON_NUMERIC_KEY.test(event.key)) {
|
|
1853
2008
|
event.preventDefault();
|
|
@@ -1863,7 +2018,7 @@ var OdyPercentageInput = class extends OdyElement {
|
|
|
1863
2018
|
#onInput = (event) => {
|
|
1864
2019
|
event.stopPropagation();
|
|
1865
2020
|
const value = event.target.value;
|
|
1866
|
-
this
|
|
2021
|
+
this.value = value;
|
|
1867
2022
|
this.dispatchEvent(new CustomEvent("input", { detail: { value }, bubbles: true }));
|
|
1868
2023
|
};
|
|
1869
2024
|
#onBlur = (event) => {
|
|
@@ -1883,6 +2038,26 @@ var OdyPercentageInput = class extends OdyElement {
|
|
|
1883
2038
|
};
|
|
1884
2039
|
define("ody-percentage-input", OdyPercentageInput);
|
|
1885
2040
|
|
|
2041
|
+
// src/ui/checkbox-state.ts
|
|
2042
|
+
function applyCheckboxState(root, checked, indeterminate) {
|
|
2043
|
+
if (!root) return;
|
|
2044
|
+
root.classList.toggle("ody-checkbox--checked", checked);
|
|
2045
|
+
const input = root.querySelector(".ody-checkbox__input");
|
|
2046
|
+
if (input) {
|
|
2047
|
+
input.checked = checked;
|
|
2048
|
+
input.indeterminate = indeterminate;
|
|
2049
|
+
}
|
|
2050
|
+
const box = root.querySelector(".ody-checkbox__box");
|
|
2051
|
+
box?.querySelector(".ody-checkbox__mark")?.remove();
|
|
2052
|
+
const icon = indeterminate ? "minus" : checked ? "check" : "";
|
|
2053
|
+
if (icon && box) {
|
|
2054
|
+
const mark = document.createElement("span");
|
|
2055
|
+
mark.className = "ody-checkbox__mark";
|
|
2056
|
+
mark.innerHTML = iconSvg(icon, "icon__svg");
|
|
2057
|
+
box.appendChild(mark);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1886
2061
|
// src/ui/components/checkbox.ts
|
|
1887
2062
|
var OdyCheckbox = class extends OdyElement {
|
|
1888
2063
|
static observedAttributes = ["label", "size", "checked", "indeterminate", "disabled"];
|
|
@@ -1901,6 +2076,27 @@ var OdyCheckbox = class extends OdyElement {
|
|
|
1901
2076
|
set value(next) {
|
|
1902
2077
|
this.checked = next;
|
|
1903
2078
|
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Reflect `checked` / `indeterminate` in place instead of a full re-render, so
|
|
2081
|
+
* selecting the box (mouse or keyboard) never rebuilds the native `<input>`
|
|
2082
|
+
* and drops focus. Other observed attributes still re-render via the base.
|
|
2083
|
+
*/
|
|
2084
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2085
|
+
if ((name === "checked" || name === "indeterminate") && this.querySelector(".ody-checkbox__input")) {
|
|
2086
|
+
if (oldValue === newValue) return;
|
|
2087
|
+
this.#syncChecked();
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
super.attributeChangedCallback();
|
|
2091
|
+
}
|
|
2092
|
+
/** Update the live control, wrapper class, and check/minus mark in place. */
|
|
2093
|
+
#syncChecked() {
|
|
2094
|
+
applyCheckboxState(
|
|
2095
|
+
this.querySelector(".ody-checkbox"),
|
|
2096
|
+
this.flag("checked"),
|
|
2097
|
+
this.flag("indeterminate")
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
1904
2100
|
render() {
|
|
1905
2101
|
const size = this.attr("size", "small");
|
|
1906
2102
|
const isChecked = this.flag("checked");
|
|
@@ -1934,6 +2130,8 @@ var groupSeq = 0;
|
|
|
1934
2130
|
var OdyRadioButtonGroup = class extends OdyElement {
|
|
1935
2131
|
static observedAttributes = ["options", "value", "size", "disabled"];
|
|
1936
2132
|
#name = `ody-radio-button-group-${groupSeq += 1}`;
|
|
2133
|
+
/** Options set via the JS property; when set it wins over the attribute. */
|
|
2134
|
+
#options = null;
|
|
1937
2135
|
/** The selected option value. */
|
|
1938
2136
|
get value() {
|
|
1939
2137
|
return this.attr("value");
|
|
@@ -1941,9 +2139,29 @@ var OdyRadioButtonGroup = class extends OdyElement {
|
|
|
1941
2139
|
set value(next) {
|
|
1942
2140
|
this.setAttribute("value", next);
|
|
1943
2141
|
}
|
|
1944
|
-
/**
|
|
2142
|
+
/** Options: the JS property wins, else the JSON `options` attribute. */
|
|
1945
2143
|
get options() {
|
|
1946
|
-
return parseOptions(this.attr("options"));
|
|
2144
|
+
return this.#options ?? parseOptions(this.attr("options"));
|
|
2145
|
+
}
|
|
2146
|
+
set options(next) {
|
|
2147
|
+
this.#options = coerceOptions(next);
|
|
2148
|
+
if (this.querySelector(".ody-radio-button-group")) this.render();
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Reflect a `value` change by re-checking the matching radio in place instead
|
|
2152
|
+
* of rebuilding the fieldset — a full re-render would replace the focused
|
|
2153
|
+
* radio and break keyboard (arrow-key) navigation between options.
|
|
2154
|
+
*/
|
|
2155
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2156
|
+
if (name === "value" && this.querySelector(".ody-radio-button-input__field")) {
|
|
2157
|
+
if (oldValue === newValue) return;
|
|
2158
|
+
const selected = this.value;
|
|
2159
|
+
for (const input of this.querySelectorAll(".ody-radio-button-input__field")) {
|
|
2160
|
+
input.checked = input.value === selected;
|
|
2161
|
+
}
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
super.attributeChangedCallback();
|
|
1947
2165
|
}
|
|
1948
2166
|
render() {
|
|
1949
2167
|
const size = this.attr("size", "base");
|
|
@@ -1967,6 +2185,11 @@ var OdyRadioButtonGroup = class extends OdyElement {
|
|
|
1967
2185
|
this.dispatchEvent(new CustomEvent("change", { detail: { value }, bubbles: true }));
|
|
1968
2186
|
};
|
|
1969
2187
|
};
|
|
2188
|
+
function coerceOptions(next) {
|
|
2189
|
+
if (Array.isArray(next)) return next;
|
|
2190
|
+
if (typeof next === "string") return parseOptions(next);
|
|
2191
|
+
return [];
|
|
2192
|
+
}
|
|
1970
2193
|
function parseOptions(raw) {
|
|
1971
2194
|
if (!raw) return [];
|
|
1972
2195
|
try {
|
|
@@ -1982,26 +2205,63 @@ define("ody-radio-button-group", OdyRadioButtonGroup);
|
|
|
1982
2205
|
// src/ui/components/checkbox-group.ts
|
|
1983
2206
|
var OdyCheckboxGroup = class extends OdyElement {
|
|
1984
2207
|
static observedAttributes = ["options", "value", "select-all-label", "size", "disabled"];
|
|
1985
|
-
/**
|
|
2208
|
+
/** Options set via the JS property; when set it wins over the attribute. */
|
|
2209
|
+
#options = null;
|
|
2210
|
+
/**
|
|
2211
|
+
* The selected option values. The reflected `value` attribute is serialized
|
|
2212
|
+
* as a JSON array so values containing commas round-trip; a legacy
|
|
2213
|
+
* comma-separated attribute is still read for backwards compatibility.
|
|
2214
|
+
*/
|
|
1986
2215
|
get value() {
|
|
1987
2216
|
const raw = this.attr("value");
|
|
1988
|
-
|
|
2217
|
+
if (!raw) return [];
|
|
2218
|
+
try {
|
|
2219
|
+
const parsed = JSON.parse(raw);
|
|
2220
|
+
if (Array.isArray(parsed)) return parsed.map(String);
|
|
2221
|
+
} catch {
|
|
2222
|
+
}
|
|
2223
|
+
return raw.split(",").filter(Boolean);
|
|
1989
2224
|
}
|
|
1990
2225
|
set value(next) {
|
|
1991
|
-
this.setAttribute("value",
|
|
2226
|
+
this.setAttribute("value", JSON.stringify(Array.isArray(next) ? next : []));
|
|
1992
2227
|
}
|
|
1993
|
-
/**
|
|
2228
|
+
/** Options: the JS property wins, else the JSON `options` attribute. */
|
|
1994
2229
|
get options() {
|
|
1995
|
-
return parseOptions2(this.attr("options"));
|
|
2230
|
+
return this.#options ?? parseOptions2(this.attr("options"));
|
|
1996
2231
|
}
|
|
1997
2232
|
/**
|
|
1998
|
-
* Accepts an array of `{ label, value }` and stores it as
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2001
|
-
* re-render via `attributeChangedCallback`.
|
|
2233
|
+
* Accepts an array of `{ label, value }` (or a JSON string) and stores it as
|
|
2234
|
+
* the source of truth. Mirrors `<ody-dropdown-*>` so `el.options = [...]`
|
|
2235
|
+
* works, without reflecting a large JSON blob onto the DOM attribute.
|
|
2002
2236
|
*/
|
|
2003
2237
|
set options(next) {
|
|
2004
|
-
this
|
|
2238
|
+
this.#options = coerceOptions2(next);
|
|
2239
|
+
if (this.querySelector(".ody-checkbox-group")) this.render();
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Reflect a `value` change by updating each checkbox (and the select-all
|
|
2243
|
+
* parent) in place, so toggling an option keeps focus instead of rebuilding
|
|
2244
|
+
* the group. Other observed attributes still re-render via the base.
|
|
2245
|
+
*/
|
|
2246
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2247
|
+
if (name === "value" && this.querySelector(".ody-checkbox-group")) {
|
|
2248
|
+
if (oldValue === newValue) return;
|
|
2249
|
+
this.#syncSelection();
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
super.attributeChangedCallback();
|
|
2253
|
+
}
|
|
2254
|
+
/** Recompute checked/indeterminate state for every checkbox in place. */
|
|
2255
|
+
#syncSelection() {
|
|
2256
|
+
const options = this.options;
|
|
2257
|
+
const selected = new Set(this.value);
|
|
2258
|
+
const allChecked = options.length > 0 && options.every((o) => selected.has(o.value));
|
|
2259
|
+
const someChecked = !allChecked && options.some((o) => selected.has(o.value));
|
|
2260
|
+
for (const cb of this.querySelectorAll(".ody-checkbox-group__item .ody-checkbox")) {
|
|
2261
|
+
const input = cb.querySelector(".ody-checkbox__input");
|
|
2262
|
+
applyCheckboxState(cb, input ? selected.has(input.value) : false, false);
|
|
2263
|
+
}
|
|
2264
|
+
applyCheckboxState(this.querySelector(".ody-checkbox-group__select-all"), allChecked, someChecked);
|
|
2005
2265
|
}
|
|
2006
2266
|
render() {
|
|
2007
2267
|
const size = this.attr("size", "small");
|
|
@@ -2067,6 +2327,11 @@ var OdyCheckboxGroup = class extends OdyElement {
|
|
|
2067
2327
|
this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value }, bubbles: true }));
|
|
2068
2328
|
}
|
|
2069
2329
|
};
|
|
2330
|
+
function coerceOptions2(next) {
|
|
2331
|
+
if (Array.isArray(next)) return next;
|
|
2332
|
+
if (typeof next === "string") return parseOptions2(next);
|
|
2333
|
+
return [];
|
|
2334
|
+
}
|
|
2070
2335
|
function parseOptions2(raw) {
|
|
2071
2336
|
if (!raw) return [];
|
|
2072
2337
|
try {
|
|
@@ -2246,6 +2511,11 @@ var OdyCopyButton = class extends OdyElement {
|
|
|
2246
2511
|
if (this.isConnected) this.render();
|
|
2247
2512
|
}, Number.isFinite(duration) ? duration : 1200);
|
|
2248
2513
|
}
|
|
2514
|
+
disconnectedCallback() {
|
|
2515
|
+
super.disconnectedCallback();
|
|
2516
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
2517
|
+
this.#timer = void 0;
|
|
2518
|
+
}
|
|
2249
2519
|
};
|
|
2250
2520
|
define("ody-copy-button", OdyCopyButton);
|
|
2251
2521
|
|
|
@@ -2455,7 +2725,7 @@ var OdyModal = class extends OdyElement {
|
|
|
2455
2725
|
this.dispatchEvent(new CustomEvent(EVENT_CLOSE, { bubbles: true }));
|
|
2456
2726
|
}
|
|
2457
2727
|
render() {
|
|
2458
|
-
if (this.#dialog) this.
|
|
2728
|
+
if (this.#dialog) this.adopt(this.#dialog);
|
|
2459
2729
|
if (this.#backdrop) removePortal(this.#backdrop);
|
|
2460
2730
|
const isOpen = this.flag("open");
|
|
2461
2731
|
const size = this.attr("size", "base");
|
|
@@ -2488,6 +2758,7 @@ var OdyModal = class extends OdyElement {
|
|
|
2488
2758
|
disconnectedCallback() {
|
|
2489
2759
|
super.disconnectedCallback();
|
|
2490
2760
|
document.removeEventListener("keydown", this.#onKeydown);
|
|
2761
|
+
this.reclaimPortaledSlot(this.#dialog);
|
|
2491
2762
|
if (this.#backdrop) removePortal(this.#backdrop);
|
|
2492
2763
|
if (this.#dialog) removePortal(this.#dialog);
|
|
2493
2764
|
this.#backdrop = null;
|
|
@@ -2550,7 +2821,7 @@ var OdyPopover = class extends OdyElement {
|
|
|
2550
2821
|
if (this.#visible) this.close();
|
|
2551
2822
|
this.#trigger?.removeEventListener("click", this.#onTriggerClick);
|
|
2552
2823
|
if (this.#panel) {
|
|
2553
|
-
this.
|
|
2824
|
+
this.adopt(this.#panel);
|
|
2554
2825
|
this.#panel.classList.remove("ody-popover__container--open");
|
|
2555
2826
|
}
|
|
2556
2827
|
const trigger = this.querySelector("[data-ody-popover-trigger-slot]")?.firstElementChild;
|
|
@@ -2581,6 +2852,7 @@ var OdyPopover = class extends OdyElement {
|
|
|
2581
2852
|
super.disconnectedCallback();
|
|
2582
2853
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
2583
2854
|
this.#trigger?.removeEventListener("click", this.#onTriggerClick);
|
|
2855
|
+
this.reclaimPortaledSlot(this.#panel);
|
|
2584
2856
|
if (this.#panel) removePortal(this.#panel);
|
|
2585
2857
|
this.#panel = null;
|
|
2586
2858
|
this.#trigger = null;
|
|
@@ -2625,7 +2897,7 @@ var OdyTooltip = class extends OdyElement {
|
|
|
2625
2897
|
if (this.#visible) this.hide();
|
|
2626
2898
|
this.#unbind();
|
|
2627
2899
|
if (this.#bubble) {
|
|
2628
|
-
this.
|
|
2900
|
+
this.adopt(this.#bubble);
|
|
2629
2901
|
this.#bubble.classList.remove("ody-tooltip--visible");
|
|
2630
2902
|
}
|
|
2631
2903
|
const slotted = this.querySelector("[data-ody-tooltip-trigger-slot]")?.firstElementChild;
|
|
@@ -2667,6 +2939,7 @@ var OdyTooltip = class extends OdyElement {
|
|
|
2667
2939
|
disconnectedCallback() {
|
|
2668
2940
|
super.disconnectedCallback();
|
|
2669
2941
|
this.#unbind();
|
|
2942
|
+
this.reclaimPortaledSlot(this.#bubble);
|
|
2670
2943
|
if (this.#bubble) removePortal(this.#bubble);
|
|
2671
2944
|
this.#bubble = null;
|
|
2672
2945
|
this.#trigger = null;
|
|
@@ -2690,7 +2963,7 @@ var OdyPanel = class extends OdyElement {
|
|
|
2690
2963
|
this.dispatchEvent(new CustomEvent(EVENT_CLOSE3, { bubbles: true }));
|
|
2691
2964
|
}
|
|
2692
2965
|
render() {
|
|
2693
|
-
if (this.#panel) this.
|
|
2966
|
+
if (this.#panel) this.adopt(this.#panel);
|
|
2694
2967
|
if (this.#overlay) removePortal(this.#overlay);
|
|
2695
2968
|
const isOpen = this.flag("open");
|
|
2696
2969
|
const heading = this.attr("heading");
|
|
@@ -2711,6 +2984,7 @@ var OdyPanel = class extends OdyElement {
|
|
|
2711
2984
|
};
|
|
2712
2985
|
disconnectedCallback() {
|
|
2713
2986
|
super.disconnectedCallback();
|
|
2987
|
+
this.reclaimPortaledSlot(this.#panel);
|
|
2714
2988
|
if (this.#panel) removePortal(this.#panel);
|
|
2715
2989
|
if (this.#overlay) removePortal(this.#overlay);
|
|
2716
2990
|
this.#panel = null;
|
|
@@ -2730,6 +3004,8 @@ var HOST_TAG = "ody-toast-host";
|
|
|
2730
3004
|
var DEFAULT_TIMEOUT = 4e3;
|
|
2731
3005
|
var REMOVE_DELAY = 400;
|
|
2732
3006
|
var OdyToastHost = class extends OdyElement {
|
|
3007
|
+
/** Per-toast cleanups (cancel rAF + auto-dismiss timer) run on host teardown. */
|
|
3008
|
+
#pending = /* @__PURE__ */ new Set();
|
|
2733
3009
|
render() {
|
|
2734
3010
|
this.mount(`<div class="toast-notification-container" data-ody-slot></div>`);
|
|
2735
3011
|
}
|
|
@@ -2744,17 +3020,28 @@ var OdyToastHost = class extends OdyElement {
|
|
|
2744
3020
|
const title = options.title ? `<h1 class="toast-notification__title">${this.esc(options.title)}</h1>` : "";
|
|
2745
3021
|
el.innerHTML = `<div class="toast-notification__header__container"><div class="toast-notification__header"><span class="toast-notification__icon icon">${iconSvg(VARIANT_ICON3[variant], "icon__svg")}</span>` + title + `</div><button type="button" class="toast-notification__close-button" aria-label="${this.localized("close-label", "close")}">${iconSvg("close", "icon__svg")}</button></div><div class="toast-notification__body__container"><p class="toast-notification__body">${this.esc(message)}</p></div>`;
|
|
2746
3022
|
container.appendChild(el);
|
|
2747
|
-
requestAnimationFrame(() => el.classList.add("toast-notification--visible"));
|
|
3023
|
+
const raf = requestAnimationFrame(() => el.classList.add("toast-notification--visible"));
|
|
2748
3024
|
let timer;
|
|
2749
|
-
const
|
|
3025
|
+
const cleanup = () => {
|
|
3026
|
+
cancelAnimationFrame(raf);
|
|
2750
3027
|
if (timer !== void 0) clearTimeout(timer);
|
|
3028
|
+
this.#pending.delete(cleanup);
|
|
3029
|
+
};
|
|
3030
|
+
const dismiss = () => {
|
|
3031
|
+
cleanup();
|
|
2751
3032
|
el.classList.remove("toast-notification--visible");
|
|
2752
3033
|
setTimeout(() => el.remove(), REMOVE_DELAY);
|
|
2753
3034
|
};
|
|
2754
3035
|
el.querySelector(".toast-notification__close-button")?.addEventListener("click", dismiss);
|
|
2755
3036
|
if (timeout > 0) timer = setTimeout(dismiss, timeout);
|
|
3037
|
+
this.#pending.add(cleanup);
|
|
2756
3038
|
return { dismiss };
|
|
2757
3039
|
}
|
|
3040
|
+
disconnectedCallback() {
|
|
3041
|
+
super.disconnectedCallback();
|
|
3042
|
+
for (const cleanup of [...this.#pending]) cleanup();
|
|
3043
|
+
this.#pending.clear();
|
|
3044
|
+
}
|
|
2758
3045
|
};
|
|
2759
3046
|
define(HOST_TAG, OdyToastHost);
|
|
2760
3047
|
var hostEl = null;
|
|
@@ -2811,6 +3098,10 @@ var OdySelectBase = class extends OdyElement {
|
|
|
2811
3098
|
if (!q) return this.options;
|
|
2812
3099
|
return this.options.filter((o) => o.label.toLowerCase().includes(q));
|
|
2813
3100
|
}
|
|
3101
|
+
connectedCallback() {
|
|
3102
|
+
super.connectedCallback();
|
|
3103
|
+
if (this.open) document.addEventListener("click", this.#onOutsideClick, true);
|
|
3104
|
+
}
|
|
2814
3105
|
// ---- Rendering -----------------------------------------------------------
|
|
2815
3106
|
render() {
|
|
2816
3107
|
const listId = `${this.uid}-listbox`;
|
|
@@ -3178,10 +3469,34 @@ var OdyDatePicker = class extends OdyElement {
|
|
|
3178
3469
|
"presets",
|
|
3179
3470
|
"display-format"
|
|
3180
3471
|
];
|
|
3472
|
+
/** Presets set via the JS property; when set it wins over the attribute. */
|
|
3473
|
+
#presets = null;
|
|
3474
|
+
#isDateDisallowed;
|
|
3475
|
+
#formatDate;
|
|
3476
|
+
/** Preset list: the JS property wins, else the JSON `presets` attribute. */
|
|
3477
|
+
get presets() {
|
|
3478
|
+
return this.#presets ?? this.#parsePresets();
|
|
3479
|
+
}
|
|
3480
|
+
set presets(next) {
|
|
3481
|
+
this.#presets = coerceDatePresets(next);
|
|
3482
|
+
if (this.querySelector(".ody-datepicker__trigger")) this.render();
|
|
3483
|
+
}
|
|
3181
3484
|
/** Disable arbitrary days; not an attribute (functions can't serialize). */
|
|
3182
|
-
isDateDisallowed
|
|
3485
|
+
get isDateDisallowed() {
|
|
3486
|
+
return this.#isDateDisallowed;
|
|
3487
|
+
}
|
|
3488
|
+
set isDateDisallowed(fn) {
|
|
3489
|
+
this.#isDateDisallowed = fn;
|
|
3490
|
+
if (this.querySelector(".ody-datepicker__trigger")) this.render();
|
|
3491
|
+
}
|
|
3183
3492
|
/** Custom trigger-label formatter; wins over `display-format`/`Intl`. */
|
|
3184
|
-
formatDate
|
|
3493
|
+
get formatDate() {
|
|
3494
|
+
return this.#formatDate;
|
|
3495
|
+
}
|
|
3496
|
+
set formatDate(fn) {
|
|
3497
|
+
this.#formatDate = fn;
|
|
3498
|
+
if (this.querySelector(".ody-datepicker__trigger")) this.render();
|
|
3499
|
+
}
|
|
3185
3500
|
#open = false;
|
|
3186
3501
|
/** First day of the currently displayed month (local). */
|
|
3187
3502
|
#viewDate = /* @__PURE__ */ new Date();
|
|
@@ -3512,7 +3827,7 @@ var OdyDatePicker = class extends OdyElement {
|
|
|
3512
3827
|
return rows.join("");
|
|
3513
3828
|
}
|
|
3514
3829
|
#renderPresets() {
|
|
3515
|
-
const presets = this
|
|
3830
|
+
const presets = this.presets;
|
|
3516
3831
|
if (presets.length === 0) return "";
|
|
3517
3832
|
const buttons = presets.map(
|
|
3518
3833
|
(p) => `<button type="button" class="ody-datepicker__preset" data-value="${this.esc(p.value)}">${this.esc(p.label)}</button>`
|
|
@@ -3554,6 +3869,18 @@ var OdyDatePicker = class extends OdyElement {
|
|
|
3554
3869
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
3555
3870
|
}
|
|
3556
3871
|
};
|
|
3872
|
+
function coerceDatePresets(next) {
|
|
3873
|
+
if (Array.isArray(next)) return next;
|
|
3874
|
+
if (typeof next === "string" && next) {
|
|
3875
|
+
try {
|
|
3876
|
+
const parsed = JSON.parse(next);
|
|
3877
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3878
|
+
} catch {
|
|
3879
|
+
return [];
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
return [];
|
|
3883
|
+
}
|
|
3557
3884
|
define("ody-datepicker", OdyDatePicker);
|
|
3558
3885
|
|
|
3559
3886
|
// src/ui/components/table.ts
|