@rogieking/figui3 8.0.3 → 8.2.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 +164 -0
- package/components.css +2 -0
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +17 -17
- package/dist/fig.css +1 -1
- package/dist/fig.js +1 -1
- package/fig-editor.css +0 -1
- package/fig-lab.css +134 -17
- package/fig-lab.js +1771 -177
- package/fig.js +1 -0
- package/package.json +1 -1
- package/dist/editor.js +0 -10718
package/fig-lab.js
CHANGED
|
@@ -18,6 +18,13 @@ function figLabBooleanAttribute(element, name) {
|
|
|
18
18
|
return element.hasAttribute(name) && element.getAttribute(name) !== "false";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function figLabSyncDisabledControls(host, controls) {
|
|
22
|
+
const disabled = figLabBooleanAttribute(host, "disabled");
|
|
23
|
+
for (const control of controls) {
|
|
24
|
+
control?.toggleAttribute("disabled", disabled);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
function figLabColorEventAliases(color, alpha, opacity) {
|
|
22
29
|
const numericAlpha = Number(alpha);
|
|
23
30
|
const numericOpacity = Number(opacity);
|
|
@@ -566,6 +573,7 @@ class PropskitSwitch extends HTMLElement {
|
|
|
566
573
|
|
|
567
574
|
#forwardSwitchEvent(type, event) {
|
|
568
575
|
event.stopImmediatePropagation();
|
|
576
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
569
577
|
const checked = this.#switch?.value === "on";
|
|
570
578
|
this.toggleAttribute("checked", checked);
|
|
571
579
|
const detail = {
|
|
@@ -583,6 +591,7 @@ class PropskitSwitch extends HTMLElement {
|
|
|
583
591
|
}
|
|
584
592
|
|
|
585
593
|
#handleClick(event) {
|
|
594
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
586
595
|
if (
|
|
587
596
|
event.target instanceof Element &&
|
|
588
597
|
event.target.closest("fig-segmented-control, fig-menu")
|
|
@@ -845,6 +854,7 @@ class PropskitColor extends HTMLElement {
|
|
|
845
854
|
|
|
846
855
|
#forwardInputEvent(type, event) {
|
|
847
856
|
event.stopImmediatePropagation();
|
|
857
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
848
858
|
const value = this.#valueFromColorEvent(event);
|
|
849
859
|
this.setAttribute("value", value);
|
|
850
860
|
if (this.#input && this.#input.getAttribute("value") !== value) {
|
|
@@ -865,6 +875,7 @@ class PropskitColor extends HTMLElement {
|
|
|
865
875
|
}
|
|
866
876
|
|
|
867
877
|
#handleClick(event) {
|
|
878
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
868
879
|
if (
|
|
869
880
|
event.target instanceof Element &&
|
|
870
881
|
event.target.closest("fig-input-color, fig-menu")
|
|
@@ -1131,6 +1142,7 @@ class PropskitGradient extends HTMLElement {
|
|
|
1131
1142
|
|
|
1132
1143
|
#forwardInputEvent(type, event) {
|
|
1133
1144
|
event.stopImmediatePropagation();
|
|
1145
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1134
1146
|
const value = this.#valueFromGradientEvent(event);
|
|
1135
1147
|
this.setAttribute("value", value);
|
|
1136
1148
|
const detail =
|
|
@@ -1148,6 +1160,7 @@ class PropskitGradient extends HTMLElement {
|
|
|
1148
1160
|
}
|
|
1149
1161
|
|
|
1150
1162
|
#handleClick(event) {
|
|
1163
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1151
1164
|
if (
|
|
1152
1165
|
event.target instanceof Element &&
|
|
1153
1166
|
event.target.closest("fig-input-gradient, fig-menu")
|
|
@@ -1481,6 +1494,7 @@ class PropskitSelect extends HTMLElement {
|
|
|
1481
1494
|
#forwardSelectEvent(type, event) {
|
|
1482
1495
|
if (event.target !== this.#select) return;
|
|
1483
1496
|
event.stopImmediatePropagation();
|
|
1497
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1484
1498
|
const value = this.#select?.value ?? "";
|
|
1485
1499
|
if (type !== "optionhover") this.setAttribute("value", String(value));
|
|
1486
1500
|
const detail =
|
|
@@ -1520,6 +1534,7 @@ class PropskitSelect extends HTMLElement {
|
|
|
1520
1534
|
}
|
|
1521
1535
|
|
|
1522
1536
|
#handleClick(event) {
|
|
1537
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1523
1538
|
if (event.target instanceof Element && event.target.closest("fig-menu")) {
|
|
1524
1539
|
return;
|
|
1525
1540
|
}
|
|
@@ -1753,6 +1768,7 @@ class PropskitText extends HTMLElement {
|
|
|
1753
1768
|
|
|
1754
1769
|
#forwardInputEvent(type, event) {
|
|
1755
1770
|
event.stopImmediatePropagation();
|
|
1771
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1756
1772
|
const value = this.#input?.value ?? "";
|
|
1757
1773
|
this.setAttribute("value", String(value));
|
|
1758
1774
|
const detail =
|
|
@@ -1770,6 +1786,7 @@ class PropskitText extends HTMLElement {
|
|
|
1770
1786
|
}
|
|
1771
1787
|
|
|
1772
1788
|
#handleClick(event) {
|
|
1789
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1773
1790
|
if (
|
|
1774
1791
|
event.target instanceof Element &&
|
|
1775
1792
|
event.target.closest("fig-input-text, fig-menu")
|
|
@@ -1978,6 +1995,7 @@ class PropskitNumber extends HTMLElement {
|
|
|
1978
1995
|
|
|
1979
1996
|
#forwardInputEvent(type, event) {
|
|
1980
1997
|
event.stopImmediatePropagation();
|
|
1998
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1981
1999
|
const detail =
|
|
1982
2000
|
event instanceof CustomEvent && event.detail !== undefined
|
|
1983
2001
|
? event.detail
|
|
@@ -1996,6 +2014,7 @@ class PropskitNumber extends HTMLElement {
|
|
|
1996
2014
|
}
|
|
1997
2015
|
|
|
1998
2016
|
#handleClick(event) {
|
|
2017
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1999
2018
|
if (
|
|
2000
2019
|
event.target instanceof Element &&
|
|
2001
2020
|
event.target.closest("fig-input-number, fig-menu")
|
|
@@ -2040,233 +2059,1793 @@ class PropskitNumber extends HTMLElement {
|
|
|
2040
2059
|
}
|
|
2041
2060
|
figLabDefineElement("propskit-number", PropskitNumber);
|
|
2042
2061
|
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2062
|
+
/**
|
|
2063
|
+
* Compact X/Y editor.
|
|
2064
|
+
*
|
|
2065
|
+
* @attr {number} x - Horizontal value.
|
|
2066
|
+
* @attr {number} y - Vertical value.
|
|
2067
|
+
* @attr {string} default - JSON reset value with x and y.
|
|
2068
|
+
* @attr {string} label - Field label. Empty values use "Position".
|
|
2069
|
+
* @attr {string} units - Set to "percent" to show percentage units.
|
|
2070
|
+
* @attr {boolean|string} disabled - Disables both number inputs.
|
|
2071
|
+
* @attr {string} size - Set to "large" for the expanded row.
|
|
2072
|
+
* @fires input - Composed event with { x, y }.
|
|
2073
|
+
* @fires change - Composed event with { x, y }.
|
|
2074
|
+
*/
|
|
2075
|
+
class PropskitPosition extends HTMLElement {
|
|
2076
|
+
static observedAttributes = [
|
|
2077
|
+
"x",
|
|
2078
|
+
"y",
|
|
2079
|
+
"default",
|
|
2080
|
+
"label",
|
|
2081
|
+
"units",
|
|
2082
|
+
"disabled",
|
|
2083
|
+
"size",
|
|
2084
|
+
];
|
|
2057
2085
|
|
|
2058
|
-
#
|
|
2059
|
-
#
|
|
2060
|
-
#
|
|
2061
|
-
#
|
|
2062
|
-
#
|
|
2063
|
-
#
|
|
2086
|
+
#field = null;
|
|
2087
|
+
#xInput = null;
|
|
2088
|
+
#yInput = null;
|
|
2089
|
+
#initialValue = null;
|
|
2090
|
+
#initialized = false;
|
|
2091
|
+
#reflecting = false;
|
|
2092
|
+
#boundHandleInput = this.#handleInputEvent.bind(this, "input");
|
|
2093
|
+
#boundHandleChange = this.#handleInputEvent.bind(this, "change");
|
|
2094
|
+
#boundHandleClick = this.#handleClick.bind(this);
|
|
2064
2095
|
|
|
2065
2096
|
connectedCallback() {
|
|
2066
|
-
this.#
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
this.#
|
|
2070
|
-
}
|
|
2097
|
+
if (!this.#initialized) {
|
|
2098
|
+
this.#initialValue = this.#readValue();
|
|
2099
|
+
this.#reflectValue(this.#initialValue);
|
|
2100
|
+
this.#initialized = true;
|
|
2101
|
+
}
|
|
2102
|
+
if (!this.#field) this.#render();
|
|
2103
|
+
this.#syncLabel();
|
|
2104
|
+
this.#syncInputs();
|
|
2105
|
+
this.#bindEvents();
|
|
2106
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
2107
|
+
this.addEventListener("click", this.#boundHandleClick);
|
|
2108
|
+
figLabConnectPropskitResetMenu(this);
|
|
2071
2109
|
}
|
|
2072
2110
|
|
|
2073
2111
|
disconnectedCallback() {
|
|
2074
|
-
this.#
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
this.#dirtyFrame = 0;
|
|
2078
|
-
}
|
|
2079
|
-
this.#header?.removeEventListener("click", this.#handleToggle);
|
|
2080
|
-
this.#header?.removeEventListener("keydown", this.#handleHeaderKeyDown);
|
|
2081
|
-
const resetBtn = this.#resetTooltip?.querySelector("fig-button");
|
|
2082
|
-
resetBtn?.removeEventListener("click", this.#handleReset);
|
|
2112
|
+
this.#unbindEvents();
|
|
2113
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
2114
|
+
figLabDisconnectPropskitResetMenu(this);
|
|
2083
2115
|
}
|
|
2084
2116
|
|
|
2085
2117
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
2086
|
-
if (oldValue === newValue) return;
|
|
2087
|
-
if (name === "
|
|
2088
|
-
this.#
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
if (name === "
|
|
2092
|
-
this.#
|
|
2093
|
-
|
|
2094
|
-
|
|
2118
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
2119
|
+
if ((name === "x" || name === "y") && !this.#reflecting) {
|
|
2120
|
+
this.#syncInputs();
|
|
2121
|
+
} else if (name === "label") {
|
|
2122
|
+
this.#syncLabel();
|
|
2123
|
+
} else if (name === "units") {
|
|
2124
|
+
this.#syncUnits();
|
|
2125
|
+
} else if (name === "disabled") {
|
|
2126
|
+
this.#syncDisabled();
|
|
2095
2127
|
}
|
|
2096
|
-
this.#render();
|
|
2097
2128
|
}
|
|
2098
2129
|
|
|
2099
|
-
|
|
2100
|
-
const
|
|
2101
|
-
return
|
|
2130
|
+
#finiteNumber(value, fallback = 50) {
|
|
2131
|
+
const number = Number(value);
|
|
2132
|
+
return Number.isFinite(number) ? number : fallback;
|
|
2102
2133
|
}
|
|
2103
2134
|
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2135
|
+
#readValue() {
|
|
2136
|
+
const x = this.getAttribute("x");
|
|
2137
|
+
const y = this.getAttribute("y");
|
|
2138
|
+
return {
|
|
2139
|
+
x: x === null || !x.trim() ? 50 : this.#finiteNumber(x, 50),
|
|
2140
|
+
y: y === null || !y.trim() ? 50 : this.#finiteNumber(y, 50),
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
#parseDefault(value) {
|
|
2145
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
2146
|
+
try {
|
|
2147
|
+
const parsed = JSON.parse(value);
|
|
2148
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
return {
|
|
2152
|
+
x: this.#finiteNumber(parsed.x, this.#initialValue?.x ?? 50),
|
|
2153
|
+
y: this.#finiteNumber(parsed.y, this.#initialValue?.y ?? 50),
|
|
2154
|
+
};
|
|
2155
|
+
} catch {
|
|
2156
|
+
return null;
|
|
2119
2157
|
}
|
|
2120
2158
|
}
|
|
2121
2159
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2160
|
+
#reflectValue(value) {
|
|
2161
|
+
const current = this.#readValue();
|
|
2162
|
+
const normalized = {
|
|
2163
|
+
x: this.#finiteNumber(value?.x, current.x),
|
|
2164
|
+
y: this.#finiteNumber(value?.y, current.y),
|
|
2165
|
+
};
|
|
2166
|
+
this.#reflecting = true;
|
|
2167
|
+
this.setAttribute("x", String(normalized.x));
|
|
2168
|
+
this.setAttribute("y", String(normalized.y));
|
|
2169
|
+
this.#reflecting = false;
|
|
2170
|
+
return normalized;
|
|
2127
2171
|
}
|
|
2128
2172
|
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2173
|
+
#createNumber(axis, value) {
|
|
2174
|
+
const input = document.createElement("fig-input-number");
|
|
2175
|
+
input.setAttribute("data-propskit-position-axis", axis);
|
|
2176
|
+
input.setAttribute("value", String(value));
|
|
2177
|
+
input.setAttribute("min", "0");
|
|
2178
|
+
input.setAttribute("max", "100");
|
|
2179
|
+
input.setAttribute("step", "1");
|
|
2180
|
+
input.setAttribute("precision", "2");
|
|
2181
|
+
if (this.units === "percent") input.setAttribute("units", "%");
|
|
2182
|
+
input.setAttribute("aria-label", `${axis.toUpperCase()} position`);
|
|
2183
|
+
const prepend = document.createElement("span");
|
|
2184
|
+
prepend.setAttribute("slot", "prepend");
|
|
2185
|
+
prepend.textContent = axis.toUpperCase();
|
|
2186
|
+
input.append(prepend);
|
|
2187
|
+
if (figLabBooleanAttribute(this, "disabled")) {
|
|
2188
|
+
input.setAttribute("disabled", "");
|
|
2189
|
+
}
|
|
2190
|
+
return input;
|
|
2132
2191
|
}
|
|
2133
2192
|
|
|
2134
|
-
|
|
2135
|
-
|
|
2193
|
+
#render() {
|
|
2194
|
+
const value = this.#readValue();
|
|
2195
|
+
const field = document.createElement("fig-field");
|
|
2196
|
+
field.className = "propskit-position-row";
|
|
2197
|
+
field.setAttribute("direction", "horizontal");
|
|
2198
|
+
const label = document.createElement("label");
|
|
2199
|
+
const xInput = this.#createNumber("x", value.x);
|
|
2200
|
+
const yInput = this.#createNumber("y", value.y);
|
|
2201
|
+
field.append(label, xInput, yInput);
|
|
2202
|
+
this.#field = field;
|
|
2203
|
+
this.#xInput = xInput;
|
|
2204
|
+
this.#yInput = yInput;
|
|
2205
|
+
this.replaceChildren(field);
|
|
2206
|
+
figLabConnectPropskitResetMenu(this);
|
|
2136
2207
|
}
|
|
2137
2208
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2209
|
+
#syncLabel() {
|
|
2210
|
+
const label = this.#field?.querySelector(":scope > label");
|
|
2211
|
+
if (!label) return;
|
|
2212
|
+
const rawLabel = this.getAttribute("label");
|
|
2213
|
+
label.textContent = rawLabel?.trim() || "Position";
|
|
2141
2214
|
}
|
|
2142
2215
|
|
|
2143
|
-
#
|
|
2144
|
-
|
|
2145
|
-
this
|
|
2146
|
-
this
|
|
2147
|
-
this
|
|
2216
|
+
#syncInputs() {
|
|
2217
|
+
const value = this.#readValue();
|
|
2218
|
+
this.#xInput?.setAttribute("value", String(value.x));
|
|
2219
|
+
this.#yInput?.setAttribute("value", String(value.y));
|
|
2220
|
+
this.#syncUnits();
|
|
2221
|
+
this.#syncDisabled();
|
|
2222
|
+
}
|
|
2148
2223
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2224
|
+
#syncUnits() {
|
|
2225
|
+
const units = this.units === "percent" ? "%" : null;
|
|
2226
|
+
for (const input of [this.#xInput, this.#yInput]) {
|
|
2227
|
+
if (!input) continue;
|
|
2228
|
+
if (units) input.setAttribute("units", units);
|
|
2229
|
+
else input.removeAttribute("units");
|
|
2153
2230
|
}
|
|
2154
|
-
this.#childObserver.disconnect();
|
|
2155
|
-
this.#childObserver.observe(this, {
|
|
2156
|
-
childList: true,
|
|
2157
|
-
subtree: true,
|
|
2158
|
-
attributes: true,
|
|
2159
|
-
attributeFilter: ["value", "checked", "default"],
|
|
2160
|
-
});
|
|
2161
2231
|
}
|
|
2162
2232
|
|
|
2163
|
-
#
|
|
2164
|
-
|
|
2165
|
-
this
|
|
2166
|
-
this.#
|
|
2233
|
+
#syncDisabled() {
|
|
2234
|
+
const disabled = figLabBooleanAttribute(this, "disabled");
|
|
2235
|
+
this.#xInput?.toggleAttribute("disabled", disabled);
|
|
2236
|
+
this.#yInput?.toggleAttribute("disabled", disabled);
|
|
2167
2237
|
}
|
|
2168
2238
|
|
|
2169
|
-
#
|
|
2170
|
-
|
|
2171
|
-
this.#
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2239
|
+
#bindEvents() {
|
|
2240
|
+
this.#unbindEvents();
|
|
2241
|
+
this.#xInput?.addEventListener("input", this.#boundHandleInput);
|
|
2242
|
+
this.#xInput?.addEventListener("change", this.#boundHandleChange);
|
|
2243
|
+
this.#yInput?.addEventListener("input", this.#boundHandleInput);
|
|
2244
|
+
this.#yInput?.addEventListener("change", this.#boundHandleChange);
|
|
2175
2245
|
}
|
|
2176
2246
|
|
|
2177
|
-
#
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2247
|
+
#unbindEvents() {
|
|
2248
|
+
this.#xInput?.removeEventListener("input", this.#boundHandleInput);
|
|
2249
|
+
this.#xInput?.removeEventListener("change", this.#boundHandleChange);
|
|
2250
|
+
this.#yInput?.removeEventListener("input", this.#boundHandleInput);
|
|
2251
|
+
this.#yInput?.removeEventListener("change", this.#boundHandleChange);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
#handleInputEvent(type, event) {
|
|
2255
|
+
event.stopImmediatePropagation();
|
|
2256
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
2257
|
+
const axis = event.currentTarget?.getAttribute("data-propskit-position-axis");
|
|
2258
|
+
if (axis !== "x" && axis !== "y") return;
|
|
2259
|
+
const value = this.value;
|
|
2260
|
+
value[axis] = this.#finiteNumber(event.currentTarget.value, value[axis]);
|
|
2261
|
+
const detail = this.#reflectValue(value);
|
|
2262
|
+
this.dispatchEvent(
|
|
2263
|
+
new CustomEvent(type, {
|
|
2264
|
+
detail: { ...detail, units: this.units },
|
|
2265
|
+
bubbles: true,
|
|
2266
|
+
cancelable: true,
|
|
2267
|
+
composed: true,
|
|
2268
|
+
}),
|
|
2185
2269
|
);
|
|
2186
2270
|
}
|
|
2187
2271
|
|
|
2188
|
-
#
|
|
2189
|
-
if (this
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2272
|
+
#handleClick(event) {
|
|
2273
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
2274
|
+
if (
|
|
2275
|
+
event.target instanceof Element &&
|
|
2276
|
+
event.target.closest("fig-input-number, fig-menu")
|
|
2277
|
+
) {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
this.focus();
|
|
2281
|
+
}
|
|
2193
2282
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
e.preventDefault();
|
|
2198
|
-
e.stopPropagation();
|
|
2199
|
-
this.open = !this.open;
|
|
2200
|
-
};
|
|
2283
|
+
get x() {
|
|
2284
|
+
return this.#readValue().x;
|
|
2285
|
+
}
|
|
2201
2286
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
this.#resetControls();
|
|
2206
|
-
};
|
|
2287
|
+
set x(value) {
|
|
2288
|
+
this.setAttribute("x", String(this.#finiteNumber(value, 50)));
|
|
2289
|
+
}
|
|
2207
2290
|
|
|
2208
|
-
|
|
2209
|
-
return
|
|
2210
|
-
...this.querySelectorAll(PropskitGroup.#CONTROL_SELECTOR),
|
|
2211
|
-
].filter(
|
|
2212
|
-
(el) =>
|
|
2213
|
-
el.closest("propskit-group") === this &&
|
|
2214
|
-
!el.parentElement?.closest(PropskitGroup.#CONTROL_SELECTOR),
|
|
2215
|
-
);
|
|
2291
|
+
get y() {
|
|
2292
|
+
return this.#readValue().y;
|
|
2216
2293
|
}
|
|
2217
2294
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2295
|
+
set y(value) {
|
|
2296
|
+
this.setAttribute("y", String(this.#finiteNumber(value, 50)));
|
|
2220
2297
|
}
|
|
2221
2298
|
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
return false;
|
|
2299
|
+
get units() {
|
|
2300
|
+
return this.getAttribute("units")?.trim().toLowerCase() === "percent"
|
|
2301
|
+
? "percent"
|
|
2302
|
+
: "none";
|
|
2227
2303
|
}
|
|
2228
2304
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2305
|
+
set units(value) {
|
|
2306
|
+
this.setAttribute(
|
|
2307
|
+
"units",
|
|
2308
|
+
String(value).trim().toLowerCase() === "none" ? "none" : "percent",
|
|
2309
|
+
);
|
|
2234
2310
|
}
|
|
2235
2311
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
el.resetToDefault();
|
|
2239
|
-
}
|
|
2312
|
+
get value() {
|
|
2313
|
+
return { ...this.#readValue() };
|
|
2240
2314
|
}
|
|
2241
2315
|
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
this.#
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2316
|
+
set value(value) {
|
|
2317
|
+
const normalized = this.#reflectValue(value);
|
|
2318
|
+
this.#syncInputs(normalized);
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
get defaultValue() {
|
|
2322
|
+
return (
|
|
2323
|
+
this.#parseDefault(this.getAttribute("default")) || {
|
|
2324
|
+
...(this.#initialValue || { x: 50, y: 50 }),
|
|
2325
|
+
}
|
|
2250
2326
|
);
|
|
2251
2327
|
}
|
|
2252
2328
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
this.#resetTooltip?.setAttribute("hidden", "");
|
|
2256
|
-
return;
|
|
2257
|
-
}
|
|
2258
|
-
this.#ensureResetButton();
|
|
2259
|
-
this.#resetTooltip?.removeAttribute("hidden");
|
|
2329
|
+
get isDefault() {
|
|
2330
|
+
return figLabPropskitJsonValuesEqual(this.value, this.defaultValue);
|
|
2260
2331
|
}
|
|
2261
2332
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2333
|
+
resetToDefault() {
|
|
2334
|
+
const value = this.defaultValue;
|
|
2335
|
+
this.value = value;
|
|
2336
|
+
figLabEmitPropskitReset(this, { ...value, units: this.units });
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
focus(options) {
|
|
2340
|
+
this.#xInput?.focus(options);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
figLabDefineElement("propskit-position", PropskitPosition);
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* Collapsible color-point group composed from color and position controls.
|
|
2347
|
+
*
|
|
2348
|
+
* @attr {string} label - Passed to the internal fig-group name.
|
|
2349
|
+
* @attr {boolean|string} collapsible - Internal group collapsibility; defaults true.
|
|
2350
|
+
* @attr {boolean|string} open - Internal group expanded state; defaults true.
|
|
2351
|
+
* @attr {string} size - Passed to both internal PropsKit controls.
|
|
2352
|
+
* @attr {boolean|string} disabled - Disables both internal controls.
|
|
2353
|
+
* @attr {string} value - JSON object with x, y, and color.
|
|
2354
|
+
* @fires input - Composed event with { x, y, color }.
|
|
2355
|
+
* @fires change - Composed event with { x, y, color }.
|
|
2356
|
+
* @fires openchange - Composed event mirroring the internal fig-group.
|
|
2357
|
+
*/
|
|
2358
|
+
class PropskitColorPoint extends HTMLElement {
|
|
2359
|
+
static observedAttributes = [
|
|
2360
|
+
"label",
|
|
2361
|
+
"collapsible",
|
|
2362
|
+
"open",
|
|
2363
|
+
"size",
|
|
2364
|
+
"disabled",
|
|
2365
|
+
"value",
|
|
2366
|
+
];
|
|
2367
|
+
|
|
2368
|
+
#group = null;
|
|
2369
|
+
#colorControl = null;
|
|
2370
|
+
#positionControl = null;
|
|
2371
|
+
#initialized = false;
|
|
2372
|
+
#reflectingValue = false;
|
|
2373
|
+
#reflectingOpen = false;
|
|
2374
|
+
#boundHandleInput = this.#handleControlEvent.bind(this, "input");
|
|
2375
|
+
#boundHandleChange = this.#handleControlEvent.bind(this, "change");
|
|
2376
|
+
#boundHandleOpenChange = this.#handleOpenChange.bind(this);
|
|
2377
|
+
|
|
2378
|
+
connectedCallback() {
|
|
2379
|
+
if (!this.#initialized) {
|
|
2380
|
+
this.#reflectValue(this.#readValue());
|
|
2381
|
+
this.#initialized = true;
|
|
2382
|
+
}
|
|
2383
|
+
if (!this.#group) this.#render();
|
|
2384
|
+
this.#syncGroupAttributes();
|
|
2385
|
+
this.#syncSize();
|
|
2386
|
+
this.#syncDisabled();
|
|
2387
|
+
this.#syncControls(this.#readValue());
|
|
2388
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
2389
|
+
this.addEventListener("input", this.#boundHandleInput);
|
|
2390
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
2391
|
+
this.addEventListener("change", this.#boundHandleChange);
|
|
2392
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
2393
|
+
this.addEventListener("openchange", this.#boundHandleOpenChange);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
disconnectedCallback() {
|
|
2397
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
2398
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
2399
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2403
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
2404
|
+
if (name === "value") {
|
|
2405
|
+
if (!this.#reflectingValue) {
|
|
2406
|
+
const value = this.#reflectValue(this.#readValue());
|
|
2407
|
+
this.#syncControls(value);
|
|
2408
|
+
}
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (name === "size") {
|
|
2412
|
+
this.#syncSize();
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
if (name === "disabled") {
|
|
2416
|
+
this.#syncDisabled();
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
this.#syncGroupAttributes();
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
#finiteNumber(value, fallback) {
|
|
2423
|
+
const number = Number(value);
|
|
2424
|
+
return Number.isFinite(number) ? number : fallback;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
#normalizeValue(value) {
|
|
2428
|
+
let source = value;
|
|
2429
|
+
if (typeof value === "string") {
|
|
2430
|
+
try {
|
|
2431
|
+
source = JSON.parse(value);
|
|
2432
|
+
} catch {
|
|
2433
|
+
source = null;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
2437
|
+
source = {};
|
|
2438
|
+
}
|
|
2439
|
+
return {
|
|
2440
|
+
x: this.#finiteNumber(source.x, 50),
|
|
2441
|
+
y: this.#finiteNumber(source.y, 50),
|
|
2442
|
+
color:
|
|
2443
|
+
typeof source.color === "string" && source.color.trim()
|
|
2444
|
+
? source.color.trim()
|
|
2445
|
+
: "#D9D9D9",
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
#readValue() {
|
|
2450
|
+
return this.#normalizeValue(this.getAttribute("value"));
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
#reflectValue(value) {
|
|
2454
|
+
const normalized = this.#normalizeValue(value);
|
|
2455
|
+
const serialized = JSON.stringify(normalized);
|
|
2456
|
+
if (this.getAttribute("value") === serialized) return normalized;
|
|
2457
|
+
this.#reflectingValue = true;
|
|
2458
|
+
this.setAttribute("value", serialized);
|
|
2459
|
+
this.#reflectingValue = false;
|
|
2460
|
+
return normalized;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
#render() {
|
|
2464
|
+
const value = this.#readValue();
|
|
2465
|
+
const group = document.createElement("fig-group");
|
|
2466
|
+
const color = document.createElement("propskit-color");
|
|
2467
|
+
const position = document.createElement("propskit-position");
|
|
2468
|
+
group.setAttribute("compact", "");
|
|
2469
|
+
color.setAttribute("label", "Color");
|
|
2470
|
+
color.setAttribute("value", value.color);
|
|
2471
|
+
color.setAttribute("data-propskit-color-point-control", "color");
|
|
2472
|
+
position.setAttribute("label", "Position");
|
|
2473
|
+
position.setAttribute("x", String(value.x));
|
|
2474
|
+
position.setAttribute("y", String(value.y));
|
|
2475
|
+
position.setAttribute("units", "percent");
|
|
2476
|
+
position.setAttribute("data-propskit-color-point-control", "position");
|
|
2477
|
+
this.#group = group;
|
|
2478
|
+
this.#colorControl = color;
|
|
2479
|
+
this.#positionControl = position;
|
|
2480
|
+
this.#syncGroupAttributes();
|
|
2481
|
+
this.#syncSize();
|
|
2482
|
+
this.#syncDisabled();
|
|
2483
|
+
group.append(color, position);
|
|
2484
|
+
this.replaceChildren(group);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
#syncGroupAttributes() {
|
|
2488
|
+
if (!this.#group) return;
|
|
2489
|
+
this.#group.setAttribute("compact", "");
|
|
2490
|
+
const label = this.getAttribute("label")?.trim();
|
|
2491
|
+
if (label) this.#group.setAttribute("name", label);
|
|
2492
|
+
else this.#group.removeAttribute("name");
|
|
2493
|
+
this.#group.toggleAttribute("collapsible", this.collapsible);
|
|
2494
|
+
if (this.collapsible) {
|
|
2495
|
+
this.#group.setAttribute("open", String(this.open));
|
|
2496
|
+
} else {
|
|
2497
|
+
this.#group.removeAttribute("open");
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
#syncSize() {
|
|
2502
|
+
const size = this.getAttribute("size");
|
|
2503
|
+
for (const control of [this.#colorControl, this.#positionControl]) {
|
|
2504
|
+
if (!control) continue;
|
|
2505
|
+
if (size === null) control.removeAttribute("size");
|
|
2506
|
+
else control.setAttribute("size", size);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
#syncDisabled() {
|
|
2511
|
+
figLabSyncDisabledControls(this, [
|
|
2512
|
+
this.#colorControl,
|
|
2513
|
+
this.#positionControl,
|
|
2514
|
+
]);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
#syncControls(value) {
|
|
2518
|
+
const normalized = this.#normalizeValue(value);
|
|
2519
|
+
if (this.#colorControl) this.#colorControl.value = normalized.color;
|
|
2520
|
+
if (this.#positionControl) {
|
|
2521
|
+
this.#positionControl.value = {
|
|
2522
|
+
x: normalized.x,
|
|
2523
|
+
y: normalized.y,
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
#handleControlEvent(type, event) {
|
|
2529
|
+
if (event.target === this) return;
|
|
2530
|
+
const control = event.target?.closest?.(
|
|
2531
|
+
"[data-propskit-color-point-control]",
|
|
2532
|
+
);
|
|
2533
|
+
if (!control || !this.contains(control)) return;
|
|
2534
|
+
event.stopImmediatePropagation();
|
|
2535
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
2536
|
+
const value = this.value;
|
|
2537
|
+
const kind = control.getAttribute("data-propskit-color-point-control");
|
|
2538
|
+
if (kind === "color") {
|
|
2539
|
+
value.color = control.value || control.getAttribute("value");
|
|
2540
|
+
} else if (kind === "position") {
|
|
2541
|
+
value.x = this.#finiteNumber(control.value?.x, value.x);
|
|
2542
|
+
value.y = this.#finiteNumber(control.value?.y, value.y);
|
|
2543
|
+
} else {
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
const detail = this.#reflectValue(value);
|
|
2547
|
+
this.dispatchEvent(
|
|
2548
|
+
new CustomEvent(type, {
|
|
2549
|
+
detail: { ...detail, units: "percent" },
|
|
2550
|
+
bubbles: true,
|
|
2551
|
+
cancelable: true,
|
|
2552
|
+
composed: true,
|
|
2553
|
+
}),
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
#handleOpenChange(event) {
|
|
2558
|
+
if (event.target !== this.#group || this.#reflectingOpen) return;
|
|
2559
|
+
event.stopImmediatePropagation();
|
|
2560
|
+
const open = Boolean(event.detail?.open);
|
|
2561
|
+
this.#reflectingOpen = true;
|
|
2562
|
+
this.setAttribute("open", String(open));
|
|
2563
|
+
this.#reflectingOpen = false;
|
|
2564
|
+
this.dispatchEvent(
|
|
2565
|
+
new CustomEvent("openchange", {
|
|
2566
|
+
detail: { open },
|
|
2567
|
+
bubbles: true,
|
|
2568
|
+
composed: true,
|
|
2569
|
+
}),
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
get collapsible() {
|
|
2574
|
+
const value = this.getAttribute("collapsible");
|
|
2575
|
+
return value === null || value !== "false";
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
set collapsible(value) {
|
|
2579
|
+
this.setAttribute("collapsible", String(Boolean(value)));
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
get open() {
|
|
2583
|
+
const value = this.getAttribute("open");
|
|
2584
|
+
return value === null || value !== "false";
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
set open(value) {
|
|
2588
|
+
this.setAttribute("open", String(Boolean(value)));
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
get value() {
|
|
2592
|
+
return { ...this.#readValue() };
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
set value(value) {
|
|
2596
|
+
const normalized = this.#reflectValue(value);
|
|
2597
|
+
this.#syncControls(normalized);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
focus(options) {
|
|
2601
|
+
this.#colorControl?.focus(options);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
figLabDefineElement("propskit-color-point", PropskitColorPoint);
|
|
2605
|
+
|
|
2606
|
+
/**
|
|
2607
|
+
* Collapsible point-radius group composed from position and number controls.
|
|
2608
|
+
*
|
|
2609
|
+
* @attr {string} label - Passed to the internal fig-group name.
|
|
2610
|
+
* @attr {boolean|string} collapsible - Internal group collapsibility; defaults true.
|
|
2611
|
+
* @attr {boolean|string} open - Internal group expanded state; defaults true.
|
|
2612
|
+
* @attr {string} size - Passed to both internal PropsKit controls.
|
|
2613
|
+
* @attr {string} units - Passed to the internal position and radius controls.
|
|
2614
|
+
* @attr {boolean|string} disabled - Disables both internal controls.
|
|
2615
|
+
* @attr {string} value - JSON object with x, y, and radius.
|
|
2616
|
+
* @fires input - Composed event with { x, y, radius }.
|
|
2617
|
+
* @fires change - Composed event with { x, y, radius }.
|
|
2618
|
+
* @fires openchange - Composed event mirroring the internal fig-group.
|
|
2619
|
+
*/
|
|
2620
|
+
class PropskitPointRadius extends HTMLElement {
|
|
2621
|
+
static observedAttributes = [
|
|
2622
|
+
"label",
|
|
2623
|
+
"collapsible",
|
|
2624
|
+
"open",
|
|
2625
|
+
"size",
|
|
2626
|
+
"units",
|
|
2627
|
+
"disabled",
|
|
2628
|
+
"value",
|
|
2629
|
+
];
|
|
2630
|
+
|
|
2631
|
+
#group = null;
|
|
2632
|
+
#positionControl = null;
|
|
2633
|
+
#radiusControl = null;
|
|
2634
|
+
#initialized = false;
|
|
2635
|
+
#reflectingValue = false;
|
|
2636
|
+
#reflectingOpen = false;
|
|
2637
|
+
#boundHandleInput = this.#handleControlEvent.bind(this, "input");
|
|
2638
|
+
#boundHandleChange = this.#handleControlEvent.bind(this, "change");
|
|
2639
|
+
#boundHandleOpenChange = this.#handleOpenChange.bind(this);
|
|
2640
|
+
|
|
2641
|
+
connectedCallback() {
|
|
2642
|
+
if (!this.#initialized) {
|
|
2643
|
+
this.#reflectValue(this.#readValue());
|
|
2644
|
+
this.#initialized = true;
|
|
2645
|
+
}
|
|
2646
|
+
if (!this.#group) this.#render();
|
|
2647
|
+
this.#syncGroupAttributes();
|
|
2648
|
+
this.#syncSize();
|
|
2649
|
+
this.#syncUnits();
|
|
2650
|
+
this.#syncDisabled();
|
|
2651
|
+
this.#syncControls(this.#readValue());
|
|
2652
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
2653
|
+
this.addEventListener("input", this.#boundHandleInput);
|
|
2654
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
2655
|
+
this.addEventListener("change", this.#boundHandleChange);
|
|
2656
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
2657
|
+
this.addEventListener("openchange", this.#boundHandleOpenChange);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
disconnectedCallback() {
|
|
2661
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
2662
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
2663
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2667
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
2668
|
+
if (name === "value") {
|
|
2669
|
+
if (!this.#reflectingValue) {
|
|
2670
|
+
const value = this.#reflectValue(this.#readValue());
|
|
2671
|
+
this.#syncControls(value);
|
|
2672
|
+
}
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (name === "size") {
|
|
2676
|
+
this.#syncSize();
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (name === "units") {
|
|
2680
|
+
this.#syncUnits();
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (name === "disabled") {
|
|
2684
|
+
this.#syncDisabled();
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
this.#syncGroupAttributes();
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
#finiteNumber(value, fallback) {
|
|
2691
|
+
const number = Number(value);
|
|
2692
|
+
return Number.isFinite(number) ? number : fallback;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
#normalizeRadius(value) {
|
|
2696
|
+
if (typeof value === "string" && value.trim().endsWith("%")) {
|
|
2697
|
+
const number = Number.parseFloat(value);
|
|
2698
|
+
if (Number.isFinite(number)) return `${number}%`;
|
|
2699
|
+
}
|
|
2700
|
+
return this.#finiteNumber(value, 0);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
#normalizeValue(value) {
|
|
2704
|
+
let source = value;
|
|
2705
|
+
if (typeof value === "string") {
|
|
2706
|
+
try {
|
|
2707
|
+
source = JSON.parse(value);
|
|
2708
|
+
} catch {
|
|
2709
|
+
source = null;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
2713
|
+
source = {};
|
|
2714
|
+
}
|
|
2715
|
+
return {
|
|
2716
|
+
x: this.#finiteNumber(source.x, 50),
|
|
2717
|
+
y: this.#finiteNumber(source.y, 50),
|
|
2718
|
+
radius: this.#normalizeRadius(source.radius),
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
#readValue() {
|
|
2723
|
+
return this.#normalizeValue(this.getAttribute("value"));
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
#reflectValue(value) {
|
|
2727
|
+
const normalized = this.#normalizeValue(value);
|
|
2728
|
+
const serialized = JSON.stringify(normalized);
|
|
2729
|
+
if (this.getAttribute("value") === serialized) return normalized;
|
|
2730
|
+
this.#reflectingValue = true;
|
|
2731
|
+
this.setAttribute("value", serialized);
|
|
2732
|
+
this.#reflectingValue = false;
|
|
2733
|
+
return normalized;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
#radiusParts(radius) {
|
|
2737
|
+
const percent =
|
|
2738
|
+
typeof radius === "string" && radius.trim().endsWith("%");
|
|
2739
|
+
return {
|
|
2740
|
+
value: percent ? Number.parseFloat(radius) : radius,
|
|
2741
|
+
units: percent ? "%" : "px",
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
#render() {
|
|
2746
|
+
const value = this.#readValue();
|
|
2747
|
+
const radiusValue = this.#radiusParts(value.radius);
|
|
2748
|
+
const group = document.createElement("fig-group");
|
|
2749
|
+
const position = document.createElement("propskit-position");
|
|
2750
|
+
const radius = document.createElement("propskit-number");
|
|
2751
|
+
group.setAttribute("compact", "");
|
|
2752
|
+
position.setAttribute("label", "Position");
|
|
2753
|
+
position.setAttribute("x", String(value.x));
|
|
2754
|
+
position.setAttribute("y", String(value.y));
|
|
2755
|
+
if (this.hasAttribute("units")) position.setAttribute("units", this.units);
|
|
2756
|
+
position.setAttribute("data-propskit-point-radius-control", "position");
|
|
2757
|
+
radius.setAttribute("label", "Radius");
|
|
2758
|
+
radius.setAttribute("value", String(radiusValue.value));
|
|
2759
|
+
if (this.units === "percent") radius.setAttribute("units", "%");
|
|
2760
|
+
radius.setAttribute("units-disallow", "");
|
|
2761
|
+
radius.setAttribute("min", "0");
|
|
2762
|
+
radius.setAttribute("precision", "2");
|
|
2763
|
+
radius.setAttribute("data-propskit-point-radius-control", "radius");
|
|
2764
|
+
this.#group = group;
|
|
2765
|
+
this.#positionControl = position;
|
|
2766
|
+
this.#radiusControl = radius;
|
|
2767
|
+
this.#syncGroupAttributes();
|
|
2768
|
+
this.#syncSize();
|
|
2769
|
+
this.#syncDisabled();
|
|
2770
|
+
group.append(position, radius);
|
|
2771
|
+
this.replaceChildren(group);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
#syncGroupAttributes() {
|
|
2775
|
+
if (!this.#group) return;
|
|
2776
|
+
this.#group.setAttribute("compact", "");
|
|
2777
|
+
const label = this.getAttribute("label")?.trim();
|
|
2778
|
+
if (label) this.#group.setAttribute("name", label);
|
|
2779
|
+
else this.#group.removeAttribute("name");
|
|
2780
|
+
this.#group.toggleAttribute("collapsible", this.collapsible);
|
|
2781
|
+
if (this.collapsible) {
|
|
2782
|
+
this.#group.setAttribute("open", String(this.open));
|
|
2783
|
+
} else {
|
|
2784
|
+
this.#group.removeAttribute("open");
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
#syncSize() {
|
|
2789
|
+
const size = this.getAttribute("size");
|
|
2790
|
+
for (const control of [this.#positionControl, this.#radiusControl]) {
|
|
2791
|
+
if (!control) continue;
|
|
2792
|
+
if (size === null) control.removeAttribute("size");
|
|
2793
|
+
else control.setAttribute("size", size);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
#syncUnits() {
|
|
2798
|
+
if (this.hasAttribute("units")) {
|
|
2799
|
+
this.#positionControl?.setAttribute("units", this.units);
|
|
2800
|
+
} else {
|
|
2801
|
+
this.#positionControl?.removeAttribute("units");
|
|
2802
|
+
}
|
|
2803
|
+
if (this.units === "percent") {
|
|
2804
|
+
this.#radiusControl?.setAttribute("units", "%");
|
|
2805
|
+
} else {
|
|
2806
|
+
this.#radiusControl?.removeAttribute("units");
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
#syncDisabled() {
|
|
2811
|
+
figLabSyncDisabledControls(this, [
|
|
2812
|
+
this.#positionControl,
|
|
2813
|
+
this.#radiusControl,
|
|
2814
|
+
]);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
#syncControls(value) {
|
|
2818
|
+
const normalized = this.#normalizeValue(value);
|
|
2819
|
+
if (this.#positionControl) {
|
|
2820
|
+
this.#positionControl.value = {
|
|
2821
|
+
x: normalized.x,
|
|
2822
|
+
y: normalized.y,
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
if (this.#radiusControl) {
|
|
2826
|
+
const radius = this.#radiusParts(normalized.radius);
|
|
2827
|
+
this.#radiusControl.value = radius.value;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
#radiusFromControl(control, fallback) {
|
|
2832
|
+
const fallbackValue = this.#radiusParts(fallback).value;
|
|
2833
|
+
const value = this.#finiteNumber(control.value, fallbackValue);
|
|
2834
|
+
const units =
|
|
2835
|
+
control.querySelector("fig-input-number")?.getAttribute("units") ||
|
|
2836
|
+
control.getAttribute("units");
|
|
2837
|
+
return units === "%" ? `${value}%` : value;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
#handleControlEvent(type, event) {
|
|
2841
|
+
if (event.target === this) return;
|
|
2842
|
+
const control = event.target?.closest?.(
|
|
2843
|
+
"[data-propskit-point-radius-control]",
|
|
2844
|
+
);
|
|
2845
|
+
if (!control || !this.contains(control)) return;
|
|
2846
|
+
event.stopImmediatePropagation();
|
|
2847
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
2848
|
+
const value = this.value;
|
|
2849
|
+
const kind = control.getAttribute("data-propskit-point-radius-control");
|
|
2850
|
+
if (kind === "position") {
|
|
2851
|
+
value.x = this.#finiteNumber(control.value?.x, value.x);
|
|
2852
|
+
value.y = this.#finiteNumber(control.value?.y, value.y);
|
|
2853
|
+
} else if (kind === "radius") {
|
|
2854
|
+
value.radius = this.#radiusFromControl(control, value.radius);
|
|
2855
|
+
} else {
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const detail = this.#reflectValue(value);
|
|
2859
|
+
this.dispatchEvent(
|
|
2860
|
+
new CustomEvent(type, {
|
|
2861
|
+
detail: {
|
|
2862
|
+
...detail,
|
|
2863
|
+
radius: this.#radiusParts(detail.radius).value,
|
|
2864
|
+
units: this.units,
|
|
2865
|
+
},
|
|
2866
|
+
bubbles: true,
|
|
2867
|
+
cancelable: true,
|
|
2868
|
+
composed: true,
|
|
2869
|
+
}),
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
#handleOpenChange(event) {
|
|
2874
|
+
if (event.target !== this.#group || this.#reflectingOpen) return;
|
|
2875
|
+
event.stopImmediatePropagation();
|
|
2876
|
+
const open = Boolean(event.detail?.open);
|
|
2877
|
+
this.#reflectingOpen = true;
|
|
2878
|
+
this.setAttribute("open", String(open));
|
|
2879
|
+
this.#reflectingOpen = false;
|
|
2880
|
+
this.dispatchEvent(
|
|
2881
|
+
new CustomEvent("openchange", {
|
|
2882
|
+
detail: { open },
|
|
2883
|
+
bubbles: true,
|
|
2884
|
+
composed: true,
|
|
2885
|
+
}),
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
get collapsible() {
|
|
2890
|
+
const value = this.getAttribute("collapsible");
|
|
2891
|
+
return value === null || value !== "false";
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
set collapsible(value) {
|
|
2895
|
+
this.setAttribute("collapsible", String(Boolean(value)));
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
get open() {
|
|
2899
|
+
const value = this.getAttribute("open");
|
|
2900
|
+
return value === null || value !== "false";
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
set open(value) {
|
|
2904
|
+
this.setAttribute("open", String(Boolean(value)));
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
get units() {
|
|
2908
|
+
return this.getAttribute("units")?.trim().toLowerCase() === "percent"
|
|
2909
|
+
? "percent"
|
|
2910
|
+
: "none";
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
set units(value) {
|
|
2914
|
+
this.setAttribute(
|
|
2915
|
+
"units",
|
|
2916
|
+
String(value).trim().toLowerCase() === "none" ? "none" : "percent",
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
get value() {
|
|
2921
|
+
return { ...this.#readValue() };
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
set value(value) {
|
|
2925
|
+
const normalized = this.#reflectValue(value);
|
|
2926
|
+
this.#syncControls(normalized);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
focus(options) {
|
|
2930
|
+
this.#positionControl?.focus(options);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
figLabDefineElement("propskit-point-radius", PropskitPointRadius);
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* Collapsible point-radius-angle group composed from PropsKit controls.
|
|
2937
|
+
*
|
|
2938
|
+
* @attr {string} label - Passed to the internal fig-group name.
|
|
2939
|
+
* @attr {boolean|string} collapsible - Internal group collapsibility; defaults true.
|
|
2940
|
+
* @attr {boolean|string} open - Internal group expanded state; defaults true.
|
|
2941
|
+
* @attr {string} size - Passed to every internal PropsKit control.
|
|
2942
|
+
* @attr {string} units - Passed to the internal position and radius controls.
|
|
2943
|
+
* @attr {boolean|string} disabled - Disables every internal control.
|
|
2944
|
+
* @attr {string} value - JSON object with x, y, radius, and angle.
|
|
2945
|
+
* @fires input - Composed event with { x, y, radius, angle }.
|
|
2946
|
+
* @fires change - Composed event with { x, y, radius, angle }.
|
|
2947
|
+
* @fires openchange - Composed event mirroring the internal fig-group.
|
|
2948
|
+
*/
|
|
2949
|
+
class PropskitPointRadiusAngle extends HTMLElement {
|
|
2950
|
+
static observedAttributes = [
|
|
2951
|
+
"label",
|
|
2952
|
+
"collapsible",
|
|
2953
|
+
"open",
|
|
2954
|
+
"size",
|
|
2955
|
+
"units",
|
|
2956
|
+
"disabled",
|
|
2957
|
+
"value",
|
|
2958
|
+
];
|
|
2959
|
+
|
|
2960
|
+
#group = null;
|
|
2961
|
+
#positionControl = null;
|
|
2962
|
+
#radiusControl = null;
|
|
2963
|
+
#angleControl = null;
|
|
2964
|
+
#initialized = false;
|
|
2965
|
+
#reflectingValue = false;
|
|
2966
|
+
#reflectingOpen = false;
|
|
2967
|
+
#boundHandleInput = this.#handleControlEvent.bind(this, "input");
|
|
2968
|
+
#boundHandleChange = this.#handleControlEvent.bind(this, "change");
|
|
2969
|
+
#boundHandleOpenChange = this.#handleOpenChange.bind(this);
|
|
2970
|
+
|
|
2971
|
+
connectedCallback() {
|
|
2972
|
+
if (!this.#initialized) {
|
|
2973
|
+
this.#reflectValue(this.#readValue());
|
|
2974
|
+
this.#initialized = true;
|
|
2975
|
+
}
|
|
2976
|
+
if (!this.#group) this.#render();
|
|
2977
|
+
this.#syncGroupAttributes();
|
|
2978
|
+
this.#syncSize();
|
|
2979
|
+
this.#syncUnits();
|
|
2980
|
+
this.#syncDisabled();
|
|
2981
|
+
this.#syncControls(this.#readValue());
|
|
2982
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
2983
|
+
this.addEventListener("input", this.#boundHandleInput);
|
|
2984
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
2985
|
+
this.addEventListener("change", this.#boundHandleChange);
|
|
2986
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
2987
|
+
this.addEventListener("openchange", this.#boundHandleOpenChange);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
disconnectedCallback() {
|
|
2991
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
2992
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
2993
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2997
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
2998
|
+
if (name === "value") {
|
|
2999
|
+
if (!this.#reflectingValue) {
|
|
3000
|
+
const value = this.#reflectValue(this.#readValue());
|
|
3001
|
+
this.#syncControls(value);
|
|
3002
|
+
}
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
if (name === "size") {
|
|
3006
|
+
this.#syncSize();
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
if (name === "units") {
|
|
3010
|
+
this.#syncUnits();
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
if (name === "disabled") {
|
|
3014
|
+
this.#syncDisabled();
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
this.#syncGroupAttributes();
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
#finiteNumber(value, fallback) {
|
|
3021
|
+
const number = Number(value);
|
|
3022
|
+
return Number.isFinite(number) ? number : fallback;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
#normalizeRadius(value) {
|
|
3026
|
+
if (typeof value === "string" && value.trim().endsWith("%")) {
|
|
3027
|
+
const number = Number.parseFloat(value);
|
|
3028
|
+
if (Number.isFinite(number)) return `${number}%`;
|
|
3029
|
+
}
|
|
3030
|
+
return this.#finiteNumber(value, 0);
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
#normalizeValue(value) {
|
|
3034
|
+
let source = value;
|
|
3035
|
+
if (typeof value === "string") {
|
|
3036
|
+
try {
|
|
3037
|
+
source = JSON.parse(value);
|
|
3038
|
+
} catch {
|
|
3039
|
+
source = null;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
3043
|
+
source = {};
|
|
3044
|
+
}
|
|
3045
|
+
return {
|
|
3046
|
+
x: this.#finiteNumber(source.x, 50),
|
|
3047
|
+
y: this.#finiteNumber(source.y, 50),
|
|
3048
|
+
radius: this.#normalizeRadius(source.radius),
|
|
3049
|
+
angle: this.#finiteNumber(source.angle, 0),
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
#readValue() {
|
|
3054
|
+
return this.#normalizeValue(this.getAttribute("value"));
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
#reflectValue(value) {
|
|
3058
|
+
const normalized = this.#normalizeValue(value);
|
|
3059
|
+
const serialized = JSON.stringify(normalized);
|
|
3060
|
+
if (this.getAttribute("value") === serialized) return normalized;
|
|
3061
|
+
this.#reflectingValue = true;
|
|
3062
|
+
this.setAttribute("value", serialized);
|
|
3063
|
+
this.#reflectingValue = false;
|
|
3064
|
+
return normalized;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
#radiusParts(radius) {
|
|
3068
|
+
const percent =
|
|
3069
|
+
typeof radius === "string" && radius.trim().endsWith("%");
|
|
3070
|
+
return {
|
|
3071
|
+
value: percent ? Number.parseFloat(radius) : radius,
|
|
3072
|
+
units: percent ? "%" : "px",
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
#render() {
|
|
3077
|
+
const value = this.#readValue();
|
|
3078
|
+
const radiusValue = this.#radiusParts(value.radius);
|
|
3079
|
+
const group = document.createElement("fig-group");
|
|
3080
|
+
const position = document.createElement("propskit-position");
|
|
3081
|
+
const radius = document.createElement("propskit-number");
|
|
3082
|
+
const angle = document.createElement("propskit-number");
|
|
3083
|
+
group.setAttribute("compact", "");
|
|
3084
|
+
position.setAttribute("label", "Position");
|
|
3085
|
+
position.setAttribute("x", String(value.x));
|
|
3086
|
+
position.setAttribute("y", String(value.y));
|
|
3087
|
+
if (this.hasAttribute("units")) position.setAttribute("units", this.units);
|
|
3088
|
+
position.setAttribute(
|
|
3089
|
+
"data-propskit-point-radius-angle-control",
|
|
3090
|
+
"position",
|
|
3091
|
+
);
|
|
3092
|
+
radius.setAttribute("label", "Radius");
|
|
3093
|
+
radius.setAttribute("value", String(radiusValue.value));
|
|
3094
|
+
if (this.units === "percent") radius.setAttribute("units", "%");
|
|
3095
|
+
radius.setAttribute("units-disallow", "");
|
|
3096
|
+
radius.setAttribute("min", "0");
|
|
3097
|
+
radius.setAttribute("precision", "2");
|
|
3098
|
+
radius.setAttribute("data-propskit-point-radius-angle-control", "radius");
|
|
3099
|
+
angle.setAttribute("label", "Angle");
|
|
3100
|
+
angle.setAttribute("value", String(value.angle));
|
|
3101
|
+
angle.setAttribute("units", "°");
|
|
3102
|
+
angle.setAttribute("units-disallow", "");
|
|
3103
|
+
angle.setAttribute("precision", "1");
|
|
3104
|
+
angle.setAttribute("data-propskit-point-radius-angle-control", "angle");
|
|
3105
|
+
this.#group = group;
|
|
3106
|
+
this.#positionControl = position;
|
|
3107
|
+
this.#radiusControl = radius;
|
|
3108
|
+
this.#angleControl = angle;
|
|
3109
|
+
this.#syncGroupAttributes();
|
|
3110
|
+
this.#syncSize();
|
|
3111
|
+
this.#syncDisabled();
|
|
3112
|
+
group.append(position, radius, angle);
|
|
3113
|
+
this.replaceChildren(group);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
#syncGroupAttributes() {
|
|
3117
|
+
if (!this.#group) return;
|
|
3118
|
+
this.#group.setAttribute("compact", "");
|
|
3119
|
+
const label = this.getAttribute("label")?.trim();
|
|
3120
|
+
if (label) this.#group.setAttribute("name", label);
|
|
3121
|
+
else this.#group.removeAttribute("name");
|
|
3122
|
+
this.#group.toggleAttribute("collapsible", this.collapsible);
|
|
3123
|
+
if (this.collapsible) {
|
|
3124
|
+
this.#group.setAttribute("open", String(this.open));
|
|
3125
|
+
} else {
|
|
3126
|
+
this.#group.removeAttribute("open");
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
#syncSize() {
|
|
3131
|
+
const size = this.getAttribute("size");
|
|
3132
|
+
for (const control of [
|
|
3133
|
+
this.#positionControl,
|
|
3134
|
+
this.#radiusControl,
|
|
3135
|
+
this.#angleControl,
|
|
3136
|
+
]) {
|
|
3137
|
+
if (!control) continue;
|
|
3138
|
+
if (size === null) control.removeAttribute("size");
|
|
3139
|
+
else control.setAttribute("size", size);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
#syncUnits() {
|
|
3144
|
+
if (this.hasAttribute("units")) {
|
|
3145
|
+
this.#positionControl?.setAttribute("units", this.units);
|
|
3146
|
+
} else {
|
|
3147
|
+
this.#positionControl?.removeAttribute("units");
|
|
3148
|
+
}
|
|
3149
|
+
if (this.units === "percent") {
|
|
3150
|
+
this.#radiusControl?.setAttribute("units", "%");
|
|
3151
|
+
} else {
|
|
3152
|
+
this.#radiusControl?.removeAttribute("units");
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
#syncDisabled() {
|
|
3157
|
+
figLabSyncDisabledControls(this, [
|
|
3158
|
+
this.#positionControl,
|
|
3159
|
+
this.#radiusControl,
|
|
3160
|
+
this.#angleControl,
|
|
3161
|
+
]);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
#syncControls(value) {
|
|
3165
|
+
const normalized = this.#normalizeValue(value);
|
|
3166
|
+
if (this.#positionControl) {
|
|
3167
|
+
this.#positionControl.value = {
|
|
3168
|
+
x: normalized.x,
|
|
3169
|
+
y: normalized.y,
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
if (this.#radiusControl) {
|
|
3173
|
+
const radius = this.#radiusParts(normalized.radius);
|
|
3174
|
+
this.#radiusControl.value = radius.value;
|
|
3175
|
+
}
|
|
3176
|
+
if (this.#angleControl) {
|
|
3177
|
+
this.#angleControl.value = normalized.angle;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
#radiusFromControl(control, fallback) {
|
|
3182
|
+
const fallbackValue = this.#radiusParts(fallback).value;
|
|
3183
|
+
const value = this.#finiteNumber(control.value, fallbackValue);
|
|
3184
|
+
const units =
|
|
3185
|
+
control.querySelector("fig-input-number")?.getAttribute("units") ||
|
|
3186
|
+
control.getAttribute("units");
|
|
3187
|
+
return units === "%" ? `${value}%` : value;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
#handleControlEvent(type, event) {
|
|
3191
|
+
if (event.target === this) return;
|
|
3192
|
+
const control = event.target?.closest?.(
|
|
3193
|
+
"[data-propskit-point-radius-angle-control]",
|
|
3194
|
+
);
|
|
3195
|
+
if (!control || !this.contains(control)) return;
|
|
3196
|
+
event.stopImmediatePropagation();
|
|
3197
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3198
|
+
const value = this.value;
|
|
3199
|
+
const kind = control.getAttribute(
|
|
3200
|
+
"data-propskit-point-radius-angle-control",
|
|
3201
|
+
);
|
|
3202
|
+
if (kind === "position") {
|
|
3203
|
+
value.x = this.#finiteNumber(control.value?.x, value.x);
|
|
3204
|
+
value.y = this.#finiteNumber(control.value?.y, value.y);
|
|
3205
|
+
} else if (kind === "radius") {
|
|
3206
|
+
value.radius = this.#radiusFromControl(control, value.radius);
|
|
3207
|
+
} else if (kind === "angle") {
|
|
3208
|
+
value.angle = this.#finiteNumber(control.value, value.angle);
|
|
3209
|
+
} else {
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
const detail = this.#reflectValue(value);
|
|
3213
|
+
this.dispatchEvent(
|
|
3214
|
+
new CustomEvent(type, {
|
|
3215
|
+
detail: {
|
|
3216
|
+
...detail,
|
|
3217
|
+
radius: this.#radiusParts(detail.radius).value,
|
|
3218
|
+
units: this.units,
|
|
3219
|
+
},
|
|
3220
|
+
bubbles: true,
|
|
3221
|
+
cancelable: true,
|
|
3222
|
+
composed: true,
|
|
3223
|
+
}),
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
#handleOpenChange(event) {
|
|
3228
|
+
if (event.target !== this.#group || this.#reflectingOpen) return;
|
|
3229
|
+
event.stopImmediatePropagation();
|
|
3230
|
+
const open = Boolean(event.detail?.open);
|
|
3231
|
+
this.#reflectingOpen = true;
|
|
3232
|
+
this.setAttribute("open", String(open));
|
|
3233
|
+
this.#reflectingOpen = false;
|
|
3234
|
+
this.dispatchEvent(
|
|
3235
|
+
new CustomEvent("openchange", {
|
|
3236
|
+
detail: { open },
|
|
3237
|
+
bubbles: true,
|
|
3238
|
+
composed: true,
|
|
3239
|
+
}),
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
get collapsible() {
|
|
3244
|
+
const value = this.getAttribute("collapsible");
|
|
3245
|
+
return value === null || value !== "false";
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
set collapsible(value) {
|
|
3249
|
+
this.setAttribute("collapsible", String(Boolean(value)));
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
get open() {
|
|
3253
|
+
const value = this.getAttribute("open");
|
|
3254
|
+
return value === null || value !== "false";
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
set open(value) {
|
|
3258
|
+
this.setAttribute("open", String(Boolean(value)));
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
get units() {
|
|
3262
|
+
return this.getAttribute("units")?.trim().toLowerCase() === "percent"
|
|
3263
|
+
? "percent"
|
|
3264
|
+
: "none";
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
set units(value) {
|
|
3268
|
+
this.setAttribute(
|
|
3269
|
+
"units",
|
|
3270
|
+
String(value).trim().toLowerCase() === "none" ? "none" : "percent",
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
get value() {
|
|
3275
|
+
return { ...this.#readValue() };
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
set value(value) {
|
|
3279
|
+
const normalized = this.#reflectValue(value);
|
|
3280
|
+
this.#syncControls(normalized);
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
focus(options) {
|
|
3284
|
+
this.#positionControl?.focus(options);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
figLabDefineElement(
|
|
3288
|
+
"propskit-point-radius-angle",
|
|
3289
|
+
PropskitPointRadiusAngle,
|
|
3290
|
+
);
|
|
3291
|
+
|
|
3292
|
+
/**
|
|
3293
|
+
* Collapsible point-point group composed from two position controls.
|
|
3294
|
+
*
|
|
3295
|
+
* @attr {string} label - Passed to the internal fig-group name.
|
|
3296
|
+
* @attr {boolean|string} collapsible - Internal group collapsibility; defaults true.
|
|
3297
|
+
* @attr {boolean|string} open - Internal group expanded state; defaults true.
|
|
3298
|
+
* @attr {string} size - Passed to both internal position controls.
|
|
3299
|
+
* @attr {string} units - Passed to both internal position controls.
|
|
3300
|
+
* @attr {boolean|string} disabled - Disables both internal position controls.
|
|
3301
|
+
* @attr {string} value - JSON object with x, y, x2, and y2.
|
|
3302
|
+
* @fires input - Composed event with { x, y, x2, y2 }.
|
|
3303
|
+
* @fires change - Composed event with { x, y, x2, y2 }.
|
|
3304
|
+
* @fires openchange - Composed event mirroring the internal fig-group.
|
|
3305
|
+
*/
|
|
3306
|
+
class PropskitPointPoint extends HTMLElement {
|
|
3307
|
+
static observedAttributes = [
|
|
3308
|
+
"label",
|
|
3309
|
+
"collapsible",
|
|
3310
|
+
"open",
|
|
3311
|
+
"size",
|
|
3312
|
+
"units",
|
|
3313
|
+
"disabled",
|
|
3314
|
+
"value",
|
|
3315
|
+
];
|
|
3316
|
+
|
|
3317
|
+
#group = null;
|
|
3318
|
+
#startControl = null;
|
|
3319
|
+
#endControl = null;
|
|
3320
|
+
#initialized = false;
|
|
3321
|
+
#reflectingValue = false;
|
|
3322
|
+
#reflectingOpen = false;
|
|
3323
|
+
#boundHandleInput = this.#handleControlEvent.bind(this, "input");
|
|
3324
|
+
#boundHandleChange = this.#handleControlEvent.bind(this, "change");
|
|
3325
|
+
#boundHandleOpenChange = this.#handleOpenChange.bind(this);
|
|
3326
|
+
|
|
3327
|
+
connectedCallback() {
|
|
3328
|
+
if (!this.#initialized) {
|
|
3329
|
+
this.#reflectValue(this.#readValue());
|
|
3330
|
+
this.#initialized = true;
|
|
3331
|
+
}
|
|
3332
|
+
if (!this.#group) this.#render();
|
|
3333
|
+
this.#syncGroupAttributes();
|
|
3334
|
+
this.#syncSize();
|
|
3335
|
+
this.#syncUnits();
|
|
3336
|
+
this.#syncDisabled();
|
|
3337
|
+
this.#syncControls(this.#readValue());
|
|
3338
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
3339
|
+
this.addEventListener("input", this.#boundHandleInput);
|
|
3340
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
3341
|
+
this.addEventListener("change", this.#boundHandleChange);
|
|
3342
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
3343
|
+
this.addEventListener("openchange", this.#boundHandleOpenChange);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
disconnectedCallback() {
|
|
3347
|
+
this.removeEventListener("input", this.#boundHandleInput);
|
|
3348
|
+
this.removeEventListener("change", this.#boundHandleChange);
|
|
3349
|
+
this.removeEventListener("openchange", this.#boundHandleOpenChange);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
3353
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
3354
|
+
if (name === "value") {
|
|
3355
|
+
if (!this.#reflectingValue) {
|
|
3356
|
+
const value = this.#reflectValue(this.#readValue());
|
|
3357
|
+
this.#syncControls(value);
|
|
3358
|
+
}
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
if (name === "size") {
|
|
3362
|
+
this.#syncSize();
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
if (name === "units") {
|
|
3366
|
+
this.#syncUnits();
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
if (name === "disabled") {
|
|
3370
|
+
this.#syncDisabled();
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
this.#syncGroupAttributes();
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
#finiteNumber(value, fallback) {
|
|
3377
|
+
const number = Number(value);
|
|
3378
|
+
return Number.isFinite(number) ? number : fallback;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
#normalizeValue(value) {
|
|
3382
|
+
let source = value;
|
|
3383
|
+
if (typeof value === "string") {
|
|
3384
|
+
try {
|
|
3385
|
+
source = JSON.parse(value);
|
|
3386
|
+
} catch {
|
|
3387
|
+
source = null;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
3391
|
+
source = {};
|
|
3392
|
+
}
|
|
3393
|
+
return {
|
|
3394
|
+
x: this.#finiteNumber(source.x, 50),
|
|
3395
|
+
y: this.#finiteNumber(source.y, 50),
|
|
3396
|
+
x2: this.#finiteNumber(source.x2, 75),
|
|
3397
|
+
y2: this.#finiteNumber(source.y2, 75),
|
|
3398
|
+
};
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
#readValue() {
|
|
3402
|
+
return this.#normalizeValue(this.getAttribute("value"));
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
#reflectValue(value) {
|
|
3406
|
+
const normalized = this.#normalizeValue(value);
|
|
3407
|
+
const serialized = JSON.stringify(normalized);
|
|
3408
|
+
if (this.getAttribute("value") === serialized) return normalized;
|
|
3409
|
+
this.#reflectingValue = true;
|
|
3410
|
+
this.setAttribute("value", serialized);
|
|
3411
|
+
this.#reflectingValue = false;
|
|
3412
|
+
return normalized;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
#createPosition(label, role, x, y) {
|
|
3416
|
+
const control = document.createElement("propskit-position");
|
|
3417
|
+
control.setAttribute("label", label);
|
|
3418
|
+
control.setAttribute("x", String(x));
|
|
3419
|
+
control.setAttribute("y", String(y));
|
|
3420
|
+
if (this.hasAttribute("units")) control.setAttribute("units", this.units);
|
|
3421
|
+
control.setAttribute("data-propskit-point-point-control", role);
|
|
3422
|
+
return control;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
#render() {
|
|
3426
|
+
const value = this.#readValue();
|
|
3427
|
+
const group = document.createElement("fig-group");
|
|
3428
|
+
const start = this.#createPosition("Start", "start", value.x, value.y);
|
|
3429
|
+
const end = this.#createPosition("End", "end", value.x2, value.y2);
|
|
3430
|
+
group.setAttribute("compact", "");
|
|
3431
|
+
this.#group = group;
|
|
3432
|
+
this.#startControl = start;
|
|
3433
|
+
this.#endControl = end;
|
|
3434
|
+
this.#syncGroupAttributes();
|
|
3435
|
+
this.#syncSize();
|
|
3436
|
+
this.#syncDisabled();
|
|
3437
|
+
group.append(start, end);
|
|
3438
|
+
this.replaceChildren(group);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
#syncGroupAttributes() {
|
|
3442
|
+
if (!this.#group) return;
|
|
3443
|
+
this.#group.setAttribute("compact", "");
|
|
3444
|
+
const label = this.getAttribute("label")?.trim();
|
|
3445
|
+
if (label) this.#group.setAttribute("name", label);
|
|
3446
|
+
else this.#group.removeAttribute("name");
|
|
3447
|
+
this.#group.toggleAttribute("collapsible", this.collapsible);
|
|
3448
|
+
if (this.collapsible) {
|
|
3449
|
+
this.#group.setAttribute("open", String(this.open));
|
|
3450
|
+
} else {
|
|
3451
|
+
this.#group.removeAttribute("open");
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
#syncSize() {
|
|
3456
|
+
const size = this.getAttribute("size");
|
|
3457
|
+
for (const control of [this.#startControl, this.#endControl]) {
|
|
3458
|
+
if (!control) continue;
|
|
3459
|
+
if (size === null) control.removeAttribute("size");
|
|
3460
|
+
else control.setAttribute("size", size);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
#syncUnits() {
|
|
3465
|
+
for (const control of [this.#startControl, this.#endControl]) {
|
|
3466
|
+
if (!control) continue;
|
|
3467
|
+
if (this.hasAttribute("units")) control.setAttribute("units", this.units);
|
|
3468
|
+
else control.removeAttribute("units");
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
#syncDisabled() {
|
|
3473
|
+
figLabSyncDisabledControls(this, [
|
|
3474
|
+
this.#startControl,
|
|
3475
|
+
this.#endControl,
|
|
3476
|
+
]);
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
#syncControls(value) {
|
|
3480
|
+
const normalized = this.#normalizeValue(value);
|
|
3481
|
+
if (this.#startControl) {
|
|
3482
|
+
this.#startControl.value = {
|
|
3483
|
+
x: normalized.x,
|
|
3484
|
+
y: normalized.y,
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
if (this.#endControl) {
|
|
3488
|
+
this.#endControl.value = {
|
|
3489
|
+
x: normalized.x2,
|
|
3490
|
+
y: normalized.y2,
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
#handleControlEvent(type, event) {
|
|
3496
|
+
if (event.target === this) return;
|
|
3497
|
+
const control = event.target?.closest?.(
|
|
3498
|
+
"[data-propskit-point-point-control]",
|
|
3499
|
+
);
|
|
3500
|
+
if (!control || !this.contains(control)) return;
|
|
3501
|
+
event.stopImmediatePropagation();
|
|
3502
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3503
|
+
const value = this.value;
|
|
3504
|
+
const point = control.value;
|
|
3505
|
+
const role = control.getAttribute("data-propskit-point-point-control");
|
|
3506
|
+
if (role === "start") {
|
|
3507
|
+
value.x = this.#finiteNumber(point?.x, value.x);
|
|
3508
|
+
value.y = this.#finiteNumber(point?.y, value.y);
|
|
3509
|
+
} else if (role === "end") {
|
|
3510
|
+
value.x2 = this.#finiteNumber(point?.x, value.x2);
|
|
3511
|
+
value.y2 = this.#finiteNumber(point?.y, value.y2);
|
|
3512
|
+
} else {
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
const detail = this.#reflectValue(value);
|
|
3516
|
+
this.dispatchEvent(
|
|
3517
|
+
new CustomEvent(type, {
|
|
3518
|
+
detail: { ...detail, units: this.units },
|
|
3519
|
+
bubbles: true,
|
|
3520
|
+
cancelable: true,
|
|
3521
|
+
composed: true,
|
|
3522
|
+
}),
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
#handleOpenChange(event) {
|
|
3527
|
+
if (event.target !== this.#group || this.#reflectingOpen) return;
|
|
3528
|
+
event.stopImmediatePropagation();
|
|
3529
|
+
const open = Boolean(event.detail?.open);
|
|
3530
|
+
this.#reflectingOpen = true;
|
|
3531
|
+
this.setAttribute("open", String(open));
|
|
3532
|
+
this.#reflectingOpen = false;
|
|
3533
|
+
this.dispatchEvent(
|
|
3534
|
+
new CustomEvent("openchange", {
|
|
3535
|
+
detail: { open },
|
|
3536
|
+
bubbles: true,
|
|
3537
|
+
composed: true,
|
|
3538
|
+
}),
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
get collapsible() {
|
|
3543
|
+
const value = this.getAttribute("collapsible");
|
|
3544
|
+
return value === null || value !== "false";
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
set collapsible(value) {
|
|
3548
|
+
this.setAttribute("collapsible", String(Boolean(value)));
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
get open() {
|
|
3552
|
+
const value = this.getAttribute("open");
|
|
3553
|
+
return value === null || value !== "false";
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
set open(value) {
|
|
3557
|
+
this.setAttribute("open", String(Boolean(value)));
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
get units() {
|
|
3561
|
+
return this.getAttribute("units")?.trim().toLowerCase() === "percent"
|
|
3562
|
+
? "percent"
|
|
3563
|
+
: "none";
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
set units(value) {
|
|
3567
|
+
this.setAttribute(
|
|
3568
|
+
"units",
|
|
3569
|
+
String(value).trim().toLowerCase() === "none" ? "none" : "percent",
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
get value() {
|
|
3574
|
+
return { ...this.#readValue() };
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
set value(value) {
|
|
3578
|
+
const normalized = this.#reflectValue(value);
|
|
3579
|
+
this.#syncControls(normalized);
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
focus(options) {
|
|
3583
|
+
this.#startControl?.focus(options);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
figLabDefineElement("propskit-point-point", PropskitPointPoint);
|
|
3587
|
+
|
|
3588
|
+
/* Collapsible property group — always collapsible (no collapsible attr). */
|
|
3589
|
+
class PropskitGroup extends HTMLElement {
|
|
3590
|
+
static observedAttributes = ["name", "open", "show-reset", "disabled"];
|
|
3591
|
+
|
|
3592
|
+
static #CONTROL_SELECTOR = [
|
|
3593
|
+
"propskit-color",
|
|
3594
|
+
"propskit-gradient",
|
|
3595
|
+
"propskit-number",
|
|
3596
|
+
"propskit-position",
|
|
3597
|
+
"propskit-color-point",
|
|
3598
|
+
"propskit-point-radius",
|
|
3599
|
+
"propskit-point-radius-angle",
|
|
3600
|
+
"propskit-point-point",
|
|
3601
|
+
"propskit-select",
|
|
3602
|
+
"propskit-slider",
|
|
3603
|
+
"propskit-switch",
|
|
3604
|
+
"propskit-text",
|
|
3605
|
+
"propskit-oscillator",
|
|
3606
|
+
].join(",");
|
|
3607
|
+
|
|
3608
|
+
#header = null;
|
|
3609
|
+
#chevron = null;
|
|
3610
|
+
#resetTooltip = null;
|
|
3611
|
+
#childObserver = null;
|
|
3612
|
+
#dirtyFrame = 0;
|
|
3613
|
+
#boundOnControlEvent = () => this.#queueDirtySync();
|
|
3614
|
+
|
|
3615
|
+
connectedCallback() {
|
|
3616
|
+
this.#render();
|
|
3617
|
+
this.#syncDisabled();
|
|
3618
|
+
this.#bindDirtyListeners();
|
|
3619
|
+
requestAnimationFrame(() => {
|
|
3620
|
+
this.#syncDirtyState();
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
disconnectedCallback() {
|
|
3625
|
+
this.#unbindDirtyListeners();
|
|
3626
|
+
if (this.#dirtyFrame) {
|
|
3627
|
+
cancelAnimationFrame(this.#dirtyFrame);
|
|
3628
|
+
this.#dirtyFrame = 0;
|
|
3629
|
+
}
|
|
3630
|
+
this.#header?.removeEventListener("click", this.#handleToggle);
|
|
3631
|
+
this.#header?.removeEventListener("keydown", this.#handleHeaderKeyDown);
|
|
3632
|
+
const resetBtn = this.#resetTooltip?.querySelector("fig-button");
|
|
3633
|
+
resetBtn?.removeEventListener("click", this.#handleReset);
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
3637
|
+
if (oldValue === newValue) return;
|
|
3638
|
+
if (name === "open") {
|
|
3639
|
+
this.#header?.setAttribute("aria-expanded", String(this.open));
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
if (name === "show-reset") {
|
|
3643
|
+
this.#syncResetButton();
|
|
3644
|
+
this.#syncDirtyState();
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (name === "disabled") {
|
|
3648
|
+
this.#syncDisabled();
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
this.#render();
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
get open() {
|
|
3655
|
+
const attr = this.getAttribute("open");
|
|
3656
|
+
return attr !== null && attr !== "false";
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
set open(value) {
|
|
3660
|
+
const was = this.open;
|
|
3661
|
+
if (value) {
|
|
3662
|
+
this.setAttribute("open", "true");
|
|
3663
|
+
} else {
|
|
3664
|
+
this.setAttribute("open", "false");
|
|
3665
|
+
}
|
|
3666
|
+
this.#header?.setAttribute("aria-expanded", String(!!value));
|
|
3667
|
+
if (was !== !!value) {
|
|
3668
|
+
this.dispatchEvent(
|
|
3669
|
+
new CustomEvent("openchange", {
|
|
3670
|
+
detail: { open: !!value },
|
|
3671
|
+
bubbles: true,
|
|
3672
|
+
}),
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
/** When true (default), show the reset control while the group is dirty. */
|
|
3678
|
+
get showReset() {
|
|
3679
|
+
const attr = this.getAttribute("show-reset");
|
|
3680
|
+
if (attr === null) return true;
|
|
3681
|
+
return attr !== "false";
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
set showReset(value) {
|
|
3685
|
+
if (value) this.setAttribute("show-reset", "true");
|
|
3686
|
+
else this.setAttribute("show-reset", "false");
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
get dirty() {
|
|
3690
|
+
return this.hasAttribute("data-dirty") && this.getAttribute("data-dirty") !== "false";
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
/** Restore all propskit controls in this group to their captured defaults. */
|
|
3694
|
+
resetProperties() {
|
|
3695
|
+
this.#resetControls();
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
#bindDirtyListeners() {
|
|
3699
|
+
this.removeEventListener("input", this.#boundOnControlEvent, true);
|
|
3700
|
+
this.removeEventListener("change", this.#boundOnControlEvent, true);
|
|
3701
|
+
this.addEventListener("input", this.#boundOnControlEvent, true);
|
|
3702
|
+
this.addEventListener("change", this.#boundOnControlEvent, true);
|
|
3703
|
+
|
|
3704
|
+
if (!this.#childObserver) {
|
|
3705
|
+
this.#childObserver = new MutationObserver(() => {
|
|
3706
|
+
this.#syncDisabled();
|
|
3707
|
+
this.#queueDirtySync();
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
this.#childObserver.disconnect();
|
|
3711
|
+
this.#childObserver.observe(this, {
|
|
3712
|
+
childList: true,
|
|
3713
|
+
subtree: true,
|
|
3714
|
+
attributes: true,
|
|
3715
|
+
attributeFilter: ["value", "checked", "default", "x", "y"],
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
#unbindDirtyListeners() {
|
|
3720
|
+
this.removeEventListener("input", this.#boundOnControlEvent, true);
|
|
3721
|
+
this.removeEventListener("change", this.#boundOnControlEvent, true);
|
|
3722
|
+
this.#childObserver?.disconnect();
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
#queueDirtySync() {
|
|
3726
|
+
if (this.#dirtyFrame) return;
|
|
3727
|
+
this.#dirtyFrame = requestAnimationFrame(() => {
|
|
3728
|
+
this.#dirtyFrame = 0;
|
|
3729
|
+
this.#syncDirtyState();
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
#isResetTarget(target) {
|
|
3734
|
+
return (
|
|
3735
|
+
target instanceof Element &&
|
|
3736
|
+
Boolean(
|
|
3737
|
+
target.closest(
|
|
3738
|
+
".propskit-group-reset, .propskit-group-reset-tooltip",
|
|
3739
|
+
),
|
|
3740
|
+
)
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
#handleToggle = (e) => {
|
|
3745
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3746
|
+
if (this.#isResetTarget(e.target)) return;
|
|
3747
|
+
e.stopPropagation();
|
|
3748
|
+
this.open = !this.open;
|
|
3749
|
+
};
|
|
3750
|
+
|
|
3751
|
+
#handleHeaderKeyDown = (e) => {
|
|
3752
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3753
|
+
if (this.#isResetTarget(e.target)) return;
|
|
3754
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
3755
|
+
e.preventDefault();
|
|
3756
|
+
e.stopPropagation();
|
|
3757
|
+
this.open = !this.open;
|
|
3758
|
+
};
|
|
3759
|
+
|
|
3760
|
+
#handleReset = (e) => {
|
|
3761
|
+
e.preventDefault();
|
|
3762
|
+
e.stopPropagation();
|
|
3763
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3764
|
+
this.#resetControls();
|
|
3765
|
+
};
|
|
3766
|
+
|
|
3767
|
+
#controls() {
|
|
3768
|
+
return [
|
|
3769
|
+
...this.querySelectorAll(PropskitGroup.#CONTROL_SELECTOR),
|
|
3770
|
+
].filter(
|
|
3771
|
+
(el) =>
|
|
3772
|
+
el.closest("propskit-group") === this &&
|
|
3773
|
+
!el.parentElement?.closest(PropskitGroup.#CONTROL_SELECTOR),
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
#syncDisabled() {
|
|
3778
|
+
const disabled = figLabBooleanAttribute(this, "disabled");
|
|
3779
|
+
for (const control of this.#controls()) {
|
|
3780
|
+
if (disabled) {
|
|
3781
|
+
if (!figLabBooleanAttribute(control, "disabled")) {
|
|
3782
|
+
control.setAttribute("disabled", "");
|
|
3783
|
+
control.setAttribute("data-propskit-group-disabled", "");
|
|
3784
|
+
}
|
|
3785
|
+
} else if (control.hasAttribute("data-propskit-group-disabled")) {
|
|
3786
|
+
control.removeAttribute("disabled");
|
|
3787
|
+
control.removeAttribute("data-propskit-group-disabled");
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
this.#header?.setAttribute("aria-disabled", String(disabled));
|
|
3791
|
+
this.#header?.setAttribute("tabindex", disabled ? "-1" : "0");
|
|
3792
|
+
this.#resetTooltip
|
|
3793
|
+
?.querySelector("fig-button")
|
|
3794
|
+
?.toggleAttribute("disabled", disabled);
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
#controlIsDirty(el) {
|
|
3798
|
+
return el.isDefault === false;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
#computeDirty() {
|
|
3802
|
+
for (const el of this.#controls()) {
|
|
3803
|
+
if (this.#controlIsDirty(el)) return true;
|
|
3804
|
+
}
|
|
3805
|
+
return false;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
#syncDirtyState() {
|
|
3809
|
+
const dirty = this.#computeDirty();
|
|
3810
|
+
if (dirty) this.setAttribute("data-dirty", "");
|
|
3811
|
+
else this.removeAttribute("data-dirty");
|
|
3812
|
+
this.#syncResetButton();
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
#restoreControl(el) {
|
|
3816
|
+
if (typeof el.resetToDefault === "function") {
|
|
3817
|
+
el.resetToDefault();
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
#resetControls() {
|
|
3822
|
+
for (const el of this.#controls()) this.#restoreControl(el);
|
|
3823
|
+
this.#syncDirtyState();
|
|
3824
|
+
this.dispatchEvent(
|
|
3825
|
+
new CustomEvent("reset", {
|
|
3826
|
+
bubbles: true,
|
|
3827
|
+
composed: true,
|
|
3828
|
+
}),
|
|
3829
|
+
);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
#syncResetButton() {
|
|
3833
|
+
if (!this.showReset) {
|
|
3834
|
+
this.#resetTooltip?.setAttribute("hidden", "");
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
this.#ensureResetButton();
|
|
3838
|
+
this.#resetTooltip?.removeAttribute("hidden");
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
#ensureResetButton() {
|
|
3842
|
+
if (!this.#header || !this.showReset) return;
|
|
3843
|
+
let tip = this.#header.querySelector(":scope > .propskit-group-reset-tooltip");
|
|
3844
|
+
if (!tip) {
|
|
3845
|
+
tip = document.createElement("fig-tooltip");
|
|
3846
|
+
tip.className = "propskit-group-reset-tooltip";
|
|
3847
|
+
tip.setAttribute("text", "Reset properties");
|
|
3848
|
+
const btn = document.createElement("fig-button");
|
|
2270
3849
|
btn.className = "propskit-group-reset";
|
|
2271
3850
|
btn.setAttribute("variant", "ghost");
|
|
2272
3851
|
btn.setAttribute("icon", "");
|
|
@@ -2352,7 +3931,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
2352
3931
|
this.#chevron = h3.querySelector(".propskit-group-chevron");
|
|
2353
3932
|
this.#syncResetButton();
|
|
2354
3933
|
this.#header.setAttribute("role", "button");
|
|
2355
|
-
this.#
|
|
3934
|
+
this.#syncDisabled();
|
|
2356
3935
|
this.#header.setAttribute("aria-expanded", String(this.open));
|
|
2357
3936
|
this.#header.removeEventListener("click", this.#handleToggle);
|
|
2358
3937
|
this.#header.addEventListener("click", this.#handleToggle);
|
|
@@ -3165,6 +4744,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
3165
4744
|
|
|
3166
4745
|
#forwardSliderEvent(type, event) {
|
|
3167
4746
|
event.stopImmediatePropagation();
|
|
4747
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3168
4748
|
if (type === "change") {
|
|
3169
4749
|
this.#resetElasticPull();
|
|
3170
4750
|
}
|
|
@@ -3437,6 +5017,14 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3437
5017
|
if (typeof v.angle === "number") this.#angle = v.angle;
|
|
3438
5018
|
if (typeof v.x2 === "number") this.#x2 = v.x2;
|
|
3439
5019
|
if (typeof v.y2 === "number") this.#y2 = v.y2;
|
|
5020
|
+
if (
|
|
5021
|
+
this.#type === "color" &&
|
|
5022
|
+
typeof v.color === "string" &&
|
|
5023
|
+
v.color.trim() &&
|
|
5024
|
+
this.getAttribute("color") !== v.color.trim()
|
|
5025
|
+
) {
|
|
5026
|
+
this.setAttribute("color", v.color.trim());
|
|
5027
|
+
}
|
|
3440
5028
|
} catch {
|
|
3441
5029
|
/* ignore */
|
|
3442
5030
|
}
|
|
@@ -4895,7 +6483,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4895
6483
|
this.#syncViewportSize();
|
|
4896
6484
|
this.#updateWaveform();
|
|
4897
6485
|
this.#setupEvents();
|
|
4898
|
-
this.#startPlayhead();
|
|
6486
|
+
if (!this.#isDisabled()) this.#startPlayhead();
|
|
4899
6487
|
figLabConnectPropskitResetMenu(this);
|
|
4900
6488
|
}
|
|
4901
6489
|
|
|
@@ -4945,6 +6533,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4945
6533
|
}
|
|
4946
6534
|
|
|
4947
6535
|
#getWaveTypeMenuHTML(disabled, index) {
|
|
6536
|
+
const disabledAttr = disabled ? " disabled" : "";
|
|
4948
6537
|
const items = PropskitOscillator.TYPES.map((type) => {
|
|
4949
6538
|
return `<fig-menu-item value="${type.value}">
|
|
4950
6539
|
${PropskitOscillator.waveIcon(type.value, 24)}
|
|
@@ -4952,9 +6541,9 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4952
6541
|
</fig-menu-item>`;
|
|
4953
6542
|
}).join("");
|
|
4954
6543
|
const indexAttr = ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
|
|
4955
|
-
return `<fig-menu class="propskit-oscillator-add-type" position="bottom right"${indexAttr}${
|
|
6544
|
+
return `<fig-menu class="propskit-oscillator-add-type" position="bottom right"${indexAttr}${disabledAttr}>
|
|
4956
6545
|
<fig-tooltip text="Add form">
|
|
4957
|
-
<fig-button class="propskit-oscillator-add-type-button" variant="ghost" icon fig-menu-trigger aria-label="Add form"><fig-icon name="plus"></fig-icon></fig-button>
|
|
6546
|
+
<fig-button class="propskit-oscillator-add-type-button" variant="ghost" icon fig-menu-trigger aria-label="Add form"${disabledAttr}><fig-icon name="plus"></fig-icon></fig-button>
|
|
4958
6547
|
</fig-tooltip>
|
|
4959
6548
|
${items}
|
|
4960
6549
|
</fig-menu>`;
|
|
@@ -6239,7 +7828,12 @@ class FigReorder extends HTMLElement {
|
|
|
6239
7828
|
"fig-input-angle",
|
|
6240
7829
|
"fig-input-joystick",
|
|
6241
7830
|
"fig-canvas-control",
|
|
7831
|
+
"propskit-color-point",
|
|
6242
7832
|
"propskit-number",
|
|
7833
|
+
"propskit-point-point",
|
|
7834
|
+
"propskit-point-radius",
|
|
7835
|
+
"propskit-point-radius-angle",
|
|
7836
|
+
"propskit-position",
|
|
6243
7837
|
"propskit-slider",
|
|
6244
7838
|
"propskit-oscillator",
|
|
6245
7839
|
];
|