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