@rogieking/figui3 4.9.1 → 4.10.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/components.css +81 -7
- package/dist/components.css +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +34 -34
- package/fig.js +358 -1
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -8388,6 +8388,13 @@ class FigMedia extends HTMLElement {
|
|
|
8388
8388
|
#boundHandleMediaPlay = this.#handleMediaPlay.bind(this);
|
|
8389
8389
|
#boundHandleMediaPause = this.#handleMediaPause.bind(this);
|
|
8390
8390
|
#boundHandleMediaEnded = this.#handleMediaEnded.bind(this);
|
|
8391
|
+
#controlsEl = null;
|
|
8392
|
+
#controlsWiredFor = null;
|
|
8393
|
+
#controlsWiredControls = null;
|
|
8394
|
+
#controlsSync = null;
|
|
8395
|
+
#controlsOnPlay = null;
|
|
8396
|
+
#controlsOnPause = null;
|
|
8397
|
+
#controlsOnSeek = null;
|
|
8391
8398
|
|
|
8392
8399
|
static get observedAttributes() {
|
|
8393
8400
|
return [
|
|
@@ -8486,6 +8493,7 @@ class FigMedia extends HTMLElement {
|
|
|
8486
8493
|
disconnectedCallback() {
|
|
8487
8494
|
this.#fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
8488
8495
|
this.#removeMediaElementListeners();
|
|
8496
|
+
this.#removeControls();
|
|
8489
8497
|
if (this.#blobUrl) {
|
|
8490
8498
|
URL.revokeObjectURL(this.#blobUrl);
|
|
8491
8499
|
this.#blobUrl = null;
|
|
@@ -8586,11 +8594,163 @@ class FigMedia extends HTMLElement {
|
|
|
8586
8594
|
} else {
|
|
8587
8595
|
this.#mediaEl.removeAttribute("poster");
|
|
8588
8596
|
}
|
|
8589
|
-
this.#mediaEl.controls =
|
|
8597
|
+
this.#mediaEl.controls = false;
|
|
8598
|
+
this.#mediaEl.removeAttribute("controls");
|
|
8590
8599
|
this.#mediaEl.autoplay = this.#isEnabledAttr("autoplay", false);
|
|
8591
8600
|
this.#mediaEl.loop = this.#isEnabledAttr("loop", false);
|
|
8592
8601
|
this.#mediaEl.muted = this.#isEnabledAttr("muted", false);
|
|
8593
8602
|
this.#mediaEl.playsInline = true;
|
|
8603
|
+
this.#syncControlsVisibility();
|
|
8604
|
+
}
|
|
8605
|
+
|
|
8606
|
+
get mediaEl() {
|
|
8607
|
+
return this.#mediaEl;
|
|
8608
|
+
}
|
|
8609
|
+
|
|
8610
|
+
#syncControlsVisibility() {
|
|
8611
|
+
if (this.mediaKind !== "video") {
|
|
8612
|
+
this.#removeControls();
|
|
8613
|
+
return;
|
|
8614
|
+
}
|
|
8615
|
+
const userControls = this.querySelector(
|
|
8616
|
+
":scope > fig-media-controls:not([data-generated])",
|
|
8617
|
+
);
|
|
8618
|
+
if (userControls) {
|
|
8619
|
+
if (this.#controlsEl !== userControls) {
|
|
8620
|
+
this.#removeControls();
|
|
8621
|
+
this.#controlsEl = userControls;
|
|
8622
|
+
}
|
|
8623
|
+
this.#wireControlsToMedia();
|
|
8624
|
+
return;
|
|
8625
|
+
}
|
|
8626
|
+
if (this.#isEnabledAttr("controls", false)) {
|
|
8627
|
+
this.#ensureControls();
|
|
8628
|
+
} else {
|
|
8629
|
+
this.#removeControls();
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
|
|
8633
|
+
#ensureControls() {
|
|
8634
|
+
if (this.#controlsEl && this.#controlsEl.isConnected) {
|
|
8635
|
+
this.#wireControlsToMedia();
|
|
8636
|
+
return;
|
|
8637
|
+
}
|
|
8638
|
+
const controls = document.createElement("fig-media-controls");
|
|
8639
|
+
controls.setAttribute("data-generated", "");
|
|
8640
|
+
controls.setAttribute("overlay", "");
|
|
8641
|
+
this.append(controls);
|
|
8642
|
+
this.#controlsEl = controls;
|
|
8643
|
+
this.#wireControlsToMedia();
|
|
8644
|
+
}
|
|
8645
|
+
|
|
8646
|
+
#wireControlsToMedia() {
|
|
8647
|
+
if (!this.#controlsEl || !this.#mediaEl) return;
|
|
8648
|
+
if (
|
|
8649
|
+
this.#controlsWiredFor === this.#mediaEl &&
|
|
8650
|
+
this.#controlsWiredControls === this.#controlsEl
|
|
8651
|
+
) {
|
|
8652
|
+
return;
|
|
8653
|
+
}
|
|
8654
|
+
this.#unwireControls();
|
|
8655
|
+
|
|
8656
|
+
const controls = this.#controlsEl;
|
|
8657
|
+
const video = this.#mediaEl;
|
|
8658
|
+
this.#controlsWiredFor = video;
|
|
8659
|
+
this.#controlsWiredControls = controls;
|
|
8660
|
+
|
|
8661
|
+
let pendingSeekTime = null;
|
|
8662
|
+
const syncFromVideo = () => {
|
|
8663
|
+
controls.playing = !video.paused && !video.ended;
|
|
8664
|
+
if (Number.isFinite(video.duration)) controls.duration = video.duration;
|
|
8665
|
+
if (pendingSeekTime !== null) {
|
|
8666
|
+
if (Math.abs(video.currentTime - pendingSeekTime) < 0.25) {
|
|
8667
|
+
pendingSeekTime = null;
|
|
8668
|
+
} else {
|
|
8669
|
+
return;
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
controls.time = video.currentTime || 0;
|
|
8673
|
+
};
|
|
8674
|
+
const onPlay = () => {
|
|
8675
|
+
const p = video.play?.();
|
|
8676
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
8677
|
+
};
|
|
8678
|
+
const onPause = () => video.pause?.();
|
|
8679
|
+
const onSeek = (e) => {
|
|
8680
|
+
const next = Number(e?.detail?.time);
|
|
8681
|
+
if (!Number.isFinite(next)) return;
|
|
8682
|
+
pendingSeekTime = next;
|
|
8683
|
+
try { video.currentTime = next; } catch {}
|
|
8684
|
+
};
|
|
8685
|
+
|
|
8686
|
+
this.#controlsSync = syncFromVideo;
|
|
8687
|
+
this.#controlsOnPlay = onPlay;
|
|
8688
|
+
this.#controlsOnPause = onPause;
|
|
8689
|
+
this.#controlsOnSeek = onSeek;
|
|
8690
|
+
|
|
8691
|
+
video.addEventListener("play", syncFromVideo);
|
|
8692
|
+
video.addEventListener("pause", syncFromVideo);
|
|
8693
|
+
video.addEventListener("ended", syncFromVideo);
|
|
8694
|
+
video.addEventListener("timeupdate", syncFromVideo);
|
|
8695
|
+
video.addEventListener("loadedmetadata", syncFromVideo);
|
|
8696
|
+
video.addEventListener("durationchange", syncFromVideo);
|
|
8697
|
+
video.addEventListener("seeked", syncFromVideo);
|
|
8698
|
+
controls.addEventListener("play", onPlay);
|
|
8699
|
+
controls.addEventListener("pause", onPause);
|
|
8700
|
+
controls.addEventListener("seek", onSeek);
|
|
8701
|
+
|
|
8702
|
+
syncFromVideo();
|
|
8703
|
+
}
|
|
8704
|
+
|
|
8705
|
+
#unwireControls() {
|
|
8706
|
+
const video = this.#controlsWiredFor;
|
|
8707
|
+
const controls = this.#controlsWiredControls;
|
|
8708
|
+
if (video && this.#controlsSync) {
|
|
8709
|
+
video.removeEventListener("play", this.#controlsSync);
|
|
8710
|
+
video.removeEventListener("pause", this.#controlsSync);
|
|
8711
|
+
video.removeEventListener("ended", this.#controlsSync);
|
|
8712
|
+
video.removeEventListener("timeupdate", this.#controlsSync);
|
|
8713
|
+
video.removeEventListener("loadedmetadata", this.#controlsSync);
|
|
8714
|
+
video.removeEventListener("durationchange", this.#controlsSync);
|
|
8715
|
+
video.removeEventListener("seeked", this.#controlsSync);
|
|
8716
|
+
}
|
|
8717
|
+
if (controls) {
|
|
8718
|
+
if (this.#controlsOnPlay) controls.removeEventListener("play", this.#controlsOnPlay);
|
|
8719
|
+
if (this.#controlsOnPause) controls.removeEventListener("pause", this.#controlsOnPause);
|
|
8720
|
+
if (this.#controlsOnSeek) controls.removeEventListener("seek", this.#controlsOnSeek);
|
|
8721
|
+
}
|
|
8722
|
+
this.#controlsWiredFor = null;
|
|
8723
|
+
this.#controlsWiredControls = null;
|
|
8724
|
+
this.#controlsSync = null;
|
|
8725
|
+
this.#controlsOnPlay = null;
|
|
8726
|
+
this.#controlsOnPause = null;
|
|
8727
|
+
this.#controlsOnSeek = null;
|
|
8728
|
+
}
|
|
8729
|
+
|
|
8730
|
+
#removeControls() {
|
|
8731
|
+
this.#unwireControls();
|
|
8732
|
+
if (!this.#controlsEl) return;
|
|
8733
|
+
if (this.#controlsEl.hasAttribute("data-generated")) {
|
|
8734
|
+
this.#controlsEl.remove();
|
|
8735
|
+
}
|
|
8736
|
+
this.#controlsEl = null;
|
|
8737
|
+
}
|
|
8738
|
+
|
|
8739
|
+
toggle() {
|
|
8740
|
+
if (!this.#mediaEl || this.mediaKind !== "video") return;
|
|
8741
|
+
if (this.#mediaEl.paused || this.#mediaEl.ended) this.play();
|
|
8742
|
+
else this.pause();
|
|
8743
|
+
}
|
|
8744
|
+
|
|
8745
|
+
play() {
|
|
8746
|
+
if (this.mediaKind !== "video" || !this.#mediaEl) return;
|
|
8747
|
+
const p = this.#mediaEl.play();
|
|
8748
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
8749
|
+
}
|
|
8750
|
+
|
|
8751
|
+
pause() {
|
|
8752
|
+
if (this.mediaKind !== "video" || !this.#mediaEl) return;
|
|
8753
|
+
this.#mediaEl.pause();
|
|
8594
8754
|
}
|
|
8595
8755
|
|
|
8596
8756
|
#createFileInput() {
|
|
@@ -8775,6 +8935,203 @@ class FigVideo extends FigMedia {
|
|
|
8775
8935
|
}
|
|
8776
8936
|
customElements.define("fig-video", FigVideo);
|
|
8777
8937
|
|
|
8938
|
+
/**
|
|
8939
|
+
* <fig-media-controls> — Standalone playback controls UI.
|
|
8940
|
+
*
|
|
8941
|
+
* Renders a play/pause button, a scrubber slider, and a MM:SS time display.
|
|
8942
|
+
* Holds its own state via attributes — no media element required.
|
|
8943
|
+
*
|
|
8944
|
+
* Attributes:
|
|
8945
|
+
* - `playing` (boolean presence) — current play/pause state
|
|
8946
|
+
* - `duration` (number, seconds) — total track length
|
|
8947
|
+
* - `time` (number, seconds) — current playhead position
|
|
8948
|
+
*
|
|
8949
|
+
* Events:
|
|
8950
|
+
* - `play` — emitted when the user toggles playback on (detail: { playing: true })
|
|
8951
|
+
* - `pause` — emitted when the user toggles playback off (detail: { playing: false })
|
|
8952
|
+
* - `seek` — emitted when the user drags the scrubber (detail: { time })
|
|
8953
|
+
*
|
|
8954
|
+
* Properties: `playing`, `duration`, `time` mirror the attributes.
|
|
8955
|
+
*/
|
|
8956
|
+
class FigMediaControls extends HTMLElement {
|
|
8957
|
+
#playBtn = null;
|
|
8958
|
+
#playTooltip = null;
|
|
8959
|
+
#timeSlider = null;
|
|
8960
|
+
#timeEl = null;
|
|
8961
|
+
#userSeeking = false;
|
|
8962
|
+
#rendered = false;
|
|
8963
|
+
|
|
8964
|
+
static get observedAttributes() {
|
|
8965
|
+
return ["playing", "duration", "time"];
|
|
8966
|
+
}
|
|
8967
|
+
|
|
8968
|
+
connectedCallback() {
|
|
8969
|
+
this.#render();
|
|
8970
|
+
this.#syncPlayingUi();
|
|
8971
|
+
this.#syncTimeUi();
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8974
|
+
get playing() {
|
|
8975
|
+
return this.hasAttribute("playing") && this.getAttribute("playing") !== "false";
|
|
8976
|
+
}
|
|
8977
|
+
set playing(value) {
|
|
8978
|
+
if (value) this.setAttribute("playing", "");
|
|
8979
|
+
else this.removeAttribute("playing");
|
|
8980
|
+
}
|
|
8981
|
+
|
|
8982
|
+
get duration() {
|
|
8983
|
+
const n = Number(this.getAttribute("duration"));
|
|
8984
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
8985
|
+
}
|
|
8986
|
+
set duration(value) {
|
|
8987
|
+
const n = Number(value);
|
|
8988
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
8989
|
+
this.removeAttribute("duration");
|
|
8990
|
+
return;
|
|
8991
|
+
}
|
|
8992
|
+
this.setAttribute("duration", String(n));
|
|
8993
|
+
}
|
|
8994
|
+
|
|
8995
|
+
get time() {
|
|
8996
|
+
const n = Number(this.getAttribute("time"));
|
|
8997
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
8998
|
+
}
|
|
8999
|
+
set time(value) {
|
|
9000
|
+
const n = Number(value);
|
|
9001
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
9002
|
+
this.removeAttribute("time");
|
|
9003
|
+
return;
|
|
9004
|
+
}
|
|
9005
|
+
this.setAttribute("time", String(n));
|
|
9006
|
+
}
|
|
9007
|
+
|
|
9008
|
+
attributeChangedCallback(name) {
|
|
9009
|
+
if (!this.#rendered) return;
|
|
9010
|
+
if (name === "playing") this.#syncPlayingUi();
|
|
9011
|
+
if (name === "duration" || name === "time") this.#syncTimeUi();
|
|
9012
|
+
}
|
|
9013
|
+
|
|
9014
|
+
#render() {
|
|
9015
|
+
if (this.#rendered) return;
|
|
9016
|
+
this.#rendered = true;
|
|
9017
|
+
|
|
9018
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
9019
|
+
tooltip.setAttribute("text", "Play");
|
|
9020
|
+
const btn = document.createElement("fig-button");
|
|
9021
|
+
btn.setAttribute("variant", "ghost");
|
|
9022
|
+
btn.setAttribute("size", "small");
|
|
9023
|
+
btn.setAttribute("icon", "true");
|
|
9024
|
+
btn.setAttribute("aria-label", "Play");
|
|
9025
|
+
const icon = document.createElement("span");
|
|
9026
|
+
icon.className = "fig-mask-icon fig-media-controls-play-icon";
|
|
9027
|
+
icon.style.setProperty("--icon", "var(--icon-play)");
|
|
9028
|
+
icon.style.setProperty("--size", "1.5rem");
|
|
9029
|
+
btn.append(icon);
|
|
9030
|
+
tooltip.append(btn);
|
|
9031
|
+
btn.addEventListener("click", (e) => {
|
|
9032
|
+
e.preventDefault();
|
|
9033
|
+
e.stopPropagation();
|
|
9034
|
+
this.toggle();
|
|
9035
|
+
});
|
|
9036
|
+
|
|
9037
|
+
const slider = document.createElement("fig-slider");
|
|
9038
|
+
slider.setAttribute("variant", "neue");
|
|
9039
|
+
slider.setAttribute("min", "0");
|
|
9040
|
+
slider.setAttribute("max", String(this.duration));
|
|
9041
|
+
slider.setAttribute("step", "0.1");
|
|
9042
|
+
slider.setAttribute("value", String(this.time));
|
|
9043
|
+
slider.setAttribute("full", "");
|
|
9044
|
+
const timeEl = document.createElement("label");
|
|
9045
|
+
timeEl.className = "fig-media-controls-time";
|
|
9046
|
+
timeEl.textContent = this.#formatTime(this.time);
|
|
9047
|
+
|
|
9048
|
+
const handleSeek = (e) => {
|
|
9049
|
+
const host = e.currentTarget;
|
|
9050
|
+
const next = Number(host?.value);
|
|
9051
|
+
if (!Number.isFinite(next)) return;
|
|
9052
|
+
this.#userSeeking = true;
|
|
9053
|
+
this.setAttribute("time", String(next));
|
|
9054
|
+
this.dispatchEvent(
|
|
9055
|
+
new CustomEvent("seek", {
|
|
9056
|
+
bubbles: true,
|
|
9057
|
+
composed: true,
|
|
9058
|
+
detail: { time: next },
|
|
9059
|
+
}),
|
|
9060
|
+
);
|
|
9061
|
+
requestAnimationFrame(() => {
|
|
9062
|
+
this.#userSeeking = false;
|
|
9063
|
+
});
|
|
9064
|
+
};
|
|
9065
|
+
slider.addEventListener("input", handleSeek);
|
|
9066
|
+
slider.addEventListener("change", handleSeek);
|
|
9067
|
+
|
|
9068
|
+
this.append(tooltip, slider, timeEl);
|
|
9069
|
+
|
|
9070
|
+
this.#playBtn = btn;
|
|
9071
|
+
this.#playTooltip = tooltip;
|
|
9072
|
+
this.#timeSlider = slider;
|
|
9073
|
+
this.#timeEl = timeEl;
|
|
9074
|
+
}
|
|
9075
|
+
|
|
9076
|
+
#formatTime(seconds) {
|
|
9077
|
+
if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
|
|
9078
|
+
const total = Math.floor(seconds);
|
|
9079
|
+
const m = Math.floor(total / 60);
|
|
9080
|
+
const s = total % 60;
|
|
9081
|
+
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
9082
|
+
}
|
|
9083
|
+
|
|
9084
|
+
#syncPlayingUi() {
|
|
9085
|
+
if (!this.#playBtn) return;
|
|
9086
|
+
const playing = this.playing;
|
|
9087
|
+
this.#playBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
|
|
9088
|
+
this.#playTooltip?.setAttribute("text", playing ? "Pause" : "Play");
|
|
9089
|
+
const icon = this.#playBtn.querySelector(".fig-media-controls-play-icon");
|
|
9090
|
+
if (icon) {
|
|
9091
|
+
icon.style.setProperty(
|
|
9092
|
+
"--icon",
|
|
9093
|
+
playing ? "var(--icon-pause)" : "var(--icon-play)",
|
|
9094
|
+
);
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
|
|
9098
|
+
#syncTimeUi() {
|
|
9099
|
+
if (!this.#timeSlider) return;
|
|
9100
|
+
const duration = this.duration;
|
|
9101
|
+
if (Number(this.#timeSlider.getAttribute("max")) !== duration) {
|
|
9102
|
+
this.#timeSlider.setAttribute("max", String(duration));
|
|
9103
|
+
}
|
|
9104
|
+
const t = this.time;
|
|
9105
|
+
if (!this.#userSeeking) {
|
|
9106
|
+
this.#timeSlider.setAttribute("value", String(t));
|
|
9107
|
+
}
|
|
9108
|
+
if (this.#timeEl) this.#timeEl.textContent = this.#formatTime(t);
|
|
9109
|
+
}
|
|
9110
|
+
|
|
9111
|
+
toggle() {
|
|
9112
|
+
const next = !this.playing;
|
|
9113
|
+
this.playing = next;
|
|
9114
|
+
this.dispatchEvent(
|
|
9115
|
+
new CustomEvent(next ? "play" : "pause", {
|
|
9116
|
+
bubbles: true,
|
|
9117
|
+
composed: true,
|
|
9118
|
+
detail: { playing: next },
|
|
9119
|
+
}),
|
|
9120
|
+
);
|
|
9121
|
+
}
|
|
9122
|
+
|
|
9123
|
+
play() {
|
|
9124
|
+
if (this.playing) return;
|
|
9125
|
+
this.toggle();
|
|
9126
|
+
}
|
|
9127
|
+
|
|
9128
|
+
pause() {
|
|
9129
|
+
if (!this.playing) return;
|
|
9130
|
+
this.toggle();
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
customElements.define("fig-media-controls", FigMediaControls);
|
|
9134
|
+
|
|
8778
9135
|
/* File Upload Input */
|
|
8779
9136
|
class FigInputFile extends HTMLElement {
|
|
8780
9137
|
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url"];
|
package/package.json
CHANGED