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