@rogieking/figui3 2.38.3 → 3.0.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 +11 -8
- package/components.css +87 -23
- package/dist/fig.js +72 -72
- package/fig.js +540 -288
- package/package.json +2 -1
- package/polyfills/custom-elements-webkit.js +216 -0
package/fig.js
CHANGED
|
@@ -2,6 +2,76 @@
|
|
|
2
2
|
* Generates a unique ID string using timestamp and random values
|
|
3
3
|
* @returns {string} A unique identifier
|
|
4
4
|
*/
|
|
5
|
+
function figIsWebKitOrIOSBrowser() {
|
|
6
|
+
if (typeof navigator === "undefined") {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const userAgent = navigator.userAgent || "";
|
|
10
|
+
const isIOSBrowser =
|
|
11
|
+
/\b(iPad|iPhone|iPod)\b/.test(userAgent) ||
|
|
12
|
+
/\bMacintosh\b/.test(userAgent) && /\bMobile\b/.test(userAgent);
|
|
13
|
+
const isDesktopWebKit =
|
|
14
|
+
/\bAppleWebKit\b/.test(userAgent) &&
|
|
15
|
+
!/\b(Chrome|Chromium|Edg|OPR|SamsungBrowser)\b/.test(userAgent);
|
|
16
|
+
return isIOSBrowser || isDesktopWebKit;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function figSupportsCustomizedBuiltIns() {
|
|
20
|
+
if (
|
|
21
|
+
typeof window === "undefined" ||
|
|
22
|
+
!window.customElements ||
|
|
23
|
+
typeof HTMLButtonElement === "undefined"
|
|
24
|
+
) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const testName = `fig-builtin-probe-${Math.random().toString(36).slice(2)}`;
|
|
29
|
+
class FigCustomizedBuiltInProbe extends HTMLButtonElement {}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
customElements.define(testName, FigCustomizedBuiltInProbe, {
|
|
33
|
+
extends: "button",
|
|
34
|
+
});
|
|
35
|
+
const probe = document.createElement("button", { is: testName });
|
|
36
|
+
return probe instanceof FigCustomizedBuiltInProbe;
|
|
37
|
+
} catch (_error) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const figNeedsBuiltInPolyfill =
|
|
43
|
+
figIsWebKitOrIOSBrowser() && !figSupportsCustomizedBuiltIns();
|
|
44
|
+
const figBuiltInPolyfillReady = (figNeedsBuiltInPolyfill
|
|
45
|
+
? import("./polyfills/custom-elements-webkit.js")
|
|
46
|
+
: Promise.resolve()
|
|
47
|
+
)
|
|
48
|
+
.then(() => {})
|
|
49
|
+
.catch((error) => {
|
|
50
|
+
throw error;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function figDefineCustomizedBuiltIn(name, constructor, options) {
|
|
54
|
+
const define = () => {
|
|
55
|
+
if (!customElements.get(name)) {
|
|
56
|
+
customElements.define(name, constructor, options);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!figNeedsBuiltInPolyfill) {
|
|
61
|
+
define();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
figBuiltInPolyfillReady
|
|
66
|
+
.then(define)
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
console.error(
|
|
69
|
+
`[figui3] Failed to load customized built-in polyfill for "${name}".`,
|
|
70
|
+
error,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
5
75
|
function figUniqueId() {
|
|
6
76
|
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
|
7
77
|
}
|
|
@@ -1084,7 +1154,7 @@ class FigDialog extends HTMLDialogElement {
|
|
|
1084
1154
|
}
|
|
1085
1155
|
}
|
|
1086
1156
|
}
|
|
1087
|
-
|
|
1157
|
+
figDefineCustomizedBuiltIn("fig-dialog", FigDialog, { extends: "dialog" });
|
|
1088
1158
|
|
|
1089
1159
|
/* Popup */
|
|
1090
1160
|
/**
|
|
@@ -1097,44 +1167,84 @@ customElements.define("fig-dialog", FigDialog, { extends: "dialog" });
|
|
|
1097
1167
|
* @attr {boolean|string} open - Open when present and not "false".
|
|
1098
1168
|
*/
|
|
1099
1169
|
class FigPopup extends HTMLDialogElement {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1170
|
+
_anchorObserver = null;
|
|
1171
|
+
_contentObserver = null;
|
|
1172
|
+
_mutationObserver = null;
|
|
1173
|
+
_anchorTrackRAF = null;
|
|
1174
|
+
_lastAnchorRect = null;
|
|
1175
|
+
_isPopupActive = false;
|
|
1176
|
+
_boundReposition;
|
|
1177
|
+
_boundScroll;
|
|
1178
|
+
_boundOutsidePointerDown;
|
|
1179
|
+
_rafId = null;
|
|
1180
|
+
_anchorRef = null;
|
|
1181
|
+
|
|
1182
|
+
_isDragging = false;
|
|
1183
|
+
_dragPending = false;
|
|
1184
|
+
_dragStartPos = { x: 0, y: 0 };
|
|
1185
|
+
_dragOffset = { x: 0, y: 0 };
|
|
1186
|
+
_dragThreshold = 3;
|
|
1187
|
+
_boundPointerDown;
|
|
1188
|
+
_boundPointerMove;
|
|
1189
|
+
_boundPointerUp;
|
|
1190
|
+
_wasDragged = false;
|
|
1121
1191
|
|
|
1122
1192
|
constructor() {
|
|
1123
1193
|
super();
|
|
1124
|
-
this
|
|
1125
|
-
this
|
|
1194
|
+
this._boundReposition = this.queueReposition.bind(this);
|
|
1195
|
+
this._boundScroll = (e) => {
|
|
1126
1196
|
if (
|
|
1127
1197
|
this.open &&
|
|
1128
1198
|
!this.contains(e.target) &&
|
|
1129
|
-
this
|
|
1199
|
+
this.shouldAutoReposition()
|
|
1130
1200
|
) {
|
|
1131
|
-
this
|
|
1201
|
+
this.queueReposition();
|
|
1132
1202
|
}
|
|
1133
1203
|
};
|
|
1134
|
-
this
|
|
1135
|
-
this
|
|
1136
|
-
this
|
|
1137
|
-
this
|
|
1204
|
+
this._boundOutsidePointerDown = this.handleOutsidePointerDown.bind(this);
|
|
1205
|
+
this._boundPointerDown = this.handlePointerDown.bind(this);
|
|
1206
|
+
this._boundPointerMove = this.handlePointerMove.bind(this);
|
|
1207
|
+
this._boundPointerUp = this.handlePointerUp.bind(this);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
ensureInitialized() {
|
|
1211
|
+
if (typeof this._anchorObserver === "undefined") this._anchorObserver = null;
|
|
1212
|
+
if (typeof this._contentObserver === "undefined") this._contentObserver = null;
|
|
1213
|
+
if (typeof this._mutationObserver === "undefined") this._mutationObserver = null;
|
|
1214
|
+
if (typeof this._anchorTrackRAF === "undefined") this._anchorTrackRAF = null;
|
|
1215
|
+
if (typeof this._lastAnchorRect === "undefined") this._lastAnchorRect = null;
|
|
1216
|
+
if (typeof this._isPopupActive === "undefined") this._isPopupActive = false;
|
|
1217
|
+
if (typeof this._rafId === "undefined") this._rafId = null;
|
|
1218
|
+
if (typeof this._anchorRef === "undefined") this._anchorRef = null;
|
|
1219
|
+
if (typeof this._isDragging === "undefined") this._isDragging = false;
|
|
1220
|
+
if (typeof this._dragPending === "undefined") this._dragPending = false;
|
|
1221
|
+
if (typeof this._dragStartPos === "undefined") this._dragStartPos = { x: 0, y: 0 };
|
|
1222
|
+
if (typeof this._dragOffset === "undefined") this._dragOffset = { x: 0, y: 0 };
|
|
1223
|
+
if (typeof this._dragThreshold !== "number") this._dragThreshold = 3;
|
|
1224
|
+
if (typeof this._wasDragged === "undefined") this._wasDragged = false;
|
|
1225
|
+
|
|
1226
|
+
if (typeof this._boundReposition !== "function") {
|
|
1227
|
+
this._boundReposition = this.queueReposition.bind(this);
|
|
1228
|
+
}
|
|
1229
|
+
if (typeof this._boundScroll !== "function") {
|
|
1230
|
+
this._boundScroll = (e) => {
|
|
1231
|
+
if (this.open && !this.contains(e.target) && this.shouldAutoReposition()) {
|
|
1232
|
+
this.queueReposition();
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof this._boundOutsidePointerDown !== "function") {
|
|
1237
|
+
this._boundOutsidePointerDown = this.handleOutsidePointerDown.bind(this);
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof this._boundPointerDown !== "function") {
|
|
1240
|
+
this._boundPointerDown = this.handlePointerDown.bind(this);
|
|
1241
|
+
}
|
|
1242
|
+
if (typeof this._boundPointerMove !== "function") {
|
|
1243
|
+
this._boundPointerMove = this.handlePointerMove.bind(this);
|
|
1244
|
+
}
|
|
1245
|
+
if (typeof this._boundPointerUp !== "function") {
|
|
1246
|
+
this._boundPointerUp = this.handlePointerUp.bind(this);
|
|
1247
|
+
}
|
|
1138
1248
|
}
|
|
1139
1249
|
|
|
1140
1250
|
static get observedAttributes() {
|
|
@@ -1167,22 +1277,23 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1167
1277
|
}
|
|
1168
1278
|
|
|
1169
1279
|
get anchor() {
|
|
1170
|
-
return this
|
|
1280
|
+
return this._anchorRef ?? this.getAttribute("anchor");
|
|
1171
1281
|
}
|
|
1172
1282
|
|
|
1173
1283
|
set anchor(value) {
|
|
1174
1284
|
if (value instanceof Element) {
|
|
1175
|
-
this
|
|
1285
|
+
this._anchorRef = value;
|
|
1176
1286
|
} else if (typeof value === "string") {
|
|
1177
|
-
this
|
|
1287
|
+
this._anchorRef = null;
|
|
1178
1288
|
this.setAttribute("anchor", value);
|
|
1179
1289
|
} else {
|
|
1180
|
-
this
|
|
1290
|
+
this._anchorRef = null;
|
|
1181
1291
|
}
|
|
1182
|
-
if (this.open) this
|
|
1292
|
+
if (this.open) this.queueReposition();
|
|
1183
1293
|
}
|
|
1184
1294
|
|
|
1185
1295
|
connectedCallback() {
|
|
1296
|
+
this.ensureInitialized();
|
|
1186
1297
|
if (!this.hasAttribute("position")) {
|
|
1187
1298
|
this.setAttribute("position", "top center");
|
|
1188
1299
|
}
|
|
@@ -1197,68 +1308,70 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1197
1308
|
this.hasAttribute("drag") && this.getAttribute("drag") !== "false";
|
|
1198
1309
|
|
|
1199
1310
|
this.addEventListener("close", () => {
|
|
1200
|
-
this
|
|
1311
|
+
this.teardownObservers();
|
|
1201
1312
|
if (this.hasAttribute("open")) {
|
|
1202
1313
|
this.removeAttribute("open");
|
|
1203
1314
|
}
|
|
1204
1315
|
});
|
|
1205
1316
|
|
|
1206
1317
|
requestAnimationFrame(() => {
|
|
1207
|
-
this
|
|
1318
|
+
this.setupDragListeners();
|
|
1208
1319
|
});
|
|
1209
1320
|
|
|
1210
1321
|
if (this.open) {
|
|
1211
|
-
this
|
|
1322
|
+
this.showPopup();
|
|
1212
1323
|
} else {
|
|
1213
|
-
this
|
|
1324
|
+
this.hidePopup();
|
|
1214
1325
|
}
|
|
1215
1326
|
}
|
|
1216
1327
|
|
|
1217
1328
|
disconnectedCallback() {
|
|
1218
|
-
this
|
|
1219
|
-
this
|
|
1329
|
+
this.ensureInitialized();
|
|
1330
|
+
this.teardownObservers();
|
|
1331
|
+
this.removeDragListeners();
|
|
1220
1332
|
document.removeEventListener(
|
|
1221
1333
|
"pointerdown",
|
|
1222
|
-
this
|
|
1334
|
+
this._boundOutsidePointerDown,
|
|
1223
1335
|
true,
|
|
1224
1336
|
);
|
|
1225
|
-
if (this
|
|
1226
|
-
cancelAnimationFrame(this
|
|
1227
|
-
this
|
|
1337
|
+
if (this._rafId !== null) {
|
|
1338
|
+
cancelAnimationFrame(this._rafId);
|
|
1339
|
+
this._rafId = null;
|
|
1228
1340
|
}
|
|
1229
1341
|
}
|
|
1230
1342
|
|
|
1231
1343
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
1344
|
+
this.ensureInitialized();
|
|
1232
1345
|
if (oldValue === newValue) return;
|
|
1233
1346
|
|
|
1234
1347
|
if (name === "open") {
|
|
1235
1348
|
if (newValue === null || newValue === "false") {
|
|
1236
|
-
this
|
|
1349
|
+
this.hidePopup();
|
|
1237
1350
|
return;
|
|
1238
1351
|
}
|
|
1239
|
-
this
|
|
1352
|
+
this.showPopup();
|
|
1240
1353
|
return;
|
|
1241
1354
|
}
|
|
1242
1355
|
|
|
1243
1356
|
if (name === "drag") {
|
|
1244
1357
|
this.drag = newValue !== null && newValue !== "false";
|
|
1245
1358
|
if (this.drag) {
|
|
1246
|
-
this
|
|
1359
|
+
this.setupDragListeners();
|
|
1247
1360
|
} else {
|
|
1248
|
-
this
|
|
1361
|
+
this.removeDragListeners();
|
|
1249
1362
|
}
|
|
1250
1363
|
return;
|
|
1251
1364
|
}
|
|
1252
1365
|
|
|
1253
1366
|
if (this.open) {
|
|
1254
|
-
this
|
|
1255
|
-
this
|
|
1367
|
+
this.queueReposition();
|
|
1368
|
+
this.setupObservers();
|
|
1256
1369
|
}
|
|
1257
1370
|
}
|
|
1258
1371
|
|
|
1259
|
-
|
|
1260
|
-
if (this
|
|
1261
|
-
this
|
|
1372
|
+
showPopup() {
|
|
1373
|
+
if (this._isPopupActive) {
|
|
1374
|
+
this.queueReposition();
|
|
1262
1375
|
return;
|
|
1263
1376
|
}
|
|
1264
1377
|
|
|
@@ -1275,30 +1388,30 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1275
1388
|
}
|
|
1276
1389
|
}
|
|
1277
1390
|
|
|
1278
|
-
this
|
|
1391
|
+
this.setupObservers();
|
|
1279
1392
|
document.addEventListener(
|
|
1280
1393
|
"pointerdown",
|
|
1281
|
-
this
|
|
1394
|
+
this._boundOutsidePointerDown,
|
|
1282
1395
|
true,
|
|
1283
1396
|
);
|
|
1284
|
-
this
|
|
1285
|
-
this
|
|
1286
|
-
this
|
|
1397
|
+
this._wasDragged = false;
|
|
1398
|
+
this.queueReposition();
|
|
1399
|
+
this._isPopupActive = true;
|
|
1287
1400
|
|
|
1288
|
-
const anchor = this
|
|
1401
|
+
const anchor = this.resolveAnchor();
|
|
1289
1402
|
if (anchor) anchor.classList.add("has-popup-open");
|
|
1290
1403
|
}
|
|
1291
1404
|
|
|
1292
|
-
|
|
1293
|
-
const anchor = this
|
|
1405
|
+
hidePopup() {
|
|
1406
|
+
const anchor = this.resolveAnchor();
|
|
1294
1407
|
if (anchor) anchor.classList.remove("has-popup-open");
|
|
1295
1408
|
|
|
1296
|
-
this
|
|
1297
|
-
this
|
|
1298
|
-
this
|
|
1409
|
+
this._isPopupActive = false;
|
|
1410
|
+
this._wasDragged = false;
|
|
1411
|
+
this.teardownObservers();
|
|
1299
1412
|
document.removeEventListener(
|
|
1300
1413
|
"pointerdown",
|
|
1301
|
-
this
|
|
1414
|
+
this._boundOutsidePointerDown,
|
|
1302
1415
|
true,
|
|
1303
1416
|
);
|
|
1304
1417
|
|
|
@@ -1316,59 +1429,59 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1316
1429
|
return val === null || val !== "false";
|
|
1317
1430
|
}
|
|
1318
1431
|
|
|
1319
|
-
|
|
1320
|
-
this
|
|
1432
|
+
setupObservers() {
|
|
1433
|
+
this.teardownObservers();
|
|
1321
1434
|
|
|
1322
|
-
const anchor = this
|
|
1435
|
+
const anchor = this.resolveAnchor();
|
|
1323
1436
|
if (anchor && "ResizeObserver" in window) {
|
|
1324
|
-
this
|
|
1325
|
-
this
|
|
1437
|
+
this._anchorObserver = new ResizeObserver(this._boundReposition);
|
|
1438
|
+
this._anchorObserver.observe(anchor);
|
|
1326
1439
|
}
|
|
1327
1440
|
|
|
1328
1441
|
if (this.autoresize) {
|
|
1329
1442
|
if ("ResizeObserver" in window) {
|
|
1330
|
-
this
|
|
1331
|
-
this
|
|
1443
|
+
this._contentObserver = new ResizeObserver(this._boundReposition);
|
|
1444
|
+
this._contentObserver.observe(this);
|
|
1332
1445
|
}
|
|
1333
1446
|
|
|
1334
|
-
this
|
|
1335
|
-
this
|
|
1447
|
+
this._mutationObserver = new MutationObserver(this._boundReposition);
|
|
1448
|
+
this._mutationObserver.observe(this, {
|
|
1336
1449
|
childList: true,
|
|
1337
1450
|
subtree: true,
|
|
1338
1451
|
characterData: true,
|
|
1339
1452
|
});
|
|
1340
1453
|
}
|
|
1341
1454
|
|
|
1342
|
-
window.addEventListener("resize", this
|
|
1343
|
-
window.addEventListener("scroll", this
|
|
1455
|
+
window.addEventListener("resize", this._boundReposition);
|
|
1456
|
+
window.addEventListener("scroll", this._boundScroll, {
|
|
1344
1457
|
capture: true,
|
|
1345
1458
|
passive: true,
|
|
1346
1459
|
});
|
|
1347
|
-
this
|
|
1460
|
+
this.startAnchorTracking();
|
|
1348
1461
|
}
|
|
1349
1462
|
|
|
1350
|
-
|
|
1351
|
-
if (this
|
|
1352
|
-
this
|
|
1353
|
-
this
|
|
1463
|
+
teardownObservers() {
|
|
1464
|
+
if (this._anchorObserver) {
|
|
1465
|
+
this._anchorObserver.disconnect();
|
|
1466
|
+
this._anchorObserver = null;
|
|
1354
1467
|
}
|
|
1355
|
-
if (this
|
|
1356
|
-
this
|
|
1357
|
-
this
|
|
1468
|
+
if (this._contentObserver) {
|
|
1469
|
+
this._contentObserver.disconnect();
|
|
1470
|
+
this._contentObserver = null;
|
|
1358
1471
|
}
|
|
1359
|
-
if (this
|
|
1360
|
-
this
|
|
1361
|
-
this
|
|
1472
|
+
if (this._mutationObserver) {
|
|
1473
|
+
this._mutationObserver.disconnect();
|
|
1474
|
+
this._mutationObserver = null;
|
|
1362
1475
|
}
|
|
1363
|
-
window.removeEventListener("resize", this
|
|
1364
|
-
window.removeEventListener("scroll", this
|
|
1476
|
+
window.removeEventListener("resize", this._boundReposition);
|
|
1477
|
+
window.removeEventListener("scroll", this._boundScroll, {
|
|
1365
1478
|
capture: true,
|
|
1366
1479
|
passive: true,
|
|
1367
1480
|
});
|
|
1368
|
-
this
|
|
1481
|
+
this.stopAnchorTracking();
|
|
1369
1482
|
}
|
|
1370
1483
|
|
|
1371
|
-
|
|
1484
|
+
readRectSnapshot(element) {
|
|
1372
1485
|
if (!element) return null;
|
|
1373
1486
|
const rect = element.getBoundingClientRect();
|
|
1374
1487
|
return {
|
|
@@ -1379,7 +1492,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1379
1492
|
};
|
|
1380
1493
|
}
|
|
1381
1494
|
|
|
1382
|
-
|
|
1495
|
+
hasRectChanged(prev, next, epsilon = 0.25) {
|
|
1383
1496
|
if (!prev && !next) return false;
|
|
1384
1497
|
if (!prev || !next) return true;
|
|
1385
1498
|
return (
|
|
@@ -1390,42 +1503,42 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1390
1503
|
);
|
|
1391
1504
|
}
|
|
1392
1505
|
|
|
1393
|
-
|
|
1394
|
-
this
|
|
1506
|
+
startAnchorTracking() {
|
|
1507
|
+
this.stopAnchorTracking();
|
|
1395
1508
|
if (!this.open) return;
|
|
1396
1509
|
|
|
1397
1510
|
const tick = () => {
|
|
1398
1511
|
if (!this.open) {
|
|
1399
|
-
this
|
|
1512
|
+
this._anchorTrackRAF = null;
|
|
1400
1513
|
return;
|
|
1401
1514
|
}
|
|
1402
1515
|
|
|
1403
|
-
const anchor = this
|
|
1404
|
-
const nextRect = this
|
|
1405
|
-
const canAutoReposition = this
|
|
1406
|
-
if (canAutoReposition && this
|
|
1407
|
-
this
|
|
1408
|
-
this
|
|
1516
|
+
const anchor = this.resolveAnchor();
|
|
1517
|
+
const nextRect = this.readRectSnapshot(anchor);
|
|
1518
|
+
const canAutoReposition = this.shouldAutoReposition();
|
|
1519
|
+
if (canAutoReposition && this.hasRectChanged(this._lastAnchorRect, nextRect)) {
|
|
1520
|
+
this._lastAnchorRect = nextRect;
|
|
1521
|
+
this.queueReposition();
|
|
1409
1522
|
} else if (!canAutoReposition) {
|
|
1410
1523
|
// Keep anchor geometry fresh without forcing reposition when user has dragged away.
|
|
1411
|
-
this
|
|
1524
|
+
this._lastAnchorRect = nextRect;
|
|
1412
1525
|
}
|
|
1413
|
-
this
|
|
1526
|
+
this._anchorTrackRAF = requestAnimationFrame(tick);
|
|
1414
1527
|
};
|
|
1415
1528
|
|
|
1416
|
-
this
|
|
1417
|
-
this
|
|
1529
|
+
this._lastAnchorRect = this.readRectSnapshot(this.resolveAnchor());
|
|
1530
|
+
this._anchorTrackRAF = requestAnimationFrame(tick);
|
|
1418
1531
|
}
|
|
1419
1532
|
|
|
1420
|
-
|
|
1421
|
-
if (this
|
|
1422
|
-
cancelAnimationFrame(this
|
|
1423
|
-
this
|
|
1533
|
+
stopAnchorTracking() {
|
|
1534
|
+
if (this._anchorTrackRAF !== null) {
|
|
1535
|
+
cancelAnimationFrame(this._anchorTrackRAF);
|
|
1536
|
+
this._anchorTrackRAF = null;
|
|
1424
1537
|
}
|
|
1425
|
-
this
|
|
1538
|
+
this._lastAnchorRect = null;
|
|
1426
1539
|
}
|
|
1427
1540
|
|
|
1428
|
-
|
|
1541
|
+
handleOutsidePointerDown(event) {
|
|
1429
1542
|
if (!this.open || !super.open) return;
|
|
1430
1543
|
const closedby = this.getAttribute("closedby");
|
|
1431
1544
|
if (closedby === "none" || closedby === "closerequest") return;
|
|
@@ -1433,15 +1546,15 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1433
1546
|
if (!(target instanceof Node)) return;
|
|
1434
1547
|
if (this.contains(target)) return;
|
|
1435
1548
|
|
|
1436
|
-
const anchor = this
|
|
1549
|
+
const anchor = this.resolveAnchor();
|
|
1437
1550
|
if (anchor && anchor.contains(target)) return;
|
|
1438
1551
|
|
|
1439
|
-
if (this
|
|
1552
|
+
if (this.isInsideDescendantPopup(target)) return;
|
|
1440
1553
|
|
|
1441
1554
|
this.open = false;
|
|
1442
1555
|
}
|
|
1443
1556
|
|
|
1444
|
-
|
|
1557
|
+
isInsideDescendantPopup(target) {
|
|
1445
1558
|
const targetDialog = target.closest?.('dialog[is="fig-popup"]');
|
|
1446
1559
|
if (!targetDialog || targetDialog === this) return false;
|
|
1447
1560
|
|
|
@@ -1459,19 +1572,19 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1459
1572
|
|
|
1460
1573
|
// ---- Drag support ----
|
|
1461
1574
|
|
|
1462
|
-
|
|
1575
|
+
setupDragListeners() {
|
|
1463
1576
|
if (this.drag) {
|
|
1464
|
-
this.addEventListener("pointerdown", this
|
|
1577
|
+
this.addEventListener("pointerdown", this._boundPointerDown);
|
|
1465
1578
|
}
|
|
1466
1579
|
}
|
|
1467
1580
|
|
|
1468
|
-
|
|
1469
|
-
this.removeEventListener("pointerdown", this
|
|
1470
|
-
document.removeEventListener("pointermove", this
|
|
1471
|
-
document.removeEventListener("pointerup", this
|
|
1581
|
+
removeDragListeners() {
|
|
1582
|
+
this.removeEventListener("pointerdown", this._boundPointerDown);
|
|
1583
|
+
document.removeEventListener("pointermove", this._boundPointerMove);
|
|
1584
|
+
document.removeEventListener("pointerup", this._boundPointerUp);
|
|
1472
1585
|
}
|
|
1473
1586
|
|
|
1474
|
-
|
|
1587
|
+
isInteractiveElement(element) {
|
|
1475
1588
|
const interactiveSelectors = [
|
|
1476
1589
|
"input",
|
|
1477
1590
|
"button",
|
|
@@ -1516,9 +1629,9 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1516
1629
|
return false;
|
|
1517
1630
|
}
|
|
1518
1631
|
|
|
1519
|
-
|
|
1632
|
+
handlePointerDown(e) {
|
|
1520
1633
|
if (!this.drag) return;
|
|
1521
|
-
if (this
|
|
1634
|
+
if (this.isInteractiveElement(e.target)) return;
|
|
1522
1635
|
|
|
1523
1636
|
const handleSelector = this.getAttribute("handle");
|
|
1524
1637
|
if (handleSelector && handleSelector.trim()) {
|
|
@@ -1526,27 +1639,27 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1526
1639
|
if (!handleEl || !handleEl.contains(e.target)) return;
|
|
1527
1640
|
}
|
|
1528
1641
|
|
|
1529
|
-
this
|
|
1530
|
-
this
|
|
1531
|
-
this
|
|
1642
|
+
this._dragPending = true;
|
|
1643
|
+
this._dragStartPos.x = e.clientX;
|
|
1644
|
+
this._dragStartPos.y = e.clientY;
|
|
1532
1645
|
|
|
1533
1646
|
const rect = this.getBoundingClientRect();
|
|
1534
|
-
this
|
|
1535
|
-
this
|
|
1647
|
+
this._dragOffset.x = e.clientX - rect.left;
|
|
1648
|
+
this._dragOffset.y = e.clientY - rect.top;
|
|
1536
1649
|
|
|
1537
|
-
document.addEventListener("pointermove", this
|
|
1538
|
-
document.addEventListener("pointerup", this
|
|
1650
|
+
document.addEventListener("pointermove", this._boundPointerMove);
|
|
1651
|
+
document.addEventListener("pointerup", this._boundPointerUp);
|
|
1539
1652
|
}
|
|
1540
1653
|
|
|
1541
|
-
|
|
1542
|
-
if (this
|
|
1543
|
-
const dx = Math.abs(e.clientX - this
|
|
1544
|
-
const dy = Math.abs(e.clientY - this
|
|
1654
|
+
handlePointerMove(e) {
|
|
1655
|
+
if (this._dragPending && !this._isDragging) {
|
|
1656
|
+
const dx = Math.abs(e.clientX - this._dragStartPos.x);
|
|
1657
|
+
const dy = Math.abs(e.clientY - this._dragStartPos.y);
|
|
1545
1658
|
|
|
1546
|
-
if (dx > this
|
|
1547
|
-
this
|
|
1548
|
-
this
|
|
1549
|
-
this
|
|
1659
|
+
if (dx > this._dragThreshold || dy > this._dragThreshold) {
|
|
1660
|
+
this._isDragging = true;
|
|
1661
|
+
this._dragPending = false;
|
|
1662
|
+
this._wasDragged = true;
|
|
1550
1663
|
this.setPointerCapture(e.pointerId);
|
|
1551
1664
|
this.style.cursor = "grabbing";
|
|
1552
1665
|
|
|
@@ -1559,31 +1672,31 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1559
1672
|
}
|
|
1560
1673
|
}
|
|
1561
1674
|
|
|
1562
|
-
if (!this
|
|
1675
|
+
if (!this._isDragging) return;
|
|
1563
1676
|
|
|
1564
|
-
this.style.left = `${e.clientX - this
|
|
1565
|
-
this.style.top = `${e.clientY - this
|
|
1677
|
+
this.style.left = `${e.clientX - this._dragOffset.x}px`;
|
|
1678
|
+
this.style.top = `${e.clientY - this._dragOffset.y}px`;
|
|
1566
1679
|
e.preventDefault();
|
|
1567
1680
|
}
|
|
1568
1681
|
|
|
1569
|
-
|
|
1570
|
-
if (this
|
|
1682
|
+
handlePointerUp(e) {
|
|
1683
|
+
if (this._isDragging) {
|
|
1571
1684
|
this.releasePointerCapture(e.pointerId);
|
|
1572
1685
|
this.style.cursor = "";
|
|
1573
1686
|
}
|
|
1574
1687
|
|
|
1575
|
-
this
|
|
1576
|
-
this
|
|
1688
|
+
this._isDragging = false;
|
|
1689
|
+
this._dragPending = false;
|
|
1577
1690
|
|
|
1578
|
-
document.removeEventListener("pointermove", this
|
|
1579
|
-
document.removeEventListener("pointerup", this
|
|
1691
|
+
document.removeEventListener("pointermove", this._boundPointerMove);
|
|
1692
|
+
document.removeEventListener("pointerup", this._boundPointerUp);
|
|
1580
1693
|
e.preventDefault();
|
|
1581
1694
|
}
|
|
1582
1695
|
|
|
1583
1696
|
// ---- Anchor resolution ----
|
|
1584
1697
|
|
|
1585
|
-
|
|
1586
|
-
if (this
|
|
1698
|
+
resolveAnchor() {
|
|
1699
|
+
if (this._anchorRef) return this._anchorRef;
|
|
1587
1700
|
|
|
1588
1701
|
const selector = this.getAttribute("anchor");
|
|
1589
1702
|
if (!selector) return null;
|
|
@@ -1599,7 +1712,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1599
1712
|
return document.querySelector(selector);
|
|
1600
1713
|
}
|
|
1601
1714
|
|
|
1602
|
-
|
|
1715
|
+
parsePosition() {
|
|
1603
1716
|
const raw = (this.getAttribute("position") || "top center")
|
|
1604
1717
|
.trim()
|
|
1605
1718
|
.toLowerCase();
|
|
@@ -1640,7 +1753,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1640
1753
|
return { vertical, horizontal, shorthand };
|
|
1641
1754
|
}
|
|
1642
1755
|
|
|
1643
|
-
|
|
1756
|
+
normalizeOffsetToken(token, fallback = "0px") {
|
|
1644
1757
|
if (!token) return fallback;
|
|
1645
1758
|
const trimmed = token.trim();
|
|
1646
1759
|
if (!trimmed) return fallback;
|
|
@@ -1650,9 +1763,9 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1650
1763
|
return trimmed;
|
|
1651
1764
|
}
|
|
1652
1765
|
|
|
1653
|
-
|
|
1766
|
+
measureLengthPx(value, axis = "x") {
|
|
1654
1767
|
if (!value) return 0;
|
|
1655
|
-
const normalized = this
|
|
1768
|
+
const normalized = this.normalizeOffsetToken(value, "0px");
|
|
1656
1769
|
if (normalized.endsWith("px")) {
|
|
1657
1770
|
const px = parseFloat(normalized);
|
|
1658
1771
|
return Number.isFinite(px) ? px : 0;
|
|
@@ -1680,22 +1793,22 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1680
1793
|
return axis === "x" ? rect.width : rect.height;
|
|
1681
1794
|
}
|
|
1682
1795
|
|
|
1683
|
-
|
|
1796
|
+
parseOffset() {
|
|
1684
1797
|
const raw = (this.getAttribute("offset") || "0 0").trim();
|
|
1685
1798
|
const tokens = raw.split(/\s+/).filter(Boolean);
|
|
1686
1799
|
|
|
1687
|
-
const xToken = this
|
|
1688
|
-
const yToken = this
|
|
1800
|
+
const xToken = this.normalizeOffsetToken(tokens[0], "0px");
|
|
1801
|
+
const yToken = this.normalizeOffsetToken(tokens[1], "0px");
|
|
1689
1802
|
|
|
1690
1803
|
return {
|
|
1691
1804
|
xToken,
|
|
1692
1805
|
yToken,
|
|
1693
|
-
xPx: this
|
|
1694
|
-
yPx: this
|
|
1806
|
+
xPx: this.measureLengthPx(xToken, "x"),
|
|
1807
|
+
yPx: this.measureLengthPx(yToken, "y"),
|
|
1695
1808
|
};
|
|
1696
1809
|
}
|
|
1697
1810
|
|
|
1698
|
-
|
|
1811
|
+
parseViewportMargins() {
|
|
1699
1812
|
const raw = (this.getAttribute("viewport-margin") || "8").trim();
|
|
1700
1813
|
const tokens = raw
|
|
1701
1814
|
.split(/\s+/)
|
|
@@ -1732,7 +1845,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1732
1845
|
};
|
|
1733
1846
|
}
|
|
1734
1847
|
|
|
1735
|
-
|
|
1848
|
+
getPlacementCandidates(vertical, horizontal, shorthand) {
|
|
1736
1849
|
const opp = {
|
|
1737
1850
|
top: "bottom",
|
|
1738
1851
|
bottom: "top",
|
|
@@ -1782,7 +1895,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1782
1895
|
];
|
|
1783
1896
|
}
|
|
1784
1897
|
|
|
1785
|
-
|
|
1898
|
+
computeCoords(
|
|
1786
1899
|
anchorRect,
|
|
1787
1900
|
popupRect,
|
|
1788
1901
|
vertical,
|
|
@@ -1841,7 +1954,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1841
1954
|
return { top, left };
|
|
1842
1955
|
}
|
|
1843
1956
|
|
|
1844
|
-
|
|
1957
|
+
oppositeSide(side) {
|
|
1845
1958
|
const map = {
|
|
1846
1959
|
top: "bottom",
|
|
1847
1960
|
bottom: "top",
|
|
@@ -1851,7 +1964,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1851
1964
|
return map[side] || "bottom";
|
|
1852
1965
|
}
|
|
1853
1966
|
|
|
1854
|
-
|
|
1967
|
+
getPlacementSide(vertical, horizontal, shorthand) {
|
|
1855
1968
|
if (shorthand === "top") return "top";
|
|
1856
1969
|
if (shorthand === "bottom") return "bottom";
|
|
1857
1970
|
if (shorthand === "left") return "left";
|
|
@@ -1862,14 +1975,14 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1862
1975
|
return "top";
|
|
1863
1976
|
}
|
|
1864
1977
|
|
|
1865
|
-
|
|
1978
|
+
updatePopoverBeak(anchorRect, popupRect, left, top, placementSide) {
|
|
1866
1979
|
if (this.getAttribute("variant") !== "popover" || !anchorRect) {
|
|
1867
1980
|
this.style.removeProperty("--beak-offset");
|
|
1868
1981
|
this.removeAttribute("data-beak-side");
|
|
1869
1982
|
return;
|
|
1870
1983
|
}
|
|
1871
1984
|
|
|
1872
|
-
const beakSide = this
|
|
1985
|
+
const beakSide = this.oppositeSide(placementSide);
|
|
1873
1986
|
this.setAttribute("data-beak-side", beakSide);
|
|
1874
1987
|
|
|
1875
1988
|
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
|
|
@@ -1898,7 +2011,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1898
2011
|
this.style.setProperty("--beak-offset", `${beakOffset}px`);
|
|
1899
2012
|
}
|
|
1900
2013
|
|
|
1901
|
-
|
|
2014
|
+
overflowScore(coords, popupRect, m) {
|
|
1902
2015
|
const vw = window.innerWidth;
|
|
1903
2016
|
const vh = window.innerHeight;
|
|
1904
2017
|
const right = coords.left + popupRect.width;
|
|
@@ -1912,11 +2025,11 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1912
2025
|
return overflowLeft + overflowTop + overflowRight + overflowBottom;
|
|
1913
2026
|
}
|
|
1914
2027
|
|
|
1915
|
-
|
|
1916
|
-
return this
|
|
2028
|
+
fits(coords, popupRect, m) {
|
|
2029
|
+
return this.overflowScore(coords, popupRect, m) === 0;
|
|
1917
2030
|
}
|
|
1918
2031
|
|
|
1919
|
-
|
|
2032
|
+
clamp(coords, popupRect, m) {
|
|
1920
2033
|
const minLeft = m.left;
|
|
1921
2034
|
const minTop = m.top;
|
|
1922
2035
|
const maxLeft = Math.max(
|
|
@@ -1934,17 +2047,17 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1934
2047
|
};
|
|
1935
2048
|
}
|
|
1936
2049
|
|
|
1937
|
-
|
|
2050
|
+
positionPopup() {
|
|
1938
2051
|
if (!this.open || !super.open) return;
|
|
1939
2052
|
|
|
1940
2053
|
const popupRect = this.getBoundingClientRect();
|
|
1941
|
-
const offset = this
|
|
1942
|
-
const { vertical, horizontal, shorthand } = this
|
|
1943
|
-
const anchor = this
|
|
1944
|
-
const m = this
|
|
2054
|
+
const offset = this.parseOffset();
|
|
2055
|
+
const { vertical, horizontal, shorthand } = this.parsePosition();
|
|
2056
|
+
const anchor = this.resolveAnchor();
|
|
2057
|
+
const m = this.parseViewportMargins();
|
|
1945
2058
|
|
|
1946
2059
|
if (!anchor) {
|
|
1947
|
-
this
|
|
2060
|
+
this.updatePopoverBeak(null, popupRect, 0, 0, "top");
|
|
1948
2061
|
const centered = {
|
|
1949
2062
|
left:
|
|
1950
2063
|
m.left + (window.innerWidth - m.right - m.left - popupRect.width) / 2,
|
|
@@ -1952,14 +2065,14 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1952
2065
|
m.top +
|
|
1953
2066
|
(window.innerHeight - m.bottom - m.top - popupRect.height) / 2,
|
|
1954
2067
|
};
|
|
1955
|
-
const clamped = this
|
|
2068
|
+
const clamped = this.clamp(centered, popupRect, m);
|
|
1956
2069
|
this.style.left = `${clamped.left}px`;
|
|
1957
2070
|
this.style.top = `${clamped.top}px`;
|
|
1958
2071
|
return;
|
|
1959
2072
|
}
|
|
1960
2073
|
|
|
1961
2074
|
const anchorRect = anchor.getBoundingClientRect();
|
|
1962
|
-
const candidates = this
|
|
2075
|
+
const candidates = this.getPlacementCandidates(
|
|
1963
2076
|
vertical,
|
|
1964
2077
|
horizontal,
|
|
1965
2078
|
shorthand,
|
|
@@ -1969,7 +2082,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1969
2082
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
1970
2083
|
|
|
1971
2084
|
for (const { v, h, s } of candidates) {
|
|
1972
|
-
const coords = this
|
|
2085
|
+
const coords = this.computeCoords(
|
|
1973
2086
|
anchorRect,
|
|
1974
2087
|
popupRect,
|
|
1975
2088
|
v,
|
|
@@ -1977,10 +2090,10 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1977
2090
|
offset,
|
|
1978
2091
|
s,
|
|
1979
2092
|
);
|
|
1980
|
-
const placementSide = this
|
|
2093
|
+
const placementSide = this.getPlacementSide(v, h, s);
|
|
1981
2094
|
|
|
1982
2095
|
if (s) {
|
|
1983
|
-
const clamped = this
|
|
2096
|
+
const clamped = this.clamp(coords, popupRect, m);
|
|
1984
2097
|
const primaryFits =
|
|
1985
2098
|
s === "left" || s === "right"
|
|
1986
2099
|
? coords.left >= m.left &&
|
|
@@ -1990,7 +2103,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1990
2103
|
if (primaryFits) {
|
|
1991
2104
|
this.style.left = `${clamped.left}px`;
|
|
1992
2105
|
this.style.top = `${clamped.top}px`;
|
|
1993
|
-
this
|
|
2106
|
+
this.updatePopoverBeak(
|
|
1994
2107
|
anchorRect,
|
|
1995
2108
|
popupRect,
|
|
1996
2109
|
clamped.left,
|
|
@@ -1999,17 +2112,17 @@ class FigPopup extends HTMLDialogElement {
|
|
|
1999
2112
|
);
|
|
2000
2113
|
return;
|
|
2001
2114
|
}
|
|
2002
|
-
const score = this
|
|
2115
|
+
const score = this.overflowScore(coords, popupRect, m);
|
|
2003
2116
|
if (score < bestScore) {
|
|
2004
2117
|
bestScore = score;
|
|
2005
2118
|
best = clamped;
|
|
2006
2119
|
bestSide = placementSide;
|
|
2007
2120
|
}
|
|
2008
2121
|
} else {
|
|
2009
|
-
if (this
|
|
2122
|
+
if (this.fits(coords, popupRect, m)) {
|
|
2010
2123
|
this.style.left = `${coords.left}px`;
|
|
2011
2124
|
this.style.top = `${coords.top}px`;
|
|
2012
|
-
this
|
|
2125
|
+
this.updatePopoverBeak(
|
|
2013
2126
|
anchorRect,
|
|
2014
2127
|
popupRect,
|
|
2015
2128
|
coords.left,
|
|
@@ -2018,7 +2131,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2018
2131
|
);
|
|
2019
2132
|
return;
|
|
2020
2133
|
}
|
|
2021
|
-
const score = this
|
|
2134
|
+
const score = this.overflowScore(coords, popupRect, m);
|
|
2022
2135
|
if (score < bestScore) {
|
|
2023
2136
|
bestScore = score;
|
|
2024
2137
|
best = coords;
|
|
@@ -2027,10 +2140,10 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2027
2140
|
}
|
|
2028
2141
|
}
|
|
2029
2142
|
|
|
2030
|
-
const clamped = this
|
|
2143
|
+
const clamped = this.clamp(best || { left: 0, top: 0 }, popupRect, m);
|
|
2031
2144
|
this.style.left = `${clamped.left}px`;
|
|
2032
2145
|
this.style.top = `${clamped.top}px`;
|
|
2033
|
-
this
|
|
2146
|
+
this.updatePopoverBeak(
|
|
2034
2147
|
anchorRect,
|
|
2035
2148
|
popupRect,
|
|
2036
2149
|
clamped.left,
|
|
@@ -2039,22 +2152,22 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2039
2152
|
);
|
|
2040
2153
|
}
|
|
2041
2154
|
|
|
2042
|
-
|
|
2043
|
-
if (!this.open || !this
|
|
2044
|
-
if (this
|
|
2155
|
+
queueReposition() {
|
|
2156
|
+
if (!this.open || !this.shouldAutoReposition()) return;
|
|
2157
|
+
if (this._rafId !== null) return;
|
|
2045
2158
|
|
|
2046
|
-
this
|
|
2047
|
-
this
|
|
2048
|
-
this
|
|
2159
|
+
this._rafId = requestAnimationFrame(() => {
|
|
2160
|
+
this._rafId = null;
|
|
2161
|
+
this.positionPopup();
|
|
2049
2162
|
});
|
|
2050
2163
|
}
|
|
2051
2164
|
|
|
2052
|
-
|
|
2053
|
-
if (!(this.drag && this
|
|
2054
|
-
return !this
|
|
2165
|
+
shouldAutoReposition() {
|
|
2166
|
+
if (!(this.drag && this._wasDragged)) return true;
|
|
2167
|
+
return !this.resolveAnchor();
|
|
2055
2168
|
}
|
|
2056
2169
|
}
|
|
2057
|
-
|
|
2170
|
+
figDefineCustomizedBuiltIn("fig-popup", FigPopup, { extends: "dialog" });
|
|
2058
2171
|
|
|
2059
2172
|
/* Tabs */
|
|
2060
2173
|
/**
|
|
@@ -5059,18 +5172,25 @@ customElements.define("fig-switch", FigSwitch);
|
|
|
5059
5172
|
* @attr {boolean} open - Whether the toast is visible
|
|
5060
5173
|
*/
|
|
5061
5174
|
class FigToast extends HTMLDialogElement {
|
|
5062
|
-
|
|
5063
|
-
|
|
5175
|
+
_defaultOffset = 16; // 1rem in pixels
|
|
5176
|
+
_autoCloseTimer = null;
|
|
5064
5177
|
|
|
5065
5178
|
constructor() {
|
|
5066
5179
|
super();
|
|
5067
5180
|
}
|
|
5068
5181
|
|
|
5069
|
-
|
|
5070
|
-
return parseInt(this.getAttribute("offset") ?? this
|
|
5182
|
+
getOffset() {
|
|
5183
|
+
return parseInt(this.getAttribute("offset") ?? this._defaultOffset);
|
|
5071
5184
|
}
|
|
5072
5185
|
|
|
5073
5186
|
connectedCallback() {
|
|
5187
|
+
if (typeof this._defaultOffset !== "number") {
|
|
5188
|
+
this._defaultOffset = 16;
|
|
5189
|
+
}
|
|
5190
|
+
if (typeof this._autoCloseTimer === "undefined") {
|
|
5191
|
+
this._autoCloseTimer = null;
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5074
5194
|
// Set default theme if not specified
|
|
5075
5195
|
if (!this.hasAttribute("theme")) {
|
|
5076
5196
|
this.setAttribute("theme", "dark");
|
|
@@ -5090,8 +5210,8 @@ class FigToast extends HTMLDialogElement {
|
|
|
5090
5210
|
}
|
|
5091
5211
|
|
|
5092
5212
|
requestAnimationFrame(() => {
|
|
5093
|
-
this
|
|
5094
|
-
this
|
|
5213
|
+
this.addCloseListeners();
|
|
5214
|
+
this.applyPosition();
|
|
5095
5215
|
|
|
5096
5216
|
// Auto-show if open attribute is explicitly true
|
|
5097
5217
|
if (shouldOpen) {
|
|
@@ -5101,46 +5221,46 @@ class FigToast extends HTMLDialogElement {
|
|
|
5101
5221
|
}
|
|
5102
5222
|
|
|
5103
5223
|
disconnectedCallback() {
|
|
5104
|
-
this
|
|
5224
|
+
this.clearAutoClose();
|
|
5105
5225
|
}
|
|
5106
5226
|
|
|
5107
|
-
|
|
5227
|
+
addCloseListeners() {
|
|
5108
5228
|
this.querySelectorAll("[close-toast]").forEach((button) => {
|
|
5109
|
-
button.removeEventListener("click", this
|
|
5110
|
-
button.addEventListener("click", this
|
|
5229
|
+
button.removeEventListener("click", this.handleClose);
|
|
5230
|
+
button.addEventListener("click", this.handleClose.bind(this));
|
|
5111
5231
|
});
|
|
5112
5232
|
}
|
|
5113
5233
|
|
|
5114
|
-
|
|
5234
|
+
handleClose() {
|
|
5115
5235
|
this.hideToast();
|
|
5116
5236
|
}
|
|
5117
5237
|
|
|
5118
|
-
|
|
5238
|
+
applyPosition() {
|
|
5119
5239
|
// Always bottom center
|
|
5120
5240
|
this.style.position = "fixed";
|
|
5121
5241
|
this.style.margin = "0";
|
|
5122
5242
|
this.style.top = "auto";
|
|
5123
|
-
this.style.bottom = `${this
|
|
5243
|
+
this.style.bottom = `${this.getOffset()}px`;
|
|
5124
5244
|
this.style.left = "50%";
|
|
5125
5245
|
this.style.right = "auto";
|
|
5126
5246
|
this.style.transform = "translateX(-50%)";
|
|
5127
5247
|
}
|
|
5128
5248
|
|
|
5129
|
-
|
|
5130
|
-
this
|
|
5249
|
+
startAutoClose() {
|
|
5250
|
+
this.clearAutoClose();
|
|
5131
5251
|
|
|
5132
5252
|
const duration = parseInt(this.getAttribute("duration") ?? "5000");
|
|
5133
5253
|
if (duration > 0) {
|
|
5134
|
-
this
|
|
5254
|
+
this._autoCloseTimer = setTimeout(() => {
|
|
5135
5255
|
this.hideToast();
|
|
5136
5256
|
}, duration);
|
|
5137
5257
|
}
|
|
5138
5258
|
}
|
|
5139
5259
|
|
|
5140
|
-
|
|
5141
|
-
if (this
|
|
5142
|
-
clearTimeout(this
|
|
5143
|
-
this
|
|
5260
|
+
clearAutoClose() {
|
|
5261
|
+
if (this._autoCloseTimer) {
|
|
5262
|
+
clearTimeout(this._autoCloseTimer);
|
|
5263
|
+
this._autoCloseTimer = null;
|
|
5144
5264
|
}
|
|
5145
5265
|
}
|
|
5146
5266
|
|
|
@@ -5149,8 +5269,8 @@ class FigToast extends HTMLDialogElement {
|
|
|
5149
5269
|
*/
|
|
5150
5270
|
showToast() {
|
|
5151
5271
|
this.show(); // Non-modal show
|
|
5152
|
-
this
|
|
5153
|
-
this
|
|
5272
|
+
this.applyPosition();
|
|
5273
|
+
this.startAutoClose();
|
|
5154
5274
|
this.dispatchEvent(new CustomEvent("toast-show", { bubbles: true }));
|
|
5155
5275
|
}
|
|
5156
5276
|
|
|
@@ -5158,7 +5278,7 @@ class FigToast extends HTMLDialogElement {
|
|
|
5158
5278
|
* Hide the toast notification
|
|
5159
5279
|
*/
|
|
5160
5280
|
hideToast() {
|
|
5161
|
-
this
|
|
5281
|
+
this.clearAutoClose();
|
|
5162
5282
|
this.close();
|
|
5163
5283
|
this.dispatchEvent(new CustomEvent("toast-hide", { bubbles: true }));
|
|
5164
5284
|
}
|
|
@@ -5169,7 +5289,7 @@ class FigToast extends HTMLDialogElement {
|
|
|
5169
5289
|
|
|
5170
5290
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
5171
5291
|
if (name === "offset") {
|
|
5172
|
-
this
|
|
5292
|
+
this.applyPosition();
|
|
5173
5293
|
}
|
|
5174
5294
|
|
|
5175
5295
|
if (name === "open") {
|
|
@@ -5181,7 +5301,7 @@ class FigToast extends HTMLDialogElement {
|
|
|
5181
5301
|
}
|
|
5182
5302
|
}
|
|
5183
5303
|
}
|
|
5184
|
-
|
|
5304
|
+
figDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
|
|
5185
5305
|
|
|
5186
5306
|
/* Combo Input */
|
|
5187
5307
|
/**
|
|
@@ -7375,12 +7495,24 @@ customElements.define("fig-origin-grid", FigOriginGrid);
|
|
|
7375
7495
|
|
|
7376
7496
|
/**
|
|
7377
7497
|
* A custom joystick input element.
|
|
7378
|
-
* @attr {string} value - The current position of the joystick (e.g., "
|
|
7498
|
+
* @attr {string} value - The current position of the joystick (e.g., "50% 50%").
|
|
7379
7499
|
* @attr {number} precision - The number of decimal places for the output.
|
|
7380
7500
|
* @attr {number} transform - A scaling factor for the output.
|
|
7381
|
-
* @attr {boolean}
|
|
7501
|
+
* @attr {boolean} fields - Whether to display X and Y inputs.
|
|
7502
|
+
* @attr {string} aspect-ratio - Aspect ratio for the joystick plane container.
|
|
7503
|
+
* @attr {string} axis-labels - Space-delimited labels. 1 token: top. 2 tokens: x y. 4 tokens: left right top bottom.
|
|
7382
7504
|
*/
|
|
7383
7505
|
class FigInputJoystick extends HTMLElement {
|
|
7506
|
+
#boundMouseDown = null;
|
|
7507
|
+
#boundTouchStart = null;
|
|
7508
|
+
#boundKeyDown = null;
|
|
7509
|
+
#boundKeyUp = null;
|
|
7510
|
+
#boundXInput = null;
|
|
7511
|
+
#boundYInput = null;
|
|
7512
|
+
#boundXFocusOut = null;
|
|
7513
|
+
#boundYFocusOut = null;
|
|
7514
|
+
#isSyncingValueAttr = false;
|
|
7515
|
+
|
|
7384
7516
|
constructor() {
|
|
7385
7517
|
super();
|
|
7386
7518
|
|
|
@@ -7393,6 +7525,14 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7393
7525
|
this.yInput = null;
|
|
7394
7526
|
this.coordinates = "screen"; // "screen" (0,0 top-left) or "math" (0,0 bottom-left)
|
|
7395
7527
|
this.#initialized = false;
|
|
7528
|
+
this.#boundMouseDown = (e) => this.#handleMouseDown(e);
|
|
7529
|
+
this.#boundTouchStart = (e) => this.#handleTouchStart(e);
|
|
7530
|
+
this.#boundKeyDown = (e) => this.#handleKeyDown(e);
|
|
7531
|
+
this.#boundKeyUp = (e) => this.#handleKeyUp(e);
|
|
7532
|
+
this.#boundXInput = (e) => this.#handleXInput(e);
|
|
7533
|
+
this.#boundYInput = (e) => this.#handleYInput(e);
|
|
7534
|
+
this.#boundXFocusOut = () => this.#handleFieldFocusOut();
|
|
7535
|
+
this.#boundYFocusOut = () => this.#handleFieldFocusOut();
|
|
7396
7536
|
}
|
|
7397
7537
|
|
|
7398
7538
|
#initialized = false;
|
|
@@ -7404,12 +7544,16 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7404
7544
|
this.precision = parseInt(this.precision);
|
|
7405
7545
|
this.transform = this.getAttribute("transform") || 1;
|
|
7406
7546
|
this.transform = Number(this.transform);
|
|
7407
|
-
this.text = this.getAttribute("text") === "true";
|
|
7408
7547
|
this.coordinates = this.getAttribute("coordinates") || "screen";
|
|
7548
|
+
this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
|
|
7549
|
+
if (!this.hasAttribute("value")) {
|
|
7550
|
+
this.setAttribute("value", "50% 50%");
|
|
7551
|
+
}
|
|
7409
7552
|
|
|
7410
7553
|
this.#render();
|
|
7411
7554
|
this.#setupListeners();
|
|
7412
7555
|
this.#syncHandlePosition();
|
|
7556
|
+
this.#syncValueAttribute();
|
|
7413
7557
|
this.#initialized = true;
|
|
7414
7558
|
});
|
|
7415
7559
|
}
|
|
@@ -7418,41 +7562,102 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7418
7562
|
#displayY(y) {
|
|
7419
7563
|
return this.coordinates === "math" ? 1 - y : y;
|
|
7420
7564
|
}
|
|
7565
|
+
|
|
7566
|
+
#syncAspectRatioVar(value) {
|
|
7567
|
+
if (value && value.trim()) {
|
|
7568
|
+
this.style.setProperty("--aspect-ratio", value.trim());
|
|
7569
|
+
} else {
|
|
7570
|
+
this.style.removeProperty("--aspect-ratio");
|
|
7571
|
+
}
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7421
7574
|
disconnectedCallback() {
|
|
7422
7575
|
this.#cleanupListeners();
|
|
7423
7576
|
}
|
|
7424
7577
|
|
|
7578
|
+
get #fieldsEnabled() {
|
|
7579
|
+
const fields = this.getAttribute("fields");
|
|
7580
|
+
if (fields === null) return false;
|
|
7581
|
+
return fields.toLowerCase() !== "false";
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7425
7584
|
#render() {
|
|
7426
7585
|
this.innerHTML = this.#getInnerHTML();
|
|
7427
7586
|
}
|
|
7587
|
+
|
|
7588
|
+
#getAxisLabels() {
|
|
7589
|
+
const raw = (this.getAttribute("axis-labels") || "").trim();
|
|
7590
|
+
if (!raw) {
|
|
7591
|
+
return { left: "", right: "", top: "", bottom: "", leftNoRotate: false };
|
|
7592
|
+
}
|
|
7593
|
+
const tokens = raw.split(/\s+/).filter(Boolean);
|
|
7594
|
+
if (tokens.length === 1) {
|
|
7595
|
+
return {
|
|
7596
|
+
left: "",
|
|
7597
|
+
right: "",
|
|
7598
|
+
top: tokens[0],
|
|
7599
|
+
bottom: "",
|
|
7600
|
+
leftNoRotate: false,
|
|
7601
|
+
};
|
|
7602
|
+
}
|
|
7603
|
+
if (tokens.length === 2) {
|
|
7604
|
+
const [x, y] = tokens;
|
|
7605
|
+
return { left: x, right: "", top: "", bottom: y, leftNoRotate: true };
|
|
7606
|
+
}
|
|
7607
|
+
if (tokens.length === 4) {
|
|
7608
|
+
const [left, right, top, bottom] = tokens;
|
|
7609
|
+
return { left, right, top, bottom, leftNoRotate: false };
|
|
7610
|
+
}
|
|
7611
|
+
return { left: "", right: "", top: "", bottom: "", leftNoRotate: false };
|
|
7612
|
+
}
|
|
7613
|
+
|
|
7428
7614
|
#getInnerHTML() {
|
|
7615
|
+
const axisLabels = this.#getAxisLabels();
|
|
7616
|
+
const labelsMarkup = [
|
|
7617
|
+
axisLabels.left
|
|
7618
|
+
? `<label class="fig-joystick-axis-label left${axisLabels.leftNoRotate ? " no-rotate" : ""}" aria-hidden="true">${axisLabels.left}</label>`
|
|
7619
|
+
: "",
|
|
7620
|
+
axisLabels.right
|
|
7621
|
+
? `<label class="fig-joystick-axis-label right" aria-hidden="true">${axisLabels.right}</label>`
|
|
7622
|
+
: "",
|
|
7623
|
+
axisLabels.top
|
|
7624
|
+
? `<label class="fig-joystick-axis-label top" aria-hidden="true">${axisLabels.top}</label>`
|
|
7625
|
+
: "",
|
|
7626
|
+
axisLabels.bottom
|
|
7627
|
+
? `<label class="fig-joystick-axis-label bottom" aria-hidden="true">${axisLabels.bottom}</label>`
|
|
7628
|
+
: "",
|
|
7629
|
+
].join("");
|
|
7630
|
+
|
|
7429
7631
|
return `
|
|
7430
7632
|
<div class="fig-input-joystick-plane-container" tabindex="0">
|
|
7633
|
+
${labelsMarkup}
|
|
7431
7634
|
<div class="fig-input-joystick-plane">
|
|
7432
7635
|
<div class="fig-input-joystick-guides"></div>
|
|
7433
7636
|
<div class="fig-input-joystick-handle"></div>
|
|
7434
7637
|
</div>
|
|
7435
7638
|
</div>
|
|
7436
7639
|
${
|
|
7437
|
-
this
|
|
7438
|
-
? `<
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7640
|
+
this.#fieldsEnabled
|
|
7641
|
+
? `<div class="joystick-values">
|
|
7642
|
+
<fig-input-number
|
|
7643
|
+
name="x"
|
|
7644
|
+
step="1"
|
|
7645
|
+
value="${(this.position.x * 100).toFixed(this.precision)}"
|
|
7646
|
+
min="0"
|
|
7647
|
+
max="100"
|
|
7648
|
+
units="%">
|
|
7649
|
+
<span slot="prepend">X</span>
|
|
7650
|
+
</fig-input-number>
|
|
7651
|
+
<fig-input-number
|
|
7652
|
+
name="y"
|
|
7653
|
+
step="1"
|
|
7654
|
+
min="0"
|
|
7655
|
+
max="100"
|
|
7656
|
+
value="${(this.position.y * 100).toFixed(this.precision)}"
|
|
7657
|
+
units="%">
|
|
7658
|
+
<span slot="prepend">Y</span>
|
|
7659
|
+
</fig-input-number>
|
|
7660
|
+
</div>`
|
|
7456
7661
|
: ""
|
|
7457
7662
|
}
|
|
7458
7663
|
`;
|
|
@@ -7463,45 +7668,57 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7463
7668
|
this.cursor = this.querySelector(".fig-input-joystick-handle");
|
|
7464
7669
|
this.xInput = this.querySelector("fig-input-number[name='x']");
|
|
7465
7670
|
this.yInput = this.querySelector("fig-input-number[name='y']");
|
|
7466
|
-
this.plane.addEventListener("mousedown", this.#
|
|
7467
|
-
this.plane.addEventListener(
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
)
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
this.
|
|
7475
|
-
this.yInput.addEventListener("
|
|
7671
|
+
this.plane.addEventListener("mousedown", this.#boundMouseDown);
|
|
7672
|
+
this.plane.addEventListener("touchstart", this.#boundTouchStart);
|
|
7673
|
+
window.addEventListener("keydown", this.#boundKeyDown);
|
|
7674
|
+
window.addEventListener("keyup", this.#boundKeyUp);
|
|
7675
|
+
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
7676
|
+
this.xInput.addEventListener("input", this.#boundXInput);
|
|
7677
|
+
this.xInput.addEventListener("change", this.#boundXInput);
|
|
7678
|
+
this.xInput.addEventListener("focusout", this.#boundXFocusOut);
|
|
7679
|
+
this.yInput.addEventListener("input", this.#boundYInput);
|
|
7680
|
+
this.yInput.addEventListener("change", this.#boundYInput);
|
|
7681
|
+
this.yInput.addEventListener("focusout", this.#boundYFocusOut);
|
|
7476
7682
|
}
|
|
7477
7683
|
}
|
|
7478
7684
|
|
|
7479
7685
|
#cleanupListeners() {
|
|
7480
7686
|
if (this.plane) {
|
|
7481
|
-
this.plane.removeEventListener("mousedown", this.#
|
|
7482
|
-
this.plane.removeEventListener("touchstart", this.#
|
|
7687
|
+
this.plane.removeEventListener("mousedown", this.#boundMouseDown);
|
|
7688
|
+
this.plane.removeEventListener("touchstart", this.#boundTouchStart);
|
|
7483
7689
|
}
|
|
7484
|
-
window.removeEventListener("keydown", this.#
|
|
7485
|
-
window.removeEventListener("keyup", this.#
|
|
7486
|
-
if (this
|
|
7487
|
-
this.xInput.removeEventListener("input", this.#
|
|
7488
|
-
this.
|
|
7690
|
+
window.removeEventListener("keydown", this.#boundKeyDown);
|
|
7691
|
+
window.removeEventListener("keyup", this.#boundKeyUp);
|
|
7692
|
+
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
7693
|
+
this.xInput.removeEventListener("input", this.#boundXInput);
|
|
7694
|
+
this.xInput.removeEventListener("change", this.#boundXInput);
|
|
7695
|
+
this.xInput.removeEventListener("focusout", this.#boundXFocusOut);
|
|
7696
|
+
this.yInput.removeEventListener("input", this.#boundYInput);
|
|
7697
|
+
this.yInput.removeEventListener("change", this.#boundYInput);
|
|
7698
|
+
this.yInput.removeEventListener("focusout", this.#boundYFocusOut);
|
|
7489
7699
|
}
|
|
7490
7700
|
}
|
|
7491
7701
|
|
|
7492
7702
|
#handleXInput(e) {
|
|
7493
|
-
e.
|
|
7494
|
-
|
|
7703
|
+
const next = Number.parseFloat(e.target.value);
|
|
7704
|
+
if (!Number.isFinite(next)) return;
|
|
7705
|
+
this.position.x = Math.max(0, Math.min(1, next / 100));
|
|
7495
7706
|
this.#syncHandlePosition();
|
|
7707
|
+
this.#syncValueAttribute();
|
|
7496
7708
|
this.#emitInputEvent();
|
|
7497
|
-
this.#emitChangeEvent();
|
|
7498
7709
|
}
|
|
7499
7710
|
|
|
7500
7711
|
#handleYInput(e) {
|
|
7501
|
-
e.
|
|
7502
|
-
|
|
7712
|
+
const next = Number.parseFloat(e.target.value);
|
|
7713
|
+
if (!Number.isFinite(next)) return;
|
|
7714
|
+
this.position.y = Math.max(0, Math.min(1, next / 100));
|
|
7503
7715
|
this.#syncHandlePosition();
|
|
7716
|
+
this.#syncValueAttribute();
|
|
7504
7717
|
this.#emitInputEvent();
|
|
7718
|
+
}
|
|
7719
|
+
|
|
7720
|
+
#handleFieldFocusOut() {
|
|
7721
|
+
this.#syncValueAttribute();
|
|
7505
7722
|
this.#emitChangeEvent();
|
|
7506
7723
|
}
|
|
7507
7724
|
|
|
@@ -7541,11 +7758,12 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7541
7758
|
const displayY = this.#displayY(snapped.y);
|
|
7542
7759
|
this.cursor.style.left = `${snapped.x * 100}%`;
|
|
7543
7760
|
this.cursor.style.top = `${displayY * 100}%`;
|
|
7544
|
-
if (this
|
|
7761
|
+
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
7545
7762
|
this.xInput.setAttribute("value", Math.round(snapped.x * 100));
|
|
7546
7763
|
this.yInput.setAttribute("value", Math.round(snapped.y * 100));
|
|
7547
7764
|
}
|
|
7548
7765
|
|
|
7766
|
+
this.#syncValueAttribute();
|
|
7549
7767
|
this.#emitInputEvent();
|
|
7550
7768
|
}
|
|
7551
7769
|
|
|
@@ -7574,12 +7792,20 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7574
7792
|
this.cursor.style.top = `${displayY * 100}%`;
|
|
7575
7793
|
}
|
|
7576
7794
|
// Also sync text inputs if they exist (convert to percentage 0-100)
|
|
7577
|
-
if (this
|
|
7795
|
+
if (this.#fieldsEnabled && this.xInput && this.yInput) {
|
|
7578
7796
|
this.xInput.setAttribute("value", Math.round(this.position.x * 100));
|
|
7579
7797
|
this.yInput.setAttribute("value", Math.round(this.position.y * 100));
|
|
7580
7798
|
}
|
|
7581
7799
|
}
|
|
7582
7800
|
|
|
7801
|
+
#syncValueAttribute() {
|
|
7802
|
+
const next = this.value;
|
|
7803
|
+
if (this.getAttribute("value") === next) return;
|
|
7804
|
+
this.#isSyncingValueAttr = true;
|
|
7805
|
+
this.setAttribute("value", next);
|
|
7806
|
+
this.#isSyncingValueAttr = false;
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7583
7809
|
#handleMouseDown(e) {
|
|
7584
7810
|
this.isDragging = true;
|
|
7585
7811
|
|
|
@@ -7598,6 +7824,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7598
7824
|
this.plane.style.cursor = "";
|
|
7599
7825
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
7600
7826
|
window.removeEventListener("mouseup", handleMouseUp);
|
|
7827
|
+
this.#syncValueAttribute();
|
|
7601
7828
|
this.#emitChangeEvent();
|
|
7602
7829
|
};
|
|
7603
7830
|
|
|
@@ -7620,6 +7847,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7620
7847
|
this.plane.classList.remove("dragging");
|
|
7621
7848
|
window.removeEventListener("touchmove", handleTouchMove);
|
|
7622
7849
|
window.removeEventListener("touchend", handleTouchEnd);
|
|
7850
|
+
this.#syncValueAttribute();
|
|
7623
7851
|
this.#emitChangeEvent();
|
|
7624
7852
|
};
|
|
7625
7853
|
|
|
@@ -7639,32 +7867,48 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7639
7867
|
container?.focus();
|
|
7640
7868
|
}
|
|
7641
7869
|
static get observedAttributes() {
|
|
7642
|
-
return ["value", "precision", "transform", "text", "coordinates"];
|
|
7643
|
-
}
|
|
7644
|
-
get value() {
|
|
7645
|
-
// Return as percentage values (0-100)
|
|
7646
7870
|
return [
|
|
7647
|
-
|
|
7648
|
-
|
|
7871
|
+
"value",
|
|
7872
|
+
"precision",
|
|
7873
|
+
"transform",
|
|
7874
|
+
"fields",
|
|
7875
|
+
"coordinates",
|
|
7876
|
+
"aspect-ratio",
|
|
7877
|
+
"axis-labels",
|
|
7649
7878
|
];
|
|
7650
7879
|
}
|
|
7880
|
+
get value() {
|
|
7881
|
+
return `${Math.round(this.position.x * 100)}% ${Math.round(this.position.y * 100)}%`;
|
|
7882
|
+
}
|
|
7651
7883
|
set value(value) {
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
.
|
|
7655
|
-
|
|
7656
|
-
.
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7884
|
+
const normalized = value == null ? "" : String(value).trim();
|
|
7885
|
+
if (!normalized) {
|
|
7886
|
+
this.position = { x: 0.5, y: 0.5 };
|
|
7887
|
+
} else {
|
|
7888
|
+
const parts = normalized.split(/[\s,]+/).filter(Boolean);
|
|
7889
|
+
const parseAxis = (token) => {
|
|
7890
|
+
if (!token) return 0.5;
|
|
7891
|
+
const isPercent = token.includes("%");
|
|
7892
|
+
const numeric = Number.parseFloat(token.replace(/%/g, "").trim());
|
|
7893
|
+
if (!Number.isFinite(numeric)) return 0.5;
|
|
7894
|
+
const decimal = isPercent || Math.abs(numeric) > 1 ? numeric / 100 : numeric;
|
|
7895
|
+
return Math.max(0, Math.min(1, decimal));
|
|
7896
|
+
};
|
|
7897
|
+
const x = parseAxis(parts[0]);
|
|
7898
|
+
const y = parseAxis(parts[1] ?? parts[0]);
|
|
7899
|
+
this.position = { x, y };
|
|
7900
|
+
}
|
|
7661
7901
|
if (this.#initialized) {
|
|
7662
7902
|
this.#syncHandlePosition();
|
|
7663
7903
|
}
|
|
7664
7904
|
}
|
|
7665
7905
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
7906
|
+
if (name === "aspect-ratio") {
|
|
7907
|
+
this.#syncAspectRatioVar(newValue);
|
|
7908
|
+
return;
|
|
7909
|
+
}
|
|
7666
7910
|
if (name === "value") {
|
|
7667
|
-
if (this.isDragging) return;
|
|
7911
|
+
if (this.#isSyncingValueAttr || this.isDragging) return;
|
|
7668
7912
|
this.value = newValue;
|
|
7669
7913
|
}
|
|
7670
7914
|
if (name === "precision") {
|
|
@@ -7673,9 +7917,17 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7673
7917
|
if (name === "transform") {
|
|
7674
7918
|
this.transform = Number(newValue);
|
|
7675
7919
|
}
|
|
7676
|
-
if (name === "
|
|
7677
|
-
this
|
|
7920
|
+
if (name === "fields" && newValue !== oldValue) {
|
|
7921
|
+
this.#cleanupListeners();
|
|
7922
|
+
this.#render();
|
|
7923
|
+
this.#setupListeners();
|
|
7924
|
+
this.#syncHandlePosition();
|
|
7925
|
+
}
|
|
7926
|
+
if (name === "axis-labels" && newValue !== oldValue) {
|
|
7927
|
+
this.#cleanupListeners();
|
|
7678
7928
|
this.#render();
|
|
7929
|
+
this.#setupListeners();
|
|
7930
|
+
this.#syncHandlePosition();
|
|
7679
7931
|
}
|
|
7680
7932
|
if (name === "coordinates") {
|
|
7681
7933
|
this.coordinates = newValue || "screen";
|
|
@@ -7684,7 +7936,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
7684
7936
|
}
|
|
7685
7937
|
}
|
|
7686
7938
|
|
|
7687
|
-
customElements.define("fig-
|
|
7939
|
+
customElements.define("fig-joystick", FigInputJoystick);
|
|
7688
7940
|
|
|
7689
7941
|
/**
|
|
7690
7942
|
* A custom angle chooser input element.
|