@rogieking/figui3 11.3.3 → 11.4.1
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/.cursor/skills/figui3/components.md +11 -0
- package/.cursor/skills/figui3/react.md +1 -0
- package/.cursor/skills/figui3/reference.md +1 -0
- package/CHANGELOG.md +21 -0
- package/README.md +27 -2
- package/components.css +54 -17
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +2 -2
- package/dist/fig.css +1 -1
- package/dist/fig.js +6 -6
- package/fig-editor.css +1 -5
- package/fig-editor.js +14 -0
- package/fig.js +574 -107
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -11181,9 +11181,9 @@ class FigInputPalette extends HTMLElement {
|
|
|
11181
11181
|
|
|
11182
11182
|
const popup = document.createElement("dialog", { is: "fig-popup" });
|
|
11183
11183
|
popup.setAttribute("is", "fig-popup");
|
|
11184
|
-
popup.setAttribute("
|
|
11185
|
-
popup.setAttribute("position", "
|
|
11186
|
-
popup.setAttribute("offset", "8
|
|
11184
|
+
popup.setAttribute("aria-label", "Edit palette");
|
|
11185
|
+
popup.setAttribute("position", "bottom left");
|
|
11186
|
+
popup.setAttribute("offset", "0 8");
|
|
11187
11187
|
popup.setAttribute("viewport-margin", "8");
|
|
11188
11188
|
popup.setAttribute("popover", "manual");
|
|
11189
11189
|
popup.className = "fig-palette-editor-popup";
|
|
@@ -11224,9 +11224,6 @@ class FigInputPalette extends HTMLElement {
|
|
|
11224
11224
|
popup.addEventListener("toggle", this.#boundPickerClosed);
|
|
11225
11225
|
|
|
11226
11226
|
(figGetOverlayRoot() ?? document.body).append(popup);
|
|
11227
|
-
popup
|
|
11228
|
-
.querySelector(":scope > fig-header")
|
|
11229
|
-
?.setAttribute("borderless", "");
|
|
11230
11227
|
this.#pickerPopup = popup;
|
|
11231
11228
|
this.#pickerEditor = editor;
|
|
11232
11229
|
this.querySelector(".palette-colors-inline")?.setAttribute(
|
|
@@ -15300,6 +15297,351 @@ class FigAudioWaveform extends HTMLElement {
|
|
|
15300
15297
|
}
|
|
15301
15298
|
figDefineElement("fig-audio-waveform", FigAudioWaveform);
|
|
15302
15299
|
|
|
15300
|
+
/**
|
|
15301
|
+
* Audio player composed from a waveform and shared media controls.
|
|
15302
|
+
*
|
|
15303
|
+
* @attr {string} src - Audio source URL
|
|
15304
|
+
* @attr {boolean} autoplay - Audio autoplay
|
|
15305
|
+
* @attr {boolean} controls - Show playback controls (default false)
|
|
15306
|
+
* @attr {boolean} muted - Mute audio playback
|
|
15307
|
+
* @attr {boolean} loop - Loop audio playback
|
|
15308
|
+
*/
|
|
15309
|
+
class FigAudio extends HTMLElement {
|
|
15310
|
+
#src = "";
|
|
15311
|
+
#audioEl = null;
|
|
15312
|
+
#waveformEl = null;
|
|
15313
|
+
#controlsEl = null;
|
|
15314
|
+
#controlsWiredFor = null;
|
|
15315
|
+
#controlsWiredControls = null;
|
|
15316
|
+
#controlsSync = null;
|
|
15317
|
+
#controlsOnPlay = null;
|
|
15318
|
+
#controlsOnPause = null;
|
|
15319
|
+
#controlsOnSeek = null;
|
|
15320
|
+
|
|
15321
|
+
static get observedAttributes() {
|
|
15322
|
+
return ["src", "autoplay", "controls", "muted", "loop"];
|
|
15323
|
+
}
|
|
15324
|
+
|
|
15325
|
+
connectedCallback() {
|
|
15326
|
+
this.#src = this.getAttribute("src") || "";
|
|
15327
|
+
this.#ensureAudioElement();
|
|
15328
|
+
this.#ensureWaveform();
|
|
15329
|
+
this.#syncAudioElement();
|
|
15330
|
+
this.#syncControlsVisibility();
|
|
15331
|
+
this.#syncPlaybackUi();
|
|
15332
|
+
}
|
|
15333
|
+
|
|
15334
|
+
disconnectedCallback() {
|
|
15335
|
+
this.#unwireControls();
|
|
15336
|
+
}
|
|
15337
|
+
|
|
15338
|
+
get src() {
|
|
15339
|
+
return this.#src;
|
|
15340
|
+
}
|
|
15341
|
+
|
|
15342
|
+
set src(value) {
|
|
15343
|
+
if (value === null || value === undefined || value === "") {
|
|
15344
|
+
this.removeAttribute("src");
|
|
15345
|
+
} else {
|
|
15346
|
+
this.setAttribute("src", String(value));
|
|
15347
|
+
}
|
|
15348
|
+
}
|
|
15349
|
+
|
|
15350
|
+
get audioEl() {
|
|
15351
|
+
return this.#audioEl;
|
|
15352
|
+
}
|
|
15353
|
+
|
|
15354
|
+
#isEnabledAttr(name) {
|
|
15355
|
+
return this.hasAttribute(name) && this.getAttribute(name) !== "false";
|
|
15356
|
+
}
|
|
15357
|
+
|
|
15358
|
+
#ensureAudioElement() {
|
|
15359
|
+
const authored = this.querySelector(":scope > audio:not([data-generated])");
|
|
15360
|
+
if (authored) {
|
|
15361
|
+
this.#audioEl = authored;
|
|
15362
|
+
return;
|
|
15363
|
+
}
|
|
15364
|
+
|
|
15365
|
+
const generated = Array.from(
|
|
15366
|
+
this.querySelectorAll(":scope > audio[data-generated]"),
|
|
15367
|
+
);
|
|
15368
|
+
const audio = generated[0] || document.createElement("audio");
|
|
15369
|
+
generated.slice(1).forEach((duplicate) => duplicate.remove());
|
|
15370
|
+
if (!audio.hasAttribute("data-generated")) {
|
|
15371
|
+
audio.setAttribute("data-generated", "");
|
|
15372
|
+
audio.preload = "metadata";
|
|
15373
|
+
this.append(audio);
|
|
15374
|
+
}
|
|
15375
|
+
this.#audioEl = audio;
|
|
15376
|
+
}
|
|
15377
|
+
|
|
15378
|
+
#ensureWaveform() {
|
|
15379
|
+
const authored = this.querySelector(
|
|
15380
|
+
":scope > fig-audio-waveform:not([data-generated])",
|
|
15381
|
+
);
|
|
15382
|
+
if (authored) {
|
|
15383
|
+
this.querySelectorAll(
|
|
15384
|
+
":scope > fig-audio-waveform[data-generated]",
|
|
15385
|
+
).forEach((waveform) => waveform.remove());
|
|
15386
|
+
this.#waveformEl = authored;
|
|
15387
|
+
} else {
|
|
15388
|
+
const generated = Array.from(
|
|
15389
|
+
this.querySelectorAll(":scope > fig-audio-waveform[data-generated]"),
|
|
15390
|
+
);
|
|
15391
|
+
const waveform = generated[0] || document.createElement("fig-audio-waveform");
|
|
15392
|
+
generated.slice(1).forEach((duplicate) => duplicate.remove());
|
|
15393
|
+
if (!waveform.hasAttribute("data-generated")) {
|
|
15394
|
+
waveform.setAttribute("data-generated", "");
|
|
15395
|
+
waveform.setAttribute("full", "");
|
|
15396
|
+
this.prepend(waveform);
|
|
15397
|
+
}
|
|
15398
|
+
this.#waveformEl = waveform;
|
|
15399
|
+
}
|
|
15400
|
+
this.#syncWaveformSource();
|
|
15401
|
+
}
|
|
15402
|
+
|
|
15403
|
+
#syncWaveformSource() {
|
|
15404
|
+
if (!this.#waveformEl) return;
|
|
15405
|
+
if (this.#src) this.#waveformEl.setAttribute("url", this.#src);
|
|
15406
|
+
else this.#waveformEl.removeAttribute("url");
|
|
15407
|
+
}
|
|
15408
|
+
|
|
15409
|
+
#syncAudioElement() {
|
|
15410
|
+
if (!this.#audioEl) return;
|
|
15411
|
+
if (this.#audioEl.getAttribute("src") !== this.#src) {
|
|
15412
|
+
if (this.#src) this.#audioEl.setAttribute("src", this.#src);
|
|
15413
|
+
else {
|
|
15414
|
+
this.#audioEl.removeAttribute("src");
|
|
15415
|
+
this.#audioEl.load?.();
|
|
15416
|
+
}
|
|
15417
|
+
}
|
|
15418
|
+
this.#audioEl.controls = false;
|
|
15419
|
+
this.#audioEl.removeAttribute("controls");
|
|
15420
|
+
this.#audioEl.autoplay = this.#isEnabledAttr("autoplay");
|
|
15421
|
+
this.#audioEl.muted = this.#isEnabledAttr("muted");
|
|
15422
|
+
this.#audioEl.loop = this.#isEnabledAttr("loop");
|
|
15423
|
+
this.#syncWaveformSource();
|
|
15424
|
+
this.#syncControlsDisabled();
|
|
15425
|
+
}
|
|
15426
|
+
|
|
15427
|
+
#hasPlayableAudio() {
|
|
15428
|
+
return Boolean(this.#src);
|
|
15429
|
+
}
|
|
15430
|
+
|
|
15431
|
+
#syncControlsVisibility() {
|
|
15432
|
+
const authored = this.querySelector(
|
|
15433
|
+
":scope > fig-media-controls:not([data-generated])",
|
|
15434
|
+
);
|
|
15435
|
+
if (authored) {
|
|
15436
|
+
if (this.#controlsEl !== authored) {
|
|
15437
|
+
this.#removeControls();
|
|
15438
|
+
this.#controlsEl = authored;
|
|
15439
|
+
}
|
|
15440
|
+
this.#wireControlsToAudio();
|
|
15441
|
+
this.#syncControlsDisabled();
|
|
15442
|
+
return;
|
|
15443
|
+
}
|
|
15444
|
+
|
|
15445
|
+
if (this.#isEnabledAttr("controls")) {
|
|
15446
|
+
this.#ensureControls();
|
|
15447
|
+
} else {
|
|
15448
|
+
this.#removeControls();
|
|
15449
|
+
}
|
|
15450
|
+
}
|
|
15451
|
+
|
|
15452
|
+
#ensureControls() {
|
|
15453
|
+
const generated = Array.from(
|
|
15454
|
+
this.querySelectorAll(":scope > fig-media-controls[data-generated]"),
|
|
15455
|
+
);
|
|
15456
|
+
const controls = generated[0] || document.createElement("fig-media-controls");
|
|
15457
|
+
generated.slice(1).forEach((duplicate) => duplicate.remove());
|
|
15458
|
+
if (!controls.hasAttribute("data-generated")) {
|
|
15459
|
+
controls.setAttribute("data-generated", "");
|
|
15460
|
+
this.append(controls);
|
|
15461
|
+
}
|
|
15462
|
+
this.#controlsEl = controls;
|
|
15463
|
+
this.#wireControlsToAudio();
|
|
15464
|
+
this.#syncControlsDisabled();
|
|
15465
|
+
}
|
|
15466
|
+
|
|
15467
|
+
#syncControlsDisabled() {
|
|
15468
|
+
if (!this.#controlsEl) return;
|
|
15469
|
+
if (this.#hasPlayableAudio()) {
|
|
15470
|
+
if (this.#controlsEl.hasAttribute("data-fig-empty-disabled")) {
|
|
15471
|
+
this.#controlsEl.removeAttribute("disabled");
|
|
15472
|
+
this.#controlsEl.removeAttribute("data-fig-empty-disabled");
|
|
15473
|
+
}
|
|
15474
|
+
return;
|
|
15475
|
+
}
|
|
15476
|
+
this.#controlsEl.setAttribute("disabled", "");
|
|
15477
|
+
this.#controlsEl.setAttribute("data-fig-empty-disabled", "");
|
|
15478
|
+
this.#controlsEl.removeAttribute("playing");
|
|
15479
|
+
this.#controlsEl.setAttribute("time", "0");
|
|
15480
|
+
this.#controlsEl.setAttribute("duration", "0");
|
|
15481
|
+
this.#syncWaveformProgress(0, 0);
|
|
15482
|
+
}
|
|
15483
|
+
|
|
15484
|
+
#syncWaveformProgress(time, duration) {
|
|
15485
|
+
if (!this.#waveformEl) return;
|
|
15486
|
+
const progress =
|
|
15487
|
+
Number.isFinite(time) && Number.isFinite(duration) && duration > 0
|
|
15488
|
+
? Math.max(0, Math.min(time / duration, 1))
|
|
15489
|
+
: 0;
|
|
15490
|
+
this.#waveformEl.setAttribute("progress", String(progress));
|
|
15491
|
+
}
|
|
15492
|
+
|
|
15493
|
+
#syncPlaybackUi() {
|
|
15494
|
+
const audio = this.#audioEl;
|
|
15495
|
+
if (!audio) return;
|
|
15496
|
+
const time = Number.isFinite(audio.currentTime) ? audio.currentTime : 0;
|
|
15497
|
+
const duration = Number.isFinite(audio.duration) ? audio.duration : 0;
|
|
15498
|
+
if (this.#controlsEl) {
|
|
15499
|
+
this.#controlsEl.toggleAttribute("playing", !audio.paused && !audio.ended);
|
|
15500
|
+
this.#controlsEl.setAttribute("time", String(time));
|
|
15501
|
+
this.#controlsEl.setAttribute("duration", String(duration));
|
|
15502
|
+
}
|
|
15503
|
+
this.#syncWaveformProgress(time, duration);
|
|
15504
|
+
}
|
|
15505
|
+
|
|
15506
|
+
#wireControlsToAudio() {
|
|
15507
|
+
if (!this.#controlsEl || !this.#audioEl) return;
|
|
15508
|
+
if (
|
|
15509
|
+
this.#controlsWiredFor === this.#audioEl &&
|
|
15510
|
+
this.#controlsWiredControls === this.#controlsEl
|
|
15511
|
+
) {
|
|
15512
|
+
this.#syncControlsDisabled();
|
|
15513
|
+
this.#syncPlaybackUi();
|
|
15514
|
+
return;
|
|
15515
|
+
}
|
|
15516
|
+
this.#unwireControls();
|
|
15517
|
+
|
|
15518
|
+
const controls = this.#controlsEl;
|
|
15519
|
+
const audio = this.#audioEl;
|
|
15520
|
+
let pendingSeekTime = null;
|
|
15521
|
+
const syncFromAudio = () => {
|
|
15522
|
+
this.#syncControlsDisabled();
|
|
15523
|
+
if (!this.#hasPlayableAudio()) return;
|
|
15524
|
+
const duration = Number.isFinite(audio.duration) ? audio.duration : 0;
|
|
15525
|
+
const time = Number.isFinite(audio.currentTime) ? audio.currentTime : 0;
|
|
15526
|
+
controls.toggleAttribute("playing", !audio.paused && !audio.ended);
|
|
15527
|
+
controls.setAttribute("duration", String(duration));
|
|
15528
|
+
if (pendingSeekTime !== null) {
|
|
15529
|
+
if (Math.abs(time - pendingSeekTime) < 0.25) pendingSeekTime = null;
|
|
15530
|
+
else {
|
|
15531
|
+
this.#syncWaveformProgress(time, duration);
|
|
15532
|
+
return;
|
|
15533
|
+
}
|
|
15534
|
+
}
|
|
15535
|
+
controls.setAttribute("time", String(time));
|
|
15536
|
+
this.#syncWaveformProgress(time, duration);
|
|
15537
|
+
};
|
|
15538
|
+
const onPlay = () => this.play();
|
|
15539
|
+
const onPause = () => this.pause();
|
|
15540
|
+
const onSeek = (event) => {
|
|
15541
|
+
if (!this.#hasPlayableAudio()) return;
|
|
15542
|
+
const next = Number(event?.detail?.time);
|
|
15543
|
+
if (!Number.isFinite(next)) return;
|
|
15544
|
+
pendingSeekTime = next;
|
|
15545
|
+
try {
|
|
15546
|
+
audio.currentTime = next;
|
|
15547
|
+
} catch {}
|
|
15548
|
+
this.#syncWaveformProgress(next, audio.duration);
|
|
15549
|
+
};
|
|
15550
|
+
|
|
15551
|
+
this.#controlsWiredFor = audio;
|
|
15552
|
+
this.#controlsWiredControls = controls;
|
|
15553
|
+
this.#controlsSync = syncFromAudio;
|
|
15554
|
+
this.#controlsOnPlay = onPlay;
|
|
15555
|
+
this.#controlsOnPause = onPause;
|
|
15556
|
+
this.#controlsOnSeek = onSeek;
|
|
15557
|
+
|
|
15558
|
+
[
|
|
15559
|
+
"play",
|
|
15560
|
+
"pause",
|
|
15561
|
+
"ended",
|
|
15562
|
+
"timeupdate",
|
|
15563
|
+
"loadedmetadata",
|
|
15564
|
+
"durationchange",
|
|
15565
|
+
"seeked",
|
|
15566
|
+
"emptied",
|
|
15567
|
+
].forEach((type) => audio.addEventListener(type, syncFromAudio));
|
|
15568
|
+
controls.addEventListener("play", onPlay);
|
|
15569
|
+
controls.addEventListener("pause", onPause);
|
|
15570
|
+
controls.addEventListener("seek", onSeek);
|
|
15571
|
+
syncFromAudio();
|
|
15572
|
+
}
|
|
15573
|
+
|
|
15574
|
+
#unwireControls() {
|
|
15575
|
+
const audio = this.#controlsWiredFor;
|
|
15576
|
+
const controls = this.#controlsWiredControls;
|
|
15577
|
+
if (audio && this.#controlsSync) {
|
|
15578
|
+
[
|
|
15579
|
+
"play",
|
|
15580
|
+
"pause",
|
|
15581
|
+
"ended",
|
|
15582
|
+
"timeupdate",
|
|
15583
|
+
"loadedmetadata",
|
|
15584
|
+
"durationchange",
|
|
15585
|
+
"seeked",
|
|
15586
|
+
"emptied",
|
|
15587
|
+
].forEach((type) => audio.removeEventListener(type, this.#controlsSync));
|
|
15588
|
+
}
|
|
15589
|
+
if (controls) {
|
|
15590
|
+
if (this.#controlsOnPlay) {
|
|
15591
|
+
controls.removeEventListener("play", this.#controlsOnPlay);
|
|
15592
|
+
}
|
|
15593
|
+
if (this.#controlsOnPause) {
|
|
15594
|
+
controls.removeEventListener("pause", this.#controlsOnPause);
|
|
15595
|
+
}
|
|
15596
|
+
if (this.#controlsOnSeek) {
|
|
15597
|
+
controls.removeEventListener("seek", this.#controlsOnSeek);
|
|
15598
|
+
}
|
|
15599
|
+
}
|
|
15600
|
+
this.#controlsWiredFor = null;
|
|
15601
|
+
this.#controlsWiredControls = null;
|
|
15602
|
+
this.#controlsSync = null;
|
|
15603
|
+
this.#controlsOnPlay = null;
|
|
15604
|
+
this.#controlsOnPause = null;
|
|
15605
|
+
this.#controlsOnSeek = null;
|
|
15606
|
+
}
|
|
15607
|
+
|
|
15608
|
+
#removeControls() {
|
|
15609
|
+
this.#unwireControls();
|
|
15610
|
+
this.querySelectorAll(
|
|
15611
|
+
":scope > fig-media-controls[data-generated]",
|
|
15612
|
+
).forEach((controls) => controls.remove());
|
|
15613
|
+
this.#controlsEl = null;
|
|
15614
|
+
}
|
|
15615
|
+
|
|
15616
|
+
play() {
|
|
15617
|
+
if (!this.#audioEl || !this.#hasPlayableAudio()) return;
|
|
15618
|
+
const promise = this.#audioEl.play?.();
|
|
15619
|
+
if (promise && typeof promise.catch === "function") promise.catch(() => {});
|
|
15620
|
+
}
|
|
15621
|
+
|
|
15622
|
+
pause() {
|
|
15623
|
+
this.#audioEl?.pause?.();
|
|
15624
|
+
}
|
|
15625
|
+
|
|
15626
|
+
toggle() {
|
|
15627
|
+
if (!this.#audioEl || !this.#hasPlayableAudio()) return;
|
|
15628
|
+
if (this.#audioEl.paused || this.#audioEl.ended) this.play();
|
|
15629
|
+
else this.pause();
|
|
15630
|
+
}
|
|
15631
|
+
|
|
15632
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
15633
|
+
if (oldValue === newValue) return;
|
|
15634
|
+
if (name === "src") this.#src = newValue || "";
|
|
15635
|
+
if (!this.isConnected) return;
|
|
15636
|
+
if (name === "controls") this.#syncControlsVisibility();
|
|
15637
|
+
if (["src", "autoplay", "muted", "loop"].includes(name)) {
|
|
15638
|
+
this.#syncAudioElement();
|
|
15639
|
+
this.#syncPlaybackUi();
|
|
15640
|
+
}
|
|
15641
|
+
}
|
|
15642
|
+
}
|
|
15643
|
+
figDefineElement("fig-audio", FigAudio);
|
|
15644
|
+
|
|
15303
15645
|
/* File Upload Input */
|
|
15304
15646
|
const FIG_INPUT_FILE_IMAGE_EXTENSIONS = [
|
|
15305
15647
|
".png",
|
|
@@ -15318,11 +15660,23 @@ const FIG_INPUT_FILE_AUDIO_EXTENSIONS = [
|
|
|
15318
15660
|
".ogg",
|
|
15319
15661
|
".flac",
|
|
15320
15662
|
];
|
|
15663
|
+
const FIG_INPUT_FILE_VIDEO_EXTENSIONS = [
|
|
15664
|
+
".mp4",
|
|
15665
|
+
".webm",
|
|
15666
|
+
".mov",
|
|
15667
|
+
".m4v",
|
|
15668
|
+
".avi",
|
|
15669
|
+
".mkv",
|
|
15670
|
+
".ogv",
|
|
15671
|
+
".mpeg",
|
|
15672
|
+
".mpg",
|
|
15673
|
+
".3gp",
|
|
15674
|
+
];
|
|
15321
15675
|
|
|
15322
15676
|
/**
|
|
15323
15677
|
* File upload input with file-aware icons, image previews, and audio playback waveforms.
|
|
15324
15678
|
* @attr {string} accepts - Native file chooser filter.
|
|
15325
|
-
* @attr {boolean} preview - Preview
|
|
15679
|
+
* @attr {boolean} preview - Preview supported files or show their media-specific icon.
|
|
15326
15680
|
* @attr {string} label - Upload button label. Defaults by known file type.
|
|
15327
15681
|
*/
|
|
15328
15682
|
class FigInputFile extends HTMLElement {
|
|
@@ -15334,13 +15688,12 @@ class FigInputFile extends HTMLElement {
|
|
|
15334
15688
|
#tooltipEl = null;
|
|
15335
15689
|
#uploadBtn = null;
|
|
15336
15690
|
#files = null;
|
|
15337
|
-
#
|
|
15691
|
+
#imageBlobUrl = null;
|
|
15338
15692
|
#audioPreview = null;
|
|
15339
15693
|
#audioIcon = null;
|
|
15340
15694
|
#audioWaveform = null;
|
|
15341
15695
|
#audioPlaying = false;
|
|
15342
15696
|
#audioBlobUrl = null;
|
|
15343
|
-
#syncingPreview = false;
|
|
15344
15697
|
#suppressAttributeRender = false;
|
|
15345
15698
|
|
|
15346
15699
|
get files() {
|
|
@@ -15380,6 +15733,10 @@ class FigInputFile extends HTMLElement {
|
|
|
15380
15733
|
);
|
|
15381
15734
|
}
|
|
15382
15735
|
|
|
15736
|
+
get #largePreviewEnabled() {
|
|
15737
|
+
return this.getAttribute("preview")?.toLowerCase() === "large";
|
|
15738
|
+
}
|
|
15739
|
+
|
|
15383
15740
|
#hasKnownExtension(filename, extensions) {
|
|
15384
15741
|
const lowerName = filename.toLowerCase();
|
|
15385
15742
|
return extensions.some((extension) => lowerName.endsWith(extension));
|
|
@@ -15467,9 +15824,10 @@ class FigInputFile extends HTMLElement {
|
|
|
15467
15824
|
|
|
15468
15825
|
#hasPreviewableImage() {
|
|
15469
15826
|
if (this.#files?.length) {
|
|
15470
|
-
return this.#
|
|
15471
|
-
this.#files[0]
|
|
15827
|
+
return this.#isKnownFile(
|
|
15828
|
+
this.#files[0],
|
|
15472
15829
|
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15830
|
+
"image/",
|
|
15473
15831
|
);
|
|
15474
15832
|
}
|
|
15475
15833
|
return (
|
|
@@ -15498,9 +15856,90 @@ class FigInputFile extends HTMLElement {
|
|
|
15498
15856
|
);
|
|
15499
15857
|
}
|
|
15500
15858
|
|
|
15859
|
+
#largePreviewKind() {
|
|
15860
|
+
const file = this.#files?.[0];
|
|
15861
|
+
if (file) {
|
|
15862
|
+
if (
|
|
15863
|
+
this.#isKnownFile(
|
|
15864
|
+
file,
|
|
15865
|
+
FIG_INPUT_FILE_VIDEO_EXTENSIONS,
|
|
15866
|
+
"video/",
|
|
15867
|
+
)
|
|
15868
|
+
) {
|
|
15869
|
+
return "video";
|
|
15870
|
+
}
|
|
15871
|
+
if (
|
|
15872
|
+
this.#isKnownFile(
|
|
15873
|
+
file,
|
|
15874
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15875
|
+
"image/",
|
|
15876
|
+
)
|
|
15877
|
+
) {
|
|
15878
|
+
return "image";
|
|
15879
|
+
}
|
|
15880
|
+
return null;
|
|
15881
|
+
}
|
|
15882
|
+
|
|
15883
|
+
if (
|
|
15884
|
+
this.#hasKnownExtension(
|
|
15885
|
+
this.#urlFilename,
|
|
15886
|
+
FIG_INPUT_FILE_VIDEO_EXTENSIONS,
|
|
15887
|
+
)
|
|
15888
|
+
) {
|
|
15889
|
+
return "video";
|
|
15890
|
+
}
|
|
15891
|
+
if (
|
|
15892
|
+
this.#hasKnownExtension(
|
|
15893
|
+
this.#urlFilename,
|
|
15894
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15895
|
+
)
|
|
15896
|
+
) {
|
|
15897
|
+
return "image";
|
|
15898
|
+
}
|
|
15899
|
+
|
|
15900
|
+
const acceptedKinds = this.#effectiveAccepts
|
|
15901
|
+
.split(",")
|
|
15902
|
+
.map((value) => value.trim().toLowerCase())
|
|
15903
|
+
.filter(Boolean)
|
|
15904
|
+
.map((value) => {
|
|
15905
|
+
if (
|
|
15906
|
+
value.startsWith("video/") ||
|
|
15907
|
+
FIG_INPUT_FILE_VIDEO_EXTENSIONS.includes(value)
|
|
15908
|
+
) {
|
|
15909
|
+
return "video";
|
|
15910
|
+
}
|
|
15911
|
+
if (
|
|
15912
|
+
value.startsWith("image/") ||
|
|
15913
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS.includes(value)
|
|
15914
|
+
) {
|
|
15915
|
+
return "image";
|
|
15916
|
+
}
|
|
15917
|
+
return null;
|
|
15918
|
+
});
|
|
15919
|
+
if (
|
|
15920
|
+
acceptedKinds.length > 0 &&
|
|
15921
|
+
acceptedKinds.every((kind) => kind === acceptedKinds[0])
|
|
15922
|
+
) {
|
|
15923
|
+
return acceptedKinds[0];
|
|
15924
|
+
}
|
|
15925
|
+
return null;
|
|
15926
|
+
}
|
|
15927
|
+
|
|
15501
15928
|
#selectedFileIcon() {
|
|
15502
15929
|
if (this.#files && this.#files.length > 0) {
|
|
15503
15930
|
const files = Array.from(this.#files);
|
|
15931
|
+
if (
|
|
15932
|
+
this.#previewEnabled &&
|
|
15933
|
+
files.every((file) =>
|
|
15934
|
+
this.#isKnownFile(
|
|
15935
|
+
file,
|
|
15936
|
+
FIG_INPUT_FILE_VIDEO_EXTENSIONS,
|
|
15937
|
+
"video/",
|
|
15938
|
+
),
|
|
15939
|
+
)
|
|
15940
|
+
) {
|
|
15941
|
+
return "video";
|
|
15942
|
+
}
|
|
15504
15943
|
if (
|
|
15505
15944
|
files.every((file) =>
|
|
15506
15945
|
this.#isKnownFile(
|
|
@@ -15524,6 +15963,15 @@ class FigInputFile extends HTMLElement {
|
|
|
15524
15963
|
return "image";
|
|
15525
15964
|
}
|
|
15526
15965
|
}
|
|
15966
|
+
if (
|
|
15967
|
+
this.#previewEnabled &&
|
|
15968
|
+
this.#hasKnownExtension(
|
|
15969
|
+
this.#urlFilename,
|
|
15970
|
+
FIG_INPUT_FILE_VIDEO_EXTENSIONS,
|
|
15971
|
+
)
|
|
15972
|
+
) {
|
|
15973
|
+
return "video";
|
|
15974
|
+
}
|
|
15527
15975
|
if (
|
|
15528
15976
|
this.#hasKnownExtension(
|
|
15529
15977
|
this.#urlFilename,
|
|
@@ -15553,7 +16001,7 @@ class FigInputFile extends HTMLElement {
|
|
|
15553
16001
|
disconnectedCallback() {
|
|
15554
16002
|
this.#fileInput?.removeEventListener("change", this.#onFileChange);
|
|
15555
16003
|
this.#clearBtn?.removeEventListener("click", this.#onClear);
|
|
15556
|
-
this.#
|
|
16004
|
+
this.#disconnectImagePreview();
|
|
15557
16005
|
this.#disconnectAudioPreview();
|
|
15558
16006
|
this.removeEventListener("dragover", this.#onDragOver);
|
|
15559
16007
|
this.removeEventListener("dragleave", this.#onDragLeave);
|
|
@@ -15581,10 +16029,17 @@ class FigInputFile extends HTMLElement {
|
|
|
15581
16029
|
this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
|
|
15582
16030
|
}
|
|
15583
16031
|
|
|
16032
|
+
#removeExternalFileAttributes() {
|
|
16033
|
+
this.#suppressAttributeRender = true;
|
|
16034
|
+
this.removeAttribute("url");
|
|
16035
|
+
this.removeAttribute("filename");
|
|
16036
|
+
this.#suppressAttributeRender = false;
|
|
16037
|
+
}
|
|
16038
|
+
|
|
15584
16039
|
#onFileChange = () => {
|
|
15585
16040
|
if (this.#fileInput.files.length > 0) {
|
|
15586
16041
|
this.#files = this.#fileInput.files;
|
|
15587
|
-
this
|
|
16042
|
+
this.#removeExternalFileAttributes();
|
|
15588
16043
|
this.#render();
|
|
15589
16044
|
this.#emitEvents();
|
|
15590
16045
|
}
|
|
@@ -15596,7 +16051,6 @@ class FigInputFile extends HTMLElement {
|
|
|
15596
16051
|
};
|
|
15597
16052
|
|
|
15598
16053
|
#onDragOver = (e) => {
|
|
15599
|
-
if (this.#previewImage) return;
|
|
15600
16054
|
if (
|
|
15601
16055
|
this.hasAttribute("disabled") &&
|
|
15602
16056
|
this.getAttribute("disabled") !== "false"
|
|
@@ -15613,7 +16067,6 @@ class FigInputFile extends HTMLElement {
|
|
|
15613
16067
|
};
|
|
15614
16068
|
|
|
15615
16069
|
#onDragLeave = () => {
|
|
15616
|
-
if (this.#previewImage) return;
|
|
15617
16070
|
this.removeAttribute("dragover");
|
|
15618
16071
|
if (this.#uploadBtn && this.#uploadBtn.dataset.prevText !== undefined) {
|
|
15619
16072
|
this.#uploadBtn.textContent = this.#uploadBtn.dataset.prevText;
|
|
@@ -15622,7 +16075,6 @@ class FigInputFile extends HTMLElement {
|
|
|
15622
16075
|
};
|
|
15623
16076
|
|
|
15624
16077
|
#onDrop = (e) => {
|
|
15625
|
-
if (this.#previewImage) return;
|
|
15626
16078
|
e.preventDefault();
|
|
15627
16079
|
this.removeAttribute("dragover");
|
|
15628
16080
|
if (this.#uploadBtn && this.#uploadBtn.dataset.prevText !== undefined) {
|
|
@@ -15664,59 +16116,15 @@ class FigInputFile extends HTMLElement {
|
|
|
15664
16116
|
if (this.#fileInput) {
|
|
15665
16117
|
this.#fileInput.files = dt.files;
|
|
15666
16118
|
}
|
|
15667
|
-
this.removeAttribute("url");
|
|
15668
|
-
this.#render();
|
|
15669
|
-
this.#emitEvents();
|
|
15670
|
-
};
|
|
15671
|
-
|
|
15672
|
-
#stopPreviewInputEvent = (e) => {
|
|
15673
|
-
e.stopPropagation();
|
|
15674
|
-
};
|
|
15675
|
-
|
|
15676
|
-
#removeExternalFileAttributes() {
|
|
15677
|
-
this.#suppressAttributeRender = true;
|
|
15678
|
-
this.removeAttribute("url");
|
|
15679
|
-
this.removeAttribute("filename");
|
|
15680
|
-
this.#suppressAttributeRender = false;
|
|
15681
|
-
}
|
|
15682
|
-
|
|
15683
|
-
#onPreviewLoaded = (e) => {
|
|
15684
|
-
if (e.currentTarget !== this.#previewImage) return;
|
|
15685
|
-
e.stopPropagation();
|
|
15686
|
-
const file = e.detail?.file;
|
|
15687
|
-
if (!file || this.#files?.[0] === file) return;
|
|
15688
|
-
|
|
15689
|
-
const dt = new DataTransfer();
|
|
15690
|
-
const selectedFiles =
|
|
15691
|
-
this.#previewImage.querySelector("fig-input-file[data-generated]")?.files;
|
|
15692
|
-
if (selectedFiles?.length) {
|
|
15693
|
-
Array.from(selectedFiles).forEach((selectedFile) =>
|
|
15694
|
-
dt.items.add(selectedFile),
|
|
15695
|
-
);
|
|
15696
|
-
} else {
|
|
15697
|
-
dt.items.add(file);
|
|
15698
|
-
}
|
|
15699
|
-
this.#files = dt.files;
|
|
15700
|
-
this.#removeExternalFileAttributes();
|
|
15701
|
-
if (!this.#syncingPreview) this.#emitEvents();
|
|
15702
|
-
};
|
|
15703
|
-
|
|
15704
|
-
#onPreviewChange = (e) => {
|
|
15705
|
-
if (e.currentTarget !== this.#previewImage) return;
|
|
15706
|
-
e.stopPropagation();
|
|
15707
|
-
if (this.#previewImage.file || (!this.#files && !this.#urlFilename)) return;
|
|
15708
|
-
|
|
15709
|
-
this.#files = null;
|
|
15710
16119
|
this.#removeExternalFileAttributes();
|
|
15711
16120
|
this.#render();
|
|
15712
|
-
|
|
16121
|
+
this.#emitEvents();
|
|
15713
16122
|
};
|
|
15714
16123
|
|
|
15715
|
-
#
|
|
15716
|
-
if (!this.#
|
|
15717
|
-
|
|
15718
|
-
this.#
|
|
15719
|
-
this.#previewImage = null;
|
|
16124
|
+
#disconnectImagePreview() {
|
|
16125
|
+
if (!this.#imageBlobUrl) return;
|
|
16126
|
+
URL.revokeObjectURL(this.#imageBlobUrl);
|
|
16127
|
+
this.#imageBlobUrl = null;
|
|
15720
16128
|
}
|
|
15721
16129
|
|
|
15722
16130
|
#disconnectAudioPreview() {
|
|
@@ -15801,38 +16209,69 @@ class FigInputFile extends HTMLElement {
|
|
|
15801
16209
|
this.append(audio);
|
|
15802
16210
|
}
|
|
15803
16211
|
|
|
15804
|
-
#
|
|
15805
|
-
const
|
|
15806
|
-
|
|
15807
|
-
|
|
15808
|
-
|
|
15809
|
-
|
|
15810
|
-
|
|
15811
|
-
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
this.#previewImage = image;
|
|
15815
|
-
image.addEventListener("loaded", this.#onPreviewLoaded);
|
|
15816
|
-
image.addEventListener("change", this.#onPreviewChange);
|
|
15817
|
-
this.appendChild(image);
|
|
16212
|
+
#createImagePreviewSwatch() {
|
|
16213
|
+
const swatch = document.createElement("fig-swatch");
|
|
16214
|
+
const file = this.#files?.[0];
|
|
16215
|
+
const url = file
|
|
16216
|
+
? (this.#imageBlobUrl = URL.createObjectURL(file))
|
|
16217
|
+
: this.getAttribute("url") || "";
|
|
16218
|
+
swatch.setAttribute("background", `url(${JSON.stringify(url)})`);
|
|
16219
|
+
swatch.setAttribute("slot", "prepend");
|
|
16220
|
+
return swatch;
|
|
16221
|
+
}
|
|
15818
16222
|
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
16223
|
+
#createMultipleFileIcon(count) {
|
|
16224
|
+
const icon = document.createElement("fig-icon");
|
|
16225
|
+
icon.setAttribute("slot", "prepend");
|
|
16226
|
+
const label = document.createElement("label");
|
|
16227
|
+
label.textContent = String(count);
|
|
16228
|
+
icon.append(label);
|
|
16229
|
+
return icon;
|
|
16230
|
+
}
|
|
15826
16231
|
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
|
|
15834
|
-
|
|
16232
|
+
#onLargePreviewChange = (event) => {
|
|
16233
|
+
if (event.target !== event.currentTarget) {
|
|
16234
|
+
event.stopPropagation();
|
|
16235
|
+
return;
|
|
16236
|
+
}
|
|
16237
|
+
event.stopPropagation();
|
|
16238
|
+
const media = event.currentTarget;
|
|
16239
|
+
const file = media.file;
|
|
16240
|
+
if (file) {
|
|
16241
|
+
const dataTransfer = new DataTransfer();
|
|
16242
|
+
dataTransfer.items.add(file);
|
|
16243
|
+
this.#files = dataTransfer.files;
|
|
16244
|
+
this.#removeExternalFileAttributes();
|
|
16245
|
+
this.#render();
|
|
16246
|
+
this.#emitEvents();
|
|
16247
|
+
return;
|
|
15835
16248
|
}
|
|
16249
|
+
|
|
16250
|
+
this.#files = null;
|
|
16251
|
+
this.#removeExternalFileAttributes();
|
|
16252
|
+
this.#render();
|
|
16253
|
+
this.#emitEvents({ cleared: true });
|
|
16254
|
+
};
|
|
16255
|
+
|
|
16256
|
+
#renderLargePreview(kind, disabled) {
|
|
16257
|
+
const media = document.createElement(
|
|
16258
|
+
kind === "video" ? "fig-video" : "fig-image",
|
|
16259
|
+
);
|
|
16260
|
+
media.setAttribute("upload", "");
|
|
16261
|
+
media.setAttribute("full", "");
|
|
16262
|
+
media.setAttribute("checkerboard", "");
|
|
16263
|
+
const file = this.#files?.[0];
|
|
16264
|
+
const url = file
|
|
16265
|
+
? (this.#imageBlobUrl = URL.createObjectURL(file))
|
|
16266
|
+
: this.getAttribute("url") || "";
|
|
16267
|
+
if (url) media.setAttribute("src", url);
|
|
16268
|
+
media.addEventListener("change", this.#onLargePreviewChange);
|
|
16269
|
+
this.append(media);
|
|
16270
|
+
const nestedInput = media.querySelector(
|
|
16271
|
+
":scope > fig-preview > fig-input-file[data-generated]",
|
|
16272
|
+
);
|
|
16273
|
+
if (disabled) nestedInput?.setAttribute("disabled", "");
|
|
16274
|
+
this.setAttribute("data-previewing", "large");
|
|
15836
16275
|
}
|
|
15837
16276
|
|
|
15838
16277
|
#render() {
|
|
@@ -15849,28 +16288,46 @@ class FigInputFile extends HTMLElement {
|
|
|
15849
16288
|
(this.#files && this.#files.length > 0) ||
|
|
15850
16289
|
!!this.getAttribute("url") ||
|
|
15851
16290
|
!!this.getAttribute("filename");
|
|
16291
|
+
const showsMultiplePreview =
|
|
16292
|
+
this.#previewEnabled &&
|
|
16293
|
+
multiple &&
|
|
16294
|
+
(this.#files?.length ?? 0) > 1;
|
|
15852
16295
|
const showsImagePreview =
|
|
15853
|
-
|
|
16296
|
+
!showsMultiplePreview &&
|
|
16297
|
+
this.#previewEnabled &&
|
|
16298
|
+
this.#hasPreviewableImage();
|
|
15854
16299
|
const showsAudioPreview =
|
|
15855
|
-
|
|
16300
|
+
!showsMultiplePreview &&
|
|
16301
|
+
this.#previewEnabled &&
|
|
16302
|
+
this.#hasPreviewableAudio();
|
|
15856
16303
|
|
|
15857
|
-
this.#
|
|
16304
|
+
this.#disconnectImagePreview();
|
|
15858
16305
|
this.#disconnectAudioPreview();
|
|
15859
16306
|
this.replaceChildren();
|
|
15860
16307
|
this.#fileInput = null;
|
|
15861
16308
|
this.#uploadBtn = null;
|
|
15862
16309
|
this.#clearBtn = null;
|
|
15863
16310
|
this.#tooltipEl = null;
|
|
15864
|
-
this.
|
|
16311
|
+
this.removeAttribute("data-previewing");
|
|
15865
16312
|
|
|
15866
|
-
|
|
15867
|
-
this.#
|
|
16313
|
+
const largePreviewKind =
|
|
16314
|
+
this.#largePreviewEnabled && !showsMultiplePreview
|
|
16315
|
+
? this.#largePreviewKind()
|
|
16316
|
+
: null;
|
|
16317
|
+
if (largePreviewKind) {
|
|
16318
|
+
this.#renderLargePreview(largePreviewKind, disabled);
|
|
15868
16319
|
return;
|
|
15869
16320
|
}
|
|
15870
16321
|
|
|
15871
16322
|
if (hasFile) {
|
|
15872
16323
|
const filename = this.value;
|
|
15873
|
-
const
|
|
16324
|
+
const selectedFileNames =
|
|
16325
|
+
multiple && this.#files?.length
|
|
16326
|
+
? Array.from(this.#files, (file) => file.name).join(", ")
|
|
16327
|
+
: "";
|
|
16328
|
+
const tooltipText = selectedFileNames
|
|
16329
|
+
? selectedFileNames
|
|
16330
|
+
: showsAudioPreview
|
|
15874
16331
|
? filename
|
|
15875
16332
|
: accepts
|
|
15876
16333
|
? `Accepts ${accepts
|
|
@@ -15884,9 +16341,13 @@ class FigInputFile extends HTMLElement {
|
|
|
15884
16341
|
if (variant === "property") this.#uploadBtn.setAttribute("full", "");
|
|
15885
16342
|
this.#uploadBtn.className = "fig-input-file-filename";
|
|
15886
16343
|
if (disabled) this.#uploadBtn.setAttribute("disabled", "");
|
|
15887
|
-
const fileIcon =
|
|
15888
|
-
|
|
15889
|
-
|
|
16344
|
+
const fileIcon = showsMultiplePreview
|
|
16345
|
+
? this.#createMultipleFileIcon(this.#files.length)
|
|
16346
|
+
: showsImagePreview
|
|
16347
|
+
? this.#createImagePreviewSwatch()
|
|
16348
|
+
: createFigIcon(
|
|
16349
|
+
showsAudioPreview ? "play" : this.#selectedFileIcon(),
|
|
16350
|
+
);
|
|
15890
16351
|
if (showsAudioPreview) {
|
|
15891
16352
|
this.#audioIcon = fileIcon;
|
|
15892
16353
|
fileIcon.setAttribute("slot", "prepend");
|
|
@@ -15895,9 +16356,14 @@ class FigInputFile extends HTMLElement {
|
|
|
15895
16356
|
const truncEl = document.createElement("fig-truncate");
|
|
15896
16357
|
truncEl.setAttribute("position", "middle");
|
|
15897
16358
|
truncEl.setAttribute("tooltip", "");
|
|
15898
|
-
const
|
|
15899
|
-
|
|
15900
|
-
|
|
16359
|
+
const buttonLabel = showsMultiplePreview
|
|
16360
|
+
? "Multiple files"
|
|
16361
|
+
: filename;
|
|
16362
|
+
const dotIdx = buttonLabel.lastIndexOf(".");
|
|
16363
|
+
if (!showsMultiplePreview && dotIdx > 0) {
|
|
16364
|
+
truncEl.setAttribute("tail", buttonLabel.slice(dotIdx));
|
|
16365
|
+
}
|
|
16366
|
+
truncEl.textContent = buttonLabel;
|
|
15901
16367
|
fileIcon.setAttribute("slot", "prepend");
|
|
15902
16368
|
this.#uploadBtn.append(fileIcon, truncEl);
|
|
15903
16369
|
}
|
|
@@ -20310,6 +20776,7 @@ const FIG_ICON_TOKENS = {
|
|
|
20310
20776
|
download: "--icon-24-download",
|
|
20311
20777
|
upload: "--icon-24-upload",
|
|
20312
20778
|
image: "--icon-24-image",
|
|
20779
|
+
video: { medium: "--icon-24-video", small: "--icon-16-video" },
|
|
20313
20780
|
sound: { medium: "--icon-24-sound", small: "--icon-16-sound" },
|
|
20314
20781
|
"sound-off": {
|
|
20315
20782
|
medium: "--icon-24-sound-off",
|