@rogieking/figui3 11.2.2 → 11.3.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/fig-lab/SKILL.md +2 -2
- package/.cursor/skills/fig-lab/components.md +0 -23
- package/.cursor/skills/fig-lab/reference.md +0 -18
- package/.cursor/skills/figui3/components.md +19 -2
- package/.cursor/skills/figui3/reference.md +1 -0
- package/CHANGELOG.md +32 -0
- package/README.md +39 -21
- package/components.css +98 -3
- package/dist/components.css +1 -1
- package/dist/fig-editor.js +1 -1
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +6 -6
- package/fig-editor.js +1 -1
- package/fig-lab.css +0 -115
- package/fig-lab.js +0 -368
- package/fig.js +885 -28
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -6342,6 +6342,7 @@ figDefineElement("fig-options", FigOptions);
|
|
|
6342
6342
|
* @attr {number} min - The minimum value
|
|
6343
6343
|
* @attr {number} max - The maximum value
|
|
6344
6344
|
* @attr {number} step - The step increment
|
|
6345
|
+
* @attr {number} default - Explicit tick and native snap target
|
|
6345
6346
|
* @attr {boolean} text - Whether to show a text input alongside the slider (default true)
|
|
6346
6347
|
* @attr {string} placeholder - Placeholder for the number input when text is enabled
|
|
6347
6348
|
* @attr {string} units - The units to display after the value
|
|
@@ -6634,6 +6635,8 @@ class FigSlider extends HTMLElement {
|
|
|
6634
6635
|
this.datalist.append(option);
|
|
6635
6636
|
this.inputContainer.append(this.datalist);
|
|
6636
6637
|
this.input.setAttribute("list", this.datalist.getAttribute("id"));
|
|
6638
|
+
} else if (this.#hasExplicitValidDefault()) {
|
|
6639
|
+
this.#createDefaultDatalist();
|
|
6637
6640
|
}
|
|
6638
6641
|
this.#syncDefaultDatalist();
|
|
6639
6642
|
this.#syncValue();
|
|
@@ -6754,6 +6757,25 @@ class FigSlider extends HTMLElement {
|
|
|
6754
6757
|
const tolerance = Number.EPSILON * Math.max(1, Math.abs(steps)) * 16;
|
|
6755
6758
|
return Math.abs(steps - nearest) <= tolerance;
|
|
6756
6759
|
}
|
|
6760
|
+
#supportsGeneratedDefaultDatalist() {
|
|
6761
|
+
return ["range", "hue", "opacity"].includes(this.type);
|
|
6762
|
+
}
|
|
6763
|
+
#hasExplicitValidDefault() {
|
|
6764
|
+
if (
|
|
6765
|
+
!this.hasAttribute("default") ||
|
|
6766
|
+
!this.#supportsGeneratedDefaultDatalist()
|
|
6767
|
+
) {
|
|
6768
|
+
return false;
|
|
6769
|
+
}
|
|
6770
|
+
const { min, max } = this.#getBounds();
|
|
6771
|
+
const parsed = this.#toFiniteNumber(this.getAttribute("default"));
|
|
6772
|
+
return (
|
|
6773
|
+
parsed !== null &&
|
|
6774
|
+
parsed >= min &&
|
|
6775
|
+
parsed <= max &&
|
|
6776
|
+
this.#isStepAligned(parsed)
|
|
6777
|
+
);
|
|
6778
|
+
}
|
|
6757
6779
|
#resolveDefaultValue(rawDefault = this.getAttribute("default")) {
|
|
6758
6780
|
const { min, max } = this.#getBounds();
|
|
6759
6781
|
const parsed = this.#toFiniteNumber(rawDefault);
|
|
@@ -6959,8 +6981,24 @@ class FigSlider extends HTMLElement {
|
|
|
6959
6981
|
}
|
|
6960
6982
|
|
|
6961
6983
|
#syncDefaultDatalist() {
|
|
6984
|
+
const generatedDefault =
|
|
6985
|
+
this.datalist?.getAttribute("data-generated") === "slider-default";
|
|
6986
|
+
const shouldGenerateDefault = this.#hasExplicitValidDefault();
|
|
6987
|
+
if (generatedDefault && !shouldGenerateDefault) {
|
|
6988
|
+
this.datalist.remove();
|
|
6989
|
+
this.datalist = null;
|
|
6990
|
+
this.input?.removeAttribute("list");
|
|
6991
|
+
} else if (!this.datalist && shouldGenerateDefault) {
|
|
6992
|
+
this.#createDefaultDatalist();
|
|
6993
|
+
}
|
|
6962
6994
|
if (!this.datalist) return;
|
|
6963
6995
|
const defaultValue = String(this.default);
|
|
6996
|
+
if (generatedDefault) {
|
|
6997
|
+
const option =
|
|
6998
|
+
this.datalist.querySelector("option") ?? document.createElement("option");
|
|
6999
|
+
option.value = defaultValue;
|
|
7000
|
+
this.datalist.replaceChildren(option);
|
|
7001
|
+
}
|
|
6964
7002
|
const options = [...this.datalist.querySelectorAll("option")];
|
|
6965
7003
|
options.forEach((option) => option.removeAttribute("default"));
|
|
6966
7004
|
let defaultOption = options.find((option) => option.value === defaultValue);
|
|
@@ -6976,6 +7014,17 @@ class FigSlider extends HTMLElement {
|
|
|
6976
7014
|
defaultOption?.setAttribute("default", "true");
|
|
6977
7015
|
}
|
|
6978
7016
|
|
|
7017
|
+
#createDefaultDatalist() {
|
|
7018
|
+
this.datalist = document.createElement("datalist");
|
|
7019
|
+
this.datalist.setAttribute("data-generated", "slider-default");
|
|
7020
|
+
this.datalist.setAttribute("id", figUniqueId());
|
|
7021
|
+
const option = document.createElement("option");
|
|
7022
|
+
option.value = String(this.default);
|
|
7023
|
+
this.datalist.append(option);
|
|
7024
|
+
this.inputContainer.append(this.datalist);
|
|
7025
|
+
this.input.setAttribute("list", this.datalist.id);
|
|
7026
|
+
}
|
|
7027
|
+
|
|
6979
7028
|
#regenerateWhenIdle() {
|
|
6980
7029
|
if (this.#isInteracting) {
|
|
6981
7030
|
this.#pendingRegeneration = true;
|
|
@@ -7779,6 +7828,12 @@ class FigInputNumber extends HTMLElement {
|
|
|
7779
7828
|
#precision;
|
|
7780
7829
|
#isInteracting = false;
|
|
7781
7830
|
#stepperEl = null;
|
|
7831
|
+
#stepperPointerId = null;
|
|
7832
|
+
#stepperRepeatTimeout = 0;
|
|
7833
|
+
#stepperRepeatInterval = 0;
|
|
7834
|
+
#stepperDidChange = false;
|
|
7835
|
+
#boundStepperPointerEnd;
|
|
7836
|
+
#boundStepperWindowBlur;
|
|
7782
7837
|
#mutationObserver = null;
|
|
7783
7838
|
#syncRaf = 0;
|
|
7784
7839
|
#previousUserSelect = "";
|
|
@@ -7791,6 +7846,8 @@ class FigInputNumber extends HTMLElement {
|
|
|
7791
7846
|
"aria-required",
|
|
7792
7847
|
];
|
|
7793
7848
|
static #DEFAULT_UNITS_DISALLOW = "px";
|
|
7849
|
+
static #STEPPER_REPEAT_DELAY = 350;
|
|
7850
|
+
static #STEPPER_REPEAT_INTERVAL = 60;
|
|
7794
7851
|
|
|
7795
7852
|
#parseUnitsDisallowList(value) {
|
|
7796
7853
|
return (value || "")
|
|
@@ -7832,11 +7889,12 @@ class FigInputNumber extends HTMLElement {
|
|
|
7832
7889
|
const btn = e.target.closest("button");
|
|
7833
7890
|
if (!btn || this.disabled || btn.disabled) return;
|
|
7834
7891
|
const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
|
|
7835
|
-
this.#
|
|
7892
|
+
this.#startStepperRepeat(e, dir);
|
|
7836
7893
|
this.input.focus();
|
|
7837
7894
|
});
|
|
7838
7895
|
this.append(this.#stepperEl);
|
|
7839
7896
|
} else if (!hasSteppers && this.#stepperEl) {
|
|
7897
|
+
this.#stopStepperRepeat();
|
|
7840
7898
|
this.#stepperEl.remove();
|
|
7841
7899
|
this.#stepperEl = null;
|
|
7842
7900
|
}
|
|
@@ -7867,13 +7925,60 @@ class FigInputNumber extends HTMLElement {
|
|
|
7867
7925
|
down.disabled = disabled || atMin;
|
|
7868
7926
|
}
|
|
7869
7927
|
|
|
7928
|
+
#startStepperRepeat(event, direction) {
|
|
7929
|
+
this.#stopStepperRepeat();
|
|
7930
|
+
this.#stepperPointerId = event.pointerId;
|
|
7931
|
+
this.#stepperDidChange = this.#stepValue(direction);
|
|
7932
|
+
window.addEventListener("pointerup", this.#boundStepperPointerEnd);
|
|
7933
|
+
window.addEventListener("pointercancel", this.#boundStepperPointerEnd);
|
|
7934
|
+
window.addEventListener("blur", this.#boundStepperWindowBlur);
|
|
7935
|
+
if (!this.#stepperDidChange) return;
|
|
7936
|
+
|
|
7937
|
+
this.#stepperRepeatTimeout = window.setTimeout(() => {
|
|
7938
|
+
this.#stepperRepeatTimeout = 0;
|
|
7939
|
+
this.#stepperRepeatInterval = window.setInterval(() => {
|
|
7940
|
+
const changed = this.#stepValue(direction);
|
|
7941
|
+
this.#stepperDidChange ||= changed;
|
|
7942
|
+
if (!changed) this.#clearStepperRepeatTimers();
|
|
7943
|
+
}, FigInputNumber.#STEPPER_REPEAT_INTERVAL);
|
|
7944
|
+
}, FigInputNumber.#STEPPER_REPEAT_DELAY);
|
|
7945
|
+
}
|
|
7946
|
+
|
|
7947
|
+
#clearStepperRepeatTimers() {
|
|
7948
|
+
if (this.#stepperRepeatTimeout) {
|
|
7949
|
+
clearTimeout(this.#stepperRepeatTimeout);
|
|
7950
|
+
this.#stepperRepeatTimeout = 0;
|
|
7951
|
+
}
|
|
7952
|
+
if (this.#stepperRepeatInterval) {
|
|
7953
|
+
clearInterval(this.#stepperRepeatInterval);
|
|
7954
|
+
this.#stepperRepeatInterval = 0;
|
|
7955
|
+
}
|
|
7956
|
+
}
|
|
7957
|
+
|
|
7958
|
+
#stopStepperRepeat(commit = true) {
|
|
7959
|
+
this.#clearStepperRepeatTimers();
|
|
7960
|
+
window.removeEventListener("pointerup", this.#boundStepperPointerEnd);
|
|
7961
|
+
window.removeEventListener("pointercancel", this.#boundStepperPointerEnd);
|
|
7962
|
+
window.removeEventListener("blur", this.#boundStepperWindowBlur);
|
|
7963
|
+
this.#stepperPointerId = null;
|
|
7964
|
+
if (commit && this.#stepperDidChange) {
|
|
7965
|
+
this.dispatchEvent(
|
|
7966
|
+
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
7967
|
+
);
|
|
7968
|
+
}
|
|
7969
|
+
this.#stepperDidChange = false;
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7870
7972
|
#stepValue(direction) {
|
|
7973
|
+
if (this.disabled) return false;
|
|
7871
7974
|
const step = this.step || 1;
|
|
7975
|
+
const previousValue = this.value;
|
|
7872
7976
|
let numericValue = this.#getNumericValue(this.input.value);
|
|
7873
7977
|
let value =
|
|
7874
7978
|
(numericValue !== "" ? Number(numericValue) / (this.transform || 1) : 0) +
|
|
7875
7979
|
step * direction;
|
|
7876
7980
|
value = this.#sanitizeInput(value, false);
|
|
7981
|
+
if (value === previousValue) return false;
|
|
7877
7982
|
this.value = value;
|
|
7878
7983
|
this.input.value = this.#formatWithUnit(this.value);
|
|
7879
7984
|
this.#syncStepperState();
|
|
@@ -7881,9 +7986,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
7881
7986
|
this.dispatchEvent(
|
|
7882
7987
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
7883
7988
|
);
|
|
7884
|
-
|
|
7885
|
-
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
7886
|
-
);
|
|
7989
|
+
return true;
|
|
7887
7990
|
}
|
|
7888
7991
|
|
|
7889
7992
|
constructor() {
|
|
@@ -7910,6 +8013,16 @@ class FigInputNumber extends HTMLElement {
|
|
|
7910
8013
|
this.#boundKeyDown = (e) => {
|
|
7911
8014
|
this.#handleKeyDown(e);
|
|
7912
8015
|
};
|
|
8016
|
+
this.#boundStepperPointerEnd = (e) => {
|
|
8017
|
+
if (
|
|
8018
|
+
this.#stepperPointerId !== null &&
|
|
8019
|
+
e.pointerId !== this.#stepperPointerId
|
|
8020
|
+
) {
|
|
8021
|
+
return;
|
|
8022
|
+
}
|
|
8023
|
+
this.#stopStepperRepeat();
|
|
8024
|
+
};
|
|
8025
|
+
this.#boundStepperWindowBlur = () => this.#stopStepperRepeat();
|
|
7913
8026
|
this.#boundFocusControl = this.focus.bind(this);
|
|
7914
8027
|
this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
|
|
7915
8028
|
}
|
|
@@ -8001,6 +8114,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
8001
8114
|
window.removeEventListener("pointermove", this.#boundMouseMove);
|
|
8002
8115
|
window.removeEventListener("pointerup", this.#boundMouseUp);
|
|
8003
8116
|
window.removeEventListener("blur", this.#boundWindowBlur);
|
|
8117
|
+
this.#stopStepperRepeat(false);
|
|
8004
8118
|
this.#mutationObserver?.disconnect();
|
|
8005
8119
|
this.#mutationObserver = null;
|
|
8006
8120
|
if (this.#syncRaf) {
|
|
@@ -8375,6 +8489,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
8375
8489
|
case "disabled":
|
|
8376
8490
|
this.disabled = this.input.disabled =
|
|
8377
8491
|
newValue !== null && newValue !== "false";
|
|
8492
|
+
if (this.disabled) this.#stopStepperRepeat();
|
|
8378
8493
|
this.#syncStepperState();
|
|
8379
8494
|
break;
|
|
8380
8495
|
case "units":
|
|
@@ -14864,9 +14979,354 @@ class FigMediaControls extends HTMLElement {
|
|
|
14864
14979
|
}
|
|
14865
14980
|
figDefineElement("fig-media-controls", FigMediaControls);
|
|
14866
14981
|
|
|
14982
|
+
/**
|
|
14983
|
+
* Responsive waveform generated from an audio URL.
|
|
14984
|
+
*
|
|
14985
|
+
* @attr {string} url - Audio URL to decode when CORS permits.
|
|
14986
|
+
* @attr {number} polarization - Waveform contrast around half-height. Defaults to 1.5.
|
|
14987
|
+
* @attr {number} progress - Completed fraction from 0 to 1. Defaults to 0.
|
|
14988
|
+
* @attr {boolean|string} full - Makes the waveform fill its container.
|
|
14989
|
+
*/
|
|
14990
|
+
class FigAudioWaveform extends HTMLElement {
|
|
14991
|
+
static #instanceCount = 0;
|
|
14992
|
+
static observedAttributes = ["url", "polarization", "progress"];
|
|
14993
|
+
|
|
14994
|
+
#surface = null;
|
|
14995
|
+
#svg = null;
|
|
14996
|
+
#waveformPath = null;
|
|
14997
|
+
#progressPath = null;
|
|
14998
|
+
#progressClipRect = null;
|
|
14999
|
+
#resizeObserver = null;
|
|
15000
|
+
#abortController = null;
|
|
15001
|
+
#audioContext = null;
|
|
15002
|
+
#levels = [];
|
|
15003
|
+
#generation = 0;
|
|
15004
|
+
#layoutFrame = 0;
|
|
15005
|
+
|
|
15006
|
+
connectedCallback() {
|
|
15007
|
+
if (!this.#surface) this.#render();
|
|
15008
|
+
this.#observeWaveform();
|
|
15009
|
+
this.#syncSource();
|
|
15010
|
+
}
|
|
15011
|
+
|
|
15012
|
+
disconnectedCallback() {
|
|
15013
|
+
this.#cancelDecode();
|
|
15014
|
+
this.#resizeObserver?.disconnect();
|
|
15015
|
+
this.#resizeObserver = null;
|
|
15016
|
+
if (this.#layoutFrame) cancelAnimationFrame(this.#layoutFrame);
|
|
15017
|
+
this.#layoutFrame = 0;
|
|
15018
|
+
}
|
|
15019
|
+
|
|
15020
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
15021
|
+
if (oldValue === newValue || !this.#surface) return;
|
|
15022
|
+
if (name === "url") this.#syncSource();
|
|
15023
|
+
if (name === "polarization") this.#queueWaveformLayout();
|
|
15024
|
+
if (name === "progress") this.#syncProgress();
|
|
15025
|
+
}
|
|
15026
|
+
|
|
15027
|
+
#render() {
|
|
15028
|
+
this.#surface = document.createElement("div");
|
|
15029
|
+
this.#surface.className = "fig-audio-waveform-surface";
|
|
15030
|
+
this.#surface.setAttribute("aria-hidden", "true");
|
|
15031
|
+
|
|
15032
|
+
this.#svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
15033
|
+
this.#svg.setAttribute("aria-hidden", "true");
|
|
15034
|
+
this.#svg.setAttribute("preserveAspectRatio", "none");
|
|
15035
|
+
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
15036
|
+
const progressClip = document.createElementNS(
|
|
15037
|
+
"http://www.w3.org/2000/svg",
|
|
15038
|
+
"clipPath",
|
|
15039
|
+
);
|
|
15040
|
+
const progressClipId = `fig-audio-waveform-progress-${++FigAudioWaveform.#instanceCount}`;
|
|
15041
|
+
progressClip.id = progressClipId;
|
|
15042
|
+
progressClip.setAttribute("clipPathUnits", "userSpaceOnUse");
|
|
15043
|
+
this.#progressClipRect = document.createElementNS(
|
|
15044
|
+
"http://www.w3.org/2000/svg",
|
|
15045
|
+
"rect",
|
|
15046
|
+
);
|
|
15047
|
+
progressClip.append(this.#progressClipRect);
|
|
15048
|
+
defs.append(progressClip);
|
|
15049
|
+
|
|
15050
|
+
this.#waveformPath = document.createElementNS(
|
|
15051
|
+
"http://www.w3.org/2000/svg",
|
|
15052
|
+
"path",
|
|
15053
|
+
);
|
|
15054
|
+
this.#waveformPath.classList.add("fig-audio-waveform-path");
|
|
15055
|
+
this.#progressPath = document.createElementNS(
|
|
15056
|
+
"http://www.w3.org/2000/svg",
|
|
15057
|
+
"path",
|
|
15058
|
+
);
|
|
15059
|
+
this.#progressPath.classList.add("fig-audio-waveform-progress");
|
|
15060
|
+
this.#progressPath.setAttribute("clip-path", `url(#${progressClipId})`);
|
|
15061
|
+
this.#svg.append(defs, this.#waveformPath, this.#progressPath);
|
|
15062
|
+
this.#surface.appendChild(this.#svg);
|
|
15063
|
+
this.replaceChildren(this.#surface);
|
|
15064
|
+
|
|
15065
|
+
this.#observeWaveform();
|
|
15066
|
+
}
|
|
15067
|
+
|
|
15068
|
+
#observeWaveform() {
|
|
15069
|
+
if (this.#resizeObserver || !this.#svg) return;
|
|
15070
|
+
this.#resizeObserver = new ResizeObserver(() => this.#queueWaveformLayout());
|
|
15071
|
+
this.#resizeObserver.observe(this.#svg);
|
|
15072
|
+
}
|
|
15073
|
+
|
|
15074
|
+
#syncSource() {
|
|
15075
|
+
const url = this.getAttribute("url");
|
|
15076
|
+
if (!url) {
|
|
15077
|
+
this.#clearWaveform();
|
|
15078
|
+
return;
|
|
15079
|
+
}
|
|
15080
|
+
|
|
15081
|
+
this.#decodeSource(async (signal) => {
|
|
15082
|
+
const response = await fetch(url, { signal });
|
|
15083
|
+
if (!response.ok) throw new Error(`Audio request failed: ${response.status}`);
|
|
15084
|
+
return response.arrayBuffer();
|
|
15085
|
+
});
|
|
15086
|
+
}
|
|
15087
|
+
|
|
15088
|
+
async #decodeSource(loadArrayBuffer) {
|
|
15089
|
+
this.#cancelDecode();
|
|
15090
|
+
const generation = ++this.#generation;
|
|
15091
|
+
const controller = new AbortController();
|
|
15092
|
+
this.#abortController = controller;
|
|
15093
|
+
this.setAttribute("data-waveform-state", "loading");
|
|
15094
|
+
this.setAttribute("aria-busy", "true");
|
|
15095
|
+
this.#levels = [];
|
|
15096
|
+
this.#queueWaveformLayout();
|
|
15097
|
+
|
|
15098
|
+
let context = null;
|
|
15099
|
+
try {
|
|
15100
|
+
const arrayBuffer = await loadArrayBuffer(controller.signal);
|
|
15101
|
+
if (generation !== this.#generation || !this.isConnected) return;
|
|
15102
|
+
|
|
15103
|
+
const AudioContextConstructor =
|
|
15104
|
+
window.AudioContext || window.webkitAudioContext;
|
|
15105
|
+
if (!AudioContextConstructor) throw new Error("Web Audio is unavailable");
|
|
15106
|
+
context = new AudioContextConstructor();
|
|
15107
|
+
this.#audioContext = context;
|
|
15108
|
+
const audioBuffer = await context.decodeAudioData(arrayBuffer);
|
|
15109
|
+
if (generation !== this.#generation || !this.isConnected) return;
|
|
15110
|
+
|
|
15111
|
+
this.#levels = this.#extractLevels(audioBuffer);
|
|
15112
|
+
this.setAttribute("data-waveform-state", "ready");
|
|
15113
|
+
this.#queueWaveformLayout();
|
|
15114
|
+
} catch (error) {
|
|
15115
|
+
if (generation !== this.#generation || error?.name === "AbortError") return;
|
|
15116
|
+
this.#levels = new Array(256).fill(0);
|
|
15117
|
+
this.setAttribute("data-waveform-state", "unavailable");
|
|
15118
|
+
this.#queueWaveformLayout();
|
|
15119
|
+
} finally {
|
|
15120
|
+
if (context) {
|
|
15121
|
+
try {
|
|
15122
|
+
await context.close();
|
|
15123
|
+
} catch {
|
|
15124
|
+
// Closing is best-effort in browsers with suspended audio contexts.
|
|
15125
|
+
}
|
|
15126
|
+
}
|
|
15127
|
+
if (this.#audioContext === context) this.#audioContext = null;
|
|
15128
|
+
if (this.#abortController === controller) this.#abortController = null;
|
|
15129
|
+
if (generation === this.#generation) this.removeAttribute("aria-busy");
|
|
15130
|
+
}
|
|
15131
|
+
}
|
|
15132
|
+
|
|
15133
|
+
#extractLevels(audioBuffer) {
|
|
15134
|
+
const binCount = 256;
|
|
15135
|
+
const levels = new Array(binCount).fill(0);
|
|
15136
|
+
const channelCount = audioBuffer.numberOfChannels;
|
|
15137
|
+
const frameCount = audioBuffer.length;
|
|
15138
|
+
const sampleStride = Math.max(1, Math.ceil(frameCount / 500000));
|
|
15139
|
+
let maximum = 0;
|
|
15140
|
+
|
|
15141
|
+
for (let bin = 0; bin < binCount; bin += 1) {
|
|
15142
|
+
const start = Math.floor((bin / binCount) * frameCount);
|
|
15143
|
+
const end = Math.max(
|
|
15144
|
+
start + 1,
|
|
15145
|
+
Math.floor(((bin + 1) / binCount) * frameCount),
|
|
15146
|
+
);
|
|
15147
|
+
let sumSquares = 0;
|
|
15148
|
+
let sampleCount = 0;
|
|
15149
|
+
for (let channel = 0; channel < channelCount; channel += 1) {
|
|
15150
|
+
const samples = audioBuffer.getChannelData(channel);
|
|
15151
|
+
for (let index = start; index < end; index += sampleStride) {
|
|
15152
|
+
const sample = samples[index] || 0;
|
|
15153
|
+
if (!Number.isFinite(sample)) continue;
|
|
15154
|
+
sumSquares += sample * sample;
|
|
15155
|
+
sampleCount += 1;
|
|
15156
|
+
}
|
|
15157
|
+
}
|
|
15158
|
+
const level = sampleCount > 0 ? Math.sqrt(sumSquares / sampleCount) : 0;
|
|
15159
|
+
levels[bin] = level;
|
|
15160
|
+
maximum = Math.max(maximum, level);
|
|
15161
|
+
}
|
|
15162
|
+
|
|
15163
|
+
if (!Number.isFinite(maximum) || maximum <= 0) return levels.fill(0);
|
|
15164
|
+
const minimum = Math.min(...levels);
|
|
15165
|
+
const range = maximum - minimum;
|
|
15166
|
+
if (!Number.isFinite(range) || range <= Number.EPSILON) {
|
|
15167
|
+
return levels.fill(0);
|
|
15168
|
+
}
|
|
15169
|
+
return levels.map((level) => {
|
|
15170
|
+
const normalized = (level - minimum) / range;
|
|
15171
|
+
return Number.isFinite(normalized) ? normalized : 0;
|
|
15172
|
+
});
|
|
15173
|
+
}
|
|
15174
|
+
|
|
15175
|
+
#clearWaveform() {
|
|
15176
|
+
this.#cancelDecode();
|
|
15177
|
+
this.#levels = [];
|
|
15178
|
+
this.removeAttribute("data-waveform-state");
|
|
15179
|
+
this.removeAttribute("aria-busy");
|
|
15180
|
+
this.#queueWaveformLayout();
|
|
15181
|
+
}
|
|
15182
|
+
|
|
15183
|
+
#polarizeLevel(level) {
|
|
15184
|
+
const configured = Number.parseFloat(this.getAttribute("polarization"));
|
|
15185
|
+
const factor = Number.isFinite(configured)
|
|
15186
|
+
? Math.max(0, Math.min(configured, 4))
|
|
15187
|
+
: 1.5;
|
|
15188
|
+
return Math.max(0, Math.min(1, 0.5 + (level - 0.5) * factor));
|
|
15189
|
+
}
|
|
15190
|
+
|
|
15191
|
+
#progress() {
|
|
15192
|
+
const configured = Number.parseFloat(this.getAttribute("progress"));
|
|
15193
|
+
return Number.isFinite(configured)
|
|
15194
|
+
? Math.max(0, Math.min(configured, 1))
|
|
15195
|
+
: 0;
|
|
15196
|
+
}
|
|
15197
|
+
|
|
15198
|
+
#syncProgress() {
|
|
15199
|
+
if (!this.#svg || !this.#progressClipRect) return;
|
|
15200
|
+
const viewBox = this.#svg.viewBox.baseVal;
|
|
15201
|
+
const bounds = this.#svg.getBoundingClientRect();
|
|
15202
|
+
const width = viewBox.width || Math.max(1, Math.round(bounds.width));
|
|
15203
|
+
const height = viewBox.height || Math.max(1, Math.round(bounds.height));
|
|
15204
|
+
this.#progressClipRect.setAttribute("x", "0");
|
|
15205
|
+
this.#progressClipRect.setAttribute("y", "0");
|
|
15206
|
+
this.#progressClipRect.setAttribute("width", String(width * this.#progress()));
|
|
15207
|
+
this.#progressClipRect.setAttribute("height", String(height));
|
|
15208
|
+
}
|
|
15209
|
+
|
|
15210
|
+
#cancelDecode() {
|
|
15211
|
+
this.#generation += 1;
|
|
15212
|
+
this.#abortController?.abort();
|
|
15213
|
+
this.#abortController = null;
|
|
15214
|
+
if (this.#audioContext) {
|
|
15215
|
+
try {
|
|
15216
|
+
this.#audioContext.close();
|
|
15217
|
+
} catch {
|
|
15218
|
+
// Closing is best-effort during teardown and source replacement.
|
|
15219
|
+
}
|
|
15220
|
+
this.#audioContext = null;
|
|
15221
|
+
}
|
|
15222
|
+
}
|
|
15223
|
+
|
|
15224
|
+
#queueWaveformLayout() {
|
|
15225
|
+
if (this.#layoutFrame) return;
|
|
15226
|
+
this.#layoutFrame = requestAnimationFrame(() => {
|
|
15227
|
+
this.#layoutFrame = 0;
|
|
15228
|
+
this.#layoutWaveform();
|
|
15229
|
+
});
|
|
15230
|
+
}
|
|
15231
|
+
|
|
15232
|
+
#layoutWaveform() {
|
|
15233
|
+
if (
|
|
15234
|
+
!this.#svg ||
|
|
15235
|
+
!this.#waveformPath ||
|
|
15236
|
+
!this.#progressPath ||
|
|
15237
|
+
!this.#progressClipRect
|
|
15238
|
+
) {
|
|
15239
|
+
return;
|
|
15240
|
+
}
|
|
15241
|
+
const bounds = this.#svg.getBoundingClientRect();
|
|
15242
|
+
const width = Math.max(1, Math.round(bounds.width));
|
|
15243
|
+
const height = Math.max(1, Math.round(bounds.height));
|
|
15244
|
+
this.#svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
15245
|
+
this.#syncProgress();
|
|
15246
|
+
|
|
15247
|
+
if (this.#levels.length === 0) {
|
|
15248
|
+
this.#waveformPath.setAttribute("d", "");
|
|
15249
|
+
this.#progressPath.setAttribute("d", "");
|
|
15250
|
+
return;
|
|
15251
|
+
}
|
|
15252
|
+
|
|
15253
|
+
const barCount = Math.min(
|
|
15254
|
+
this.#levels.length,
|
|
15255
|
+
Math.max(16, Math.floor(width / 3)),
|
|
15256
|
+
);
|
|
15257
|
+
const cellWidth = width / barCount;
|
|
15258
|
+
const configuredStrokeWidth = Number.parseFloat(
|
|
15259
|
+
getComputedStyle(this).getPropertyValue(
|
|
15260
|
+
"--fig-audio-waveform-stroke-width",
|
|
15261
|
+
),
|
|
15262
|
+
);
|
|
15263
|
+
const strokeWidth =
|
|
15264
|
+
Number.isFinite(configuredStrokeWidth) && configuredStrokeWidth > 0
|
|
15265
|
+
? configuredStrokeWidth
|
|
15266
|
+
: 2;
|
|
15267
|
+
const barWidth = Math.max(0.5, Math.min(strokeWidth, cellWidth * 0.75));
|
|
15268
|
+
const halfWidth = barWidth / 2;
|
|
15269
|
+
const centerY = height / 2;
|
|
15270
|
+
const maxBarHeight = Math.max(2, height - 12);
|
|
15271
|
+
const commands = [];
|
|
15272
|
+
|
|
15273
|
+
for (let index = 0; index < barCount; index += 1) {
|
|
15274
|
+
const levelIndex = Math.min(
|
|
15275
|
+
this.#levels.length - 1,
|
|
15276
|
+
Math.floor((index / barCount) * this.#levels.length),
|
|
15277
|
+
);
|
|
15278
|
+
const polarizedLevel = this.#polarizeLevel(this.#levels[levelIndex]);
|
|
15279
|
+
const barHeight = Math.max(barWidth, polarizedLevel * maxBarHeight);
|
|
15280
|
+
const x = (index + 0.5) * cellWidth;
|
|
15281
|
+
const top = centerY - barHeight / 2;
|
|
15282
|
+
const bottom = centerY + barHeight / 2;
|
|
15283
|
+
const left = x - halfWidth;
|
|
15284
|
+
const right = x + halfWidth;
|
|
15285
|
+
commands.push(
|
|
15286
|
+
`M ${x} ${top}` +
|
|
15287
|
+
` Q ${right} ${top} ${right} ${top + halfWidth}` +
|
|
15288
|
+
` V ${bottom - halfWidth}` +
|
|
15289
|
+
` Q ${right} ${bottom} ${x} ${bottom}` +
|
|
15290
|
+
` Q ${left} ${bottom} ${left} ${bottom - halfWidth}` +
|
|
15291
|
+
` V ${top + halfWidth}` +
|
|
15292
|
+
` Q ${left} ${top} ${x} ${top} Z`,
|
|
15293
|
+
);
|
|
15294
|
+
}
|
|
15295
|
+
|
|
15296
|
+
const path = commands.join(" ");
|
|
15297
|
+
this.#waveformPath.setAttribute("d", path);
|
|
15298
|
+
this.#progressPath.setAttribute("d", path);
|
|
15299
|
+
}
|
|
15300
|
+
}
|
|
15301
|
+
figDefineElement("fig-audio-waveform", FigAudioWaveform);
|
|
15302
|
+
|
|
14867
15303
|
/* File Upload Input */
|
|
15304
|
+
const FIG_INPUT_FILE_IMAGE_EXTENSIONS = [
|
|
15305
|
+
".png",
|
|
15306
|
+
".jpg",
|
|
15307
|
+
".jpeg",
|
|
15308
|
+
".gif",
|
|
15309
|
+
".webp",
|
|
15310
|
+
".svg",
|
|
15311
|
+
".avif",
|
|
15312
|
+
];
|
|
15313
|
+
const FIG_INPUT_FILE_AUDIO_EXTENSIONS = [
|
|
15314
|
+
".mp3",
|
|
15315
|
+
".wav",
|
|
15316
|
+
".aac",
|
|
15317
|
+
".m4a",
|
|
15318
|
+
".ogg",
|
|
15319
|
+
".flac",
|
|
15320
|
+
];
|
|
15321
|
+
|
|
15322
|
+
/**
|
|
15323
|
+
* File upload input with file-aware icons, image previews, and audio playback waveforms.
|
|
15324
|
+
* @attr {string} accepts - Native file chooser filter.
|
|
15325
|
+
* @attr {boolean} preview - Preview selected files with a supported image extension.
|
|
15326
|
+
* @attr {string} label - Upload button label. Defaults by known file type.
|
|
15327
|
+
*/
|
|
14868
15328
|
class FigInputFile extends HTMLElement {
|
|
14869
|
-
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url", "filename"];
|
|
15329
|
+
static observedAttributes = ["accepts", "preview", "label", "disabled", "multiple", "variant", "url", "filename"];
|
|
14870
15330
|
|
|
14871
15331
|
#fileInput = null;
|
|
14872
15332
|
#filenameEl = null;
|
|
@@ -14874,6 +15334,14 @@ class FigInputFile extends HTMLElement {
|
|
|
14874
15334
|
#tooltipEl = null;
|
|
14875
15335
|
#uploadBtn = null;
|
|
14876
15336
|
#files = null;
|
|
15337
|
+
#previewImage = null;
|
|
15338
|
+
#audioPreview = null;
|
|
15339
|
+
#audioIcon = null;
|
|
15340
|
+
#audioWaveform = null;
|
|
15341
|
+
#audioPlaying = false;
|
|
15342
|
+
#audioBlobUrl = null;
|
|
15343
|
+
#syncingPreview = false;
|
|
15344
|
+
#suppressAttributeRender = false;
|
|
14877
15345
|
|
|
14878
15346
|
get files() {
|
|
14879
15347
|
return this.#files;
|
|
@@ -14901,6 +15369,180 @@ class FigInputFile extends HTMLElement {
|
|
|
14901
15369
|
return this.#urlFilename;
|
|
14902
15370
|
}
|
|
14903
15371
|
|
|
15372
|
+
get #effectiveAccepts() {
|
|
15373
|
+
return this.getAttribute("accepts") || "";
|
|
15374
|
+
}
|
|
15375
|
+
|
|
15376
|
+
get #previewEnabled() {
|
|
15377
|
+
return (
|
|
15378
|
+
this.hasAttribute("preview") &&
|
|
15379
|
+
this.getAttribute("preview") !== "false"
|
|
15380
|
+
);
|
|
15381
|
+
}
|
|
15382
|
+
|
|
15383
|
+
#hasKnownExtension(filename, extensions) {
|
|
15384
|
+
const lowerName = filename.toLowerCase();
|
|
15385
|
+
return extensions.some((extension) => lowerName.endsWith(extension));
|
|
15386
|
+
}
|
|
15387
|
+
|
|
15388
|
+
#isKnownFile(file, extensions, mimePrefix) {
|
|
15389
|
+
return (
|
|
15390
|
+
this.#hasKnownExtension(file.name, extensions) ||
|
|
15391
|
+
file.type.toLowerCase().startsWith(mimePrefix)
|
|
15392
|
+
);
|
|
15393
|
+
}
|
|
15394
|
+
|
|
15395
|
+
#generatedLabel() {
|
|
15396
|
+
if (this.#files?.[0]) {
|
|
15397
|
+
if (
|
|
15398
|
+
this.#isKnownFile(
|
|
15399
|
+
this.#files[0],
|
|
15400
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS,
|
|
15401
|
+
"audio/",
|
|
15402
|
+
)
|
|
15403
|
+
) {
|
|
15404
|
+
return "Upload audio";
|
|
15405
|
+
}
|
|
15406
|
+
if (
|
|
15407
|
+
this.#isKnownFile(
|
|
15408
|
+
this.#files[0],
|
|
15409
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15410
|
+
"image/",
|
|
15411
|
+
)
|
|
15412
|
+
) {
|
|
15413
|
+
return "Upload image";
|
|
15414
|
+
}
|
|
15415
|
+
}
|
|
15416
|
+
|
|
15417
|
+
if (
|
|
15418
|
+
this.#hasKnownExtension(
|
|
15419
|
+
this.#urlFilename,
|
|
15420
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS,
|
|
15421
|
+
)
|
|
15422
|
+
) {
|
|
15423
|
+
return "Upload audio";
|
|
15424
|
+
}
|
|
15425
|
+
if (
|
|
15426
|
+
this.#hasKnownExtension(
|
|
15427
|
+
this.#urlFilename,
|
|
15428
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15429
|
+
)
|
|
15430
|
+
) {
|
|
15431
|
+
return "Upload image";
|
|
15432
|
+
}
|
|
15433
|
+
|
|
15434
|
+
const acceptedTypes = this.#effectiveAccepts
|
|
15435
|
+
.split(",")
|
|
15436
|
+
.map((value) => value.trim().toLowerCase())
|
|
15437
|
+
.filter(Boolean)
|
|
15438
|
+
.map((value) => {
|
|
15439
|
+
if (
|
|
15440
|
+
value.startsWith("audio/") ||
|
|
15441
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS.includes(value)
|
|
15442
|
+
) {
|
|
15443
|
+
return "audio";
|
|
15444
|
+
}
|
|
15445
|
+
if (
|
|
15446
|
+
value.startsWith("image/") ||
|
|
15447
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS.includes(value)
|
|
15448
|
+
) {
|
|
15449
|
+
return "image";
|
|
15450
|
+
}
|
|
15451
|
+
return "file";
|
|
15452
|
+
});
|
|
15453
|
+
if (
|
|
15454
|
+
acceptedTypes.length > 0 &&
|
|
15455
|
+
acceptedTypes.every((type) => type === "audio")
|
|
15456
|
+
) {
|
|
15457
|
+
return "Upload audio";
|
|
15458
|
+
}
|
|
15459
|
+
if (
|
|
15460
|
+
acceptedTypes.length > 0 &&
|
|
15461
|
+
acceptedTypes.every((type) => type === "image")
|
|
15462
|
+
) {
|
|
15463
|
+
return "Upload image";
|
|
15464
|
+
}
|
|
15465
|
+
return "Upload file";
|
|
15466
|
+
}
|
|
15467
|
+
|
|
15468
|
+
#hasPreviewableImage() {
|
|
15469
|
+
if (this.#files?.length) {
|
|
15470
|
+
return this.#hasKnownExtension(
|
|
15471
|
+
this.#files[0].name,
|
|
15472
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15473
|
+
);
|
|
15474
|
+
}
|
|
15475
|
+
return (
|
|
15476
|
+
!!this.getAttribute("url") &&
|
|
15477
|
+
this.#hasKnownExtension(
|
|
15478
|
+
this.#urlFilename,
|
|
15479
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15480
|
+
)
|
|
15481
|
+
);
|
|
15482
|
+
}
|
|
15483
|
+
|
|
15484
|
+
#hasPreviewableAudio() {
|
|
15485
|
+
if (this.#files?.length) {
|
|
15486
|
+
return this.#isKnownFile(
|
|
15487
|
+
this.#files[0],
|
|
15488
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS,
|
|
15489
|
+
"audio/",
|
|
15490
|
+
);
|
|
15491
|
+
}
|
|
15492
|
+
return (
|
|
15493
|
+
!!this.getAttribute("url") &&
|
|
15494
|
+
this.#hasKnownExtension(
|
|
15495
|
+
this.#urlFilename,
|
|
15496
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS,
|
|
15497
|
+
)
|
|
15498
|
+
);
|
|
15499
|
+
}
|
|
15500
|
+
|
|
15501
|
+
#selectedFileIcon() {
|
|
15502
|
+
if (this.#files && this.#files.length > 0) {
|
|
15503
|
+
const files = Array.from(this.#files);
|
|
15504
|
+
if (
|
|
15505
|
+
files.every((file) =>
|
|
15506
|
+
this.#isKnownFile(
|
|
15507
|
+
file,
|
|
15508
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS,
|
|
15509
|
+
"audio/",
|
|
15510
|
+
),
|
|
15511
|
+
)
|
|
15512
|
+
) {
|
|
15513
|
+
return "sound";
|
|
15514
|
+
}
|
|
15515
|
+
if (
|
|
15516
|
+
files.every((file) =>
|
|
15517
|
+
this.#isKnownFile(
|
|
15518
|
+
file,
|
|
15519
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15520
|
+
"image/",
|
|
15521
|
+
),
|
|
15522
|
+
)
|
|
15523
|
+
) {
|
|
15524
|
+
return "image";
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
if (
|
|
15528
|
+
this.#hasKnownExtension(
|
|
15529
|
+
this.#urlFilename,
|
|
15530
|
+
FIG_INPUT_FILE_AUDIO_EXTENSIONS,
|
|
15531
|
+
)
|
|
15532
|
+
) {
|
|
15533
|
+
return "sound";
|
|
15534
|
+
}
|
|
15535
|
+
if (
|
|
15536
|
+
this.#hasKnownExtension(
|
|
15537
|
+
this.#urlFilename,
|
|
15538
|
+
FIG_INPUT_FILE_IMAGE_EXTENSIONS,
|
|
15539
|
+
)
|
|
15540
|
+
) {
|
|
15541
|
+
return "image";
|
|
15542
|
+
}
|
|
15543
|
+
return "upload";
|
|
15544
|
+
}
|
|
15545
|
+
|
|
14904
15546
|
connectedCallback() {
|
|
14905
15547
|
this.#render();
|
|
14906
15548
|
this.addEventListener("dragover", this.#onDragOver);
|
|
@@ -14911,6 +15553,8 @@ class FigInputFile extends HTMLElement {
|
|
|
14911
15553
|
disconnectedCallback() {
|
|
14912
15554
|
this.#fileInput?.removeEventListener("change", this.#onFileChange);
|
|
14913
15555
|
this.#clearBtn?.removeEventListener("click", this.#onClear);
|
|
15556
|
+
this.#disconnectPreview();
|
|
15557
|
+
this.#disconnectAudioPreview();
|
|
14914
15558
|
this.removeEventListener("dragover", this.#onDragOver);
|
|
14915
15559
|
this.removeEventListener("dragleave", this.#onDragLeave);
|
|
14916
15560
|
this.removeEventListener("drop", this.#onDrop);
|
|
@@ -14918,6 +15562,7 @@ class FigInputFile extends HTMLElement {
|
|
|
14918
15562
|
|
|
14919
15563
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
14920
15564
|
if (oldValue === newValue) return;
|
|
15565
|
+
if (this.#suppressAttributeRender) return;
|
|
14921
15566
|
this.#render();
|
|
14922
15567
|
}
|
|
14923
15568
|
|
|
@@ -14951,6 +15596,7 @@ class FigInputFile extends HTMLElement {
|
|
|
14951
15596
|
};
|
|
14952
15597
|
|
|
14953
15598
|
#onDragOver = (e) => {
|
|
15599
|
+
if (this.#previewImage) return;
|
|
14954
15600
|
if (
|
|
14955
15601
|
this.hasAttribute("disabled") &&
|
|
14956
15602
|
this.getAttribute("disabled") !== "false"
|
|
@@ -14967,6 +15613,7 @@ class FigInputFile extends HTMLElement {
|
|
|
14967
15613
|
};
|
|
14968
15614
|
|
|
14969
15615
|
#onDragLeave = () => {
|
|
15616
|
+
if (this.#previewImage) return;
|
|
14970
15617
|
this.removeAttribute("dragover");
|
|
14971
15618
|
if (this.#uploadBtn && this.#uploadBtn.dataset.prevText !== undefined) {
|
|
14972
15619
|
this.#uploadBtn.textContent = this.#uploadBtn.dataset.prevText;
|
|
@@ -14975,6 +15622,7 @@ class FigInputFile extends HTMLElement {
|
|
|
14975
15622
|
};
|
|
14976
15623
|
|
|
14977
15624
|
#onDrop = (e) => {
|
|
15625
|
+
if (this.#previewImage) return;
|
|
14978
15626
|
e.preventDefault();
|
|
14979
15627
|
this.removeAttribute("dragover");
|
|
14980
15628
|
if (this.#uploadBtn && this.#uploadBtn.dataset.prevText !== undefined) {
|
|
@@ -14987,7 +15635,7 @@ class FigInputFile extends HTMLElement {
|
|
|
14987
15635
|
)
|
|
14988
15636
|
return;
|
|
14989
15637
|
|
|
14990
|
-
const accepts = this
|
|
15638
|
+
const accepts = this.#effectiveAccepts;
|
|
14991
15639
|
let dropped = Array.from(e.dataTransfer.files);
|
|
14992
15640
|
if (accepts) {
|
|
14993
15641
|
const allowed = accepts.split(",").map((s) => s.trim().toLowerCase());
|
|
@@ -15021,24 +15669,210 @@ class FigInputFile extends HTMLElement {
|
|
|
15021
15669
|
this.#emitEvents();
|
|
15022
15670
|
};
|
|
15023
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
|
+
this.#removeExternalFileAttributes();
|
|
15711
|
+
this.#render();
|
|
15712
|
+
if (!this.#syncingPreview) this.#emitEvents({ cleared: true });
|
|
15713
|
+
};
|
|
15714
|
+
|
|
15715
|
+
#disconnectPreview() {
|
|
15716
|
+
if (!this.#previewImage) return;
|
|
15717
|
+
this.#previewImage.removeEventListener("loaded", this.#onPreviewLoaded);
|
|
15718
|
+
this.#previewImage.removeEventListener("change", this.#onPreviewChange);
|
|
15719
|
+
this.#previewImage = null;
|
|
15720
|
+
}
|
|
15721
|
+
|
|
15722
|
+
#disconnectAudioPreview() {
|
|
15723
|
+
this.#audioPreview?.pause();
|
|
15724
|
+
if (this.#audioBlobUrl) {
|
|
15725
|
+
URL.revokeObjectURL(this.#audioBlobUrl);
|
|
15726
|
+
this.#audioBlobUrl = null;
|
|
15727
|
+
}
|
|
15728
|
+
this.#audioPreview = null;
|
|
15729
|
+
this.#audioIcon = null;
|
|
15730
|
+
this.#audioWaveform = null;
|
|
15731
|
+
this.#audioPlaying = false;
|
|
15732
|
+
}
|
|
15733
|
+
|
|
15734
|
+
#setAudioPlaybackUi(playing) {
|
|
15735
|
+
this.#audioPlaying = playing;
|
|
15736
|
+
this.#audioIcon?.setAttribute("name", playing ? "pause" : "play");
|
|
15737
|
+
this.#uploadBtn?.toggleAttribute("selected", playing);
|
|
15738
|
+
this.#uploadBtn?.setAttribute(
|
|
15739
|
+
"aria-label",
|
|
15740
|
+
playing ? "Pause audio" : "Play audio",
|
|
15741
|
+
);
|
|
15742
|
+
}
|
|
15743
|
+
|
|
15744
|
+
#syncAudioWaveformProgress() {
|
|
15745
|
+
if (!this.#audioPreview || !this.#audioWaveform) return;
|
|
15746
|
+
const duration = this.#audioPreview.duration;
|
|
15747
|
+
const currentTime = this.#audioPreview.currentTime;
|
|
15748
|
+
const progress =
|
|
15749
|
+
Number.isFinite(duration) && duration > 0 && Number.isFinite(currentTime)
|
|
15750
|
+
? Math.max(0, Math.min(currentTime / duration, 1))
|
|
15751
|
+
: 0;
|
|
15752
|
+
this.#audioWaveform.setAttribute("progress", String(progress));
|
|
15753
|
+
}
|
|
15754
|
+
|
|
15755
|
+
#onAudioPlaybackToggle = (event) => {
|
|
15756
|
+
event.preventDefault();
|
|
15757
|
+
if (!this.#audioPreview || this.#uploadBtn?.hasAttribute("disabled")) return;
|
|
15758
|
+
if (this.#audioPlaying) {
|
|
15759
|
+
this.#audioPreview.pause();
|
|
15760
|
+
return;
|
|
15761
|
+
}
|
|
15762
|
+
const playback = this.#audioPreview.play();
|
|
15763
|
+
if (playback && typeof playback.catch === "function") {
|
|
15764
|
+
playback.catch(() => this.#setAudioPlaybackUi(false));
|
|
15765
|
+
}
|
|
15766
|
+
};
|
|
15767
|
+
|
|
15768
|
+
#renderAudioPreview() {
|
|
15769
|
+
const audio = document.createElement("audio");
|
|
15770
|
+
audio.hidden = true;
|
|
15771
|
+
audio.preload = "metadata";
|
|
15772
|
+
if (this.#files?.[0]) {
|
|
15773
|
+
this.#audioBlobUrl = URL.createObjectURL(this.#files[0]);
|
|
15774
|
+
audio.src = this.#audioBlobUrl;
|
|
15775
|
+
} else {
|
|
15776
|
+
audio.src = this.getAttribute("url") || "";
|
|
15777
|
+
}
|
|
15778
|
+
|
|
15779
|
+
audio.addEventListener("play", () => this.#setAudioPlaybackUi(true));
|
|
15780
|
+
audio.addEventListener("pause", () => this.#setAudioPlaybackUi(false));
|
|
15781
|
+
audio.addEventListener("ended", () => {
|
|
15782
|
+
this.#setAudioPlaybackUi(false);
|
|
15783
|
+
this.#audioWaveform?.setAttribute("progress", "0");
|
|
15784
|
+
});
|
|
15785
|
+
audio.addEventListener("timeupdate", () =>
|
|
15786
|
+
this.#syncAudioWaveformProgress(),
|
|
15787
|
+
);
|
|
15788
|
+
audio.addEventListener("durationchange", () =>
|
|
15789
|
+
this.#syncAudioWaveformProgress(),
|
|
15790
|
+
);
|
|
15791
|
+
|
|
15792
|
+
this.#audioPreview = audio;
|
|
15793
|
+
this.#audioWaveform = document.createElement("fig-audio-waveform");
|
|
15794
|
+
this.#audioWaveform.className = "fig-input-file-audio-waveform";
|
|
15795
|
+
this.#audioWaveform.setAttribute("url", audio.src);
|
|
15796
|
+
this.#audioWaveform.setAttribute("polarization", "3");
|
|
15797
|
+
this.#audioWaveform.setAttribute("progress", "0");
|
|
15798
|
+
this.#uploadBtn?.append(this.#audioWaveform);
|
|
15799
|
+
this.#uploadBtn?.addEventListener("click", this.#onAudioPlaybackToggle);
|
|
15800
|
+
this.#setAudioPlaybackUi(false);
|
|
15801
|
+
this.append(audio);
|
|
15802
|
+
}
|
|
15803
|
+
|
|
15804
|
+
#renderImagePreview({ accepts, label, disabled, multiple }) {
|
|
15805
|
+
const image = document.createElement("fig-image");
|
|
15806
|
+
image.setAttribute("full", "");
|
|
15807
|
+
image.setAttribute("checkerboard", "");
|
|
15808
|
+
image.setAttribute("upload", "");
|
|
15809
|
+
image.setAttribute("aspect-ratio", "4/3");
|
|
15810
|
+
image.setAttribute("label", label);
|
|
15811
|
+
const url = this.getAttribute("url");
|
|
15812
|
+
if (url) image.setAttribute("src", url);
|
|
15813
|
+
|
|
15814
|
+
this.#previewImage = image;
|
|
15815
|
+
image.addEventListener("loaded", this.#onPreviewLoaded);
|
|
15816
|
+
image.addEventListener("change", this.#onPreviewChange);
|
|
15817
|
+
this.appendChild(image);
|
|
15818
|
+
|
|
15819
|
+
const nestedInput = image.querySelector("fig-input-file[data-generated]");
|
|
15820
|
+
if (!nestedInput) return;
|
|
15821
|
+
nestedInput.addEventListener("input", this.#stopPreviewInputEvent);
|
|
15822
|
+
nestedInput.addEventListener("change", this.#stopPreviewInputEvent);
|
|
15823
|
+
nestedInput.setAttribute("accepts", accepts);
|
|
15824
|
+
if (disabled) nestedInput.setAttribute("disabled", "");
|
|
15825
|
+
if (multiple) nestedInput.setAttribute("multiple", "");
|
|
15826
|
+
|
|
15827
|
+
if (this.#files?.length) {
|
|
15828
|
+
const nativeInput = nestedInput.querySelector('input[type="file"]');
|
|
15829
|
+
if (nativeInput) {
|
|
15830
|
+
this.#syncingPreview = true;
|
|
15831
|
+
nativeInput.files = this.#files;
|
|
15832
|
+
nativeInput.dispatchEvent(new Event("change", { bubbles: true }));
|
|
15833
|
+
this.#syncingPreview = false;
|
|
15834
|
+
}
|
|
15835
|
+
}
|
|
15836
|
+
}
|
|
15837
|
+
|
|
15024
15838
|
#render() {
|
|
15025
|
-
const accepts = this
|
|
15026
|
-
const
|
|
15839
|
+
const accepts = this.#effectiveAccepts;
|
|
15840
|
+
const customLabel = this.getAttribute("label");
|
|
15841
|
+
const label = customLabel?.trim() ? customLabel : this.#generatedLabel();
|
|
15027
15842
|
const disabled =
|
|
15028
15843
|
this.hasAttribute("disabled") &&
|
|
15029
15844
|
this.getAttribute("disabled") !== "false";
|
|
15030
15845
|
const multiple =
|
|
15031
15846
|
this.hasAttribute("multiple") && this.getAttribute("multiple") !== "false";
|
|
15032
|
-
const variant = this.getAttribute("variant") || "
|
|
15847
|
+
const variant = this.getAttribute("variant") || "property";
|
|
15033
15848
|
const hasFile =
|
|
15034
15849
|
(this.#files && this.#files.length > 0) ||
|
|
15035
15850
|
!!this.getAttribute("url") ||
|
|
15036
15851
|
!!this.getAttribute("filename");
|
|
15852
|
+
const showsImagePreview =
|
|
15853
|
+
this.#previewEnabled && this.#hasPreviewableImage();
|
|
15854
|
+
const showsAudioPreview =
|
|
15855
|
+
this.#previewEnabled && this.#hasPreviewableAudio();
|
|
15037
15856
|
|
|
15857
|
+
this.#disconnectPreview();
|
|
15858
|
+
this.#disconnectAudioPreview();
|
|
15038
15859
|
this.replaceChildren();
|
|
15860
|
+
this.#fileInput = null;
|
|
15861
|
+
this.#uploadBtn = null;
|
|
15862
|
+
this.#clearBtn = null;
|
|
15863
|
+
this.#tooltipEl = null;
|
|
15864
|
+
this.toggleAttribute("data-previewing", showsImagePreview);
|
|
15865
|
+
|
|
15866
|
+
if (showsImagePreview) {
|
|
15867
|
+
this.#renderImagePreview({ accepts, label, disabled, multiple });
|
|
15868
|
+
return;
|
|
15869
|
+
}
|
|
15039
15870
|
|
|
15040
15871
|
if (hasFile) {
|
|
15041
|
-
const
|
|
15872
|
+
const filename = this.value;
|
|
15873
|
+
const tooltipText = showsAudioPreview
|
|
15874
|
+
? filename
|
|
15875
|
+
: accepts
|
|
15042
15876
|
? `Accepts ${accepts
|
|
15043
15877
|
.split(",")
|
|
15044
15878
|
.map((s) => s.trim())
|
|
@@ -15047,26 +15881,38 @@ class FigInputFile extends HTMLElement {
|
|
|
15047
15881
|
|
|
15048
15882
|
this.#uploadBtn = document.createElement("fig-button");
|
|
15049
15883
|
this.#uploadBtn.setAttribute("variant", variant);
|
|
15050
|
-
this.#uploadBtn.setAttribute("
|
|
15884
|
+
if (variant === "property") this.#uploadBtn.setAttribute("full", "");
|
|
15051
15885
|
this.#uploadBtn.className = "fig-input-file-filename";
|
|
15052
15886
|
if (disabled) this.#uploadBtn.setAttribute("disabled", "");
|
|
15053
|
-
const
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15887
|
+
const fileIcon = createFigIcon(
|
|
15888
|
+
showsAudioPreview ? "play" : this.#selectedFileIcon(),
|
|
15889
|
+
);
|
|
15890
|
+
if (showsAudioPreview) {
|
|
15891
|
+
this.#audioIcon = fileIcon;
|
|
15892
|
+
fileIcon.setAttribute("slot", "prepend");
|
|
15893
|
+
this.#uploadBtn.append(fileIcon);
|
|
15894
|
+
} else {
|
|
15895
|
+
const truncEl = document.createElement("fig-truncate");
|
|
15896
|
+
truncEl.setAttribute("position", "middle");
|
|
15897
|
+
truncEl.setAttribute("tooltip", "");
|
|
15898
|
+
const dotIdx = filename.lastIndexOf(".");
|
|
15899
|
+
if (dotIdx > 0) truncEl.setAttribute("tail", filename.slice(dotIdx));
|
|
15900
|
+
truncEl.textContent = filename;
|
|
15901
|
+
fileIcon.setAttribute("slot", "prepend");
|
|
15902
|
+
this.#uploadBtn.append(fileIcon, truncEl);
|
|
15903
|
+
}
|
|
15904
|
+
|
|
15905
|
+
if (!showsAudioPreview) {
|
|
15906
|
+
this.#uploadBtn.setAttribute("type", "upload");
|
|
15907
|
+
this.#fileInput = document.createElement("input");
|
|
15908
|
+
this.#fileInput.type = "file";
|
|
15909
|
+
this.#fileInput.title = "";
|
|
15910
|
+
this.#fileInput.disabled = disabled;
|
|
15911
|
+
if (accepts) this.#fileInput.setAttribute("accept", accepts);
|
|
15912
|
+
if (multiple) this.#fileInput.setAttribute("multiple", "");
|
|
15913
|
+
this.#fileInput.addEventListener("change", this.#onFileChange);
|
|
15914
|
+
this.#uploadBtn.appendChild(this.#fileInput);
|
|
15915
|
+
}
|
|
15070
15916
|
|
|
15071
15917
|
if (tooltipText) {
|
|
15072
15918
|
this.#tooltipEl = document.createElement("fig-tooltip");
|
|
@@ -15089,6 +15935,7 @@ class FigInputFile extends HTMLElement {
|
|
|
15089
15935
|
this.#clearBtn.addEventListener("click", this.#onClear);
|
|
15090
15936
|
clearTooltip.appendChild(this.#clearBtn);
|
|
15091
15937
|
this.appendChild(clearTooltip);
|
|
15938
|
+
if (showsAudioPreview) this.#renderAudioPreview();
|
|
15092
15939
|
} else {
|
|
15093
15940
|
const tooltipText = accepts
|
|
15094
15941
|
? `Accepts ${accepts
|
|
@@ -15104,8 +15951,12 @@ class FigInputFile extends HTMLElement {
|
|
|
15104
15951
|
|
|
15105
15952
|
this.#uploadBtn = document.createElement("fig-button");
|
|
15106
15953
|
this.#uploadBtn.setAttribute("variant", variant);
|
|
15954
|
+
if (variant === "property") this.#uploadBtn.setAttribute("full", "");
|
|
15107
15955
|
this.#uploadBtn.setAttribute("type", "upload");
|
|
15108
15956
|
this.#uploadBtn.textContent = label;
|
|
15957
|
+
const uploadIcon = createFigIcon("upload");
|
|
15958
|
+
uploadIcon.setAttribute("slot", "prepend");
|
|
15959
|
+
this.#uploadBtn.prepend(uploadIcon);
|
|
15109
15960
|
if (disabled) this.#uploadBtn.setAttribute("disabled", "");
|
|
15110
15961
|
|
|
15111
15962
|
this.#fileInput = document.createElement("input");
|
|
@@ -19458,6 +20309,12 @@ const FIG_ICON_TOKENS = {
|
|
|
19458
20309
|
search: "--icon-24-search",
|
|
19459
20310
|
download: "--icon-24-download",
|
|
19460
20311
|
upload: "--icon-24-upload",
|
|
20312
|
+
image: "--icon-24-image",
|
|
20313
|
+
sound: { medium: "--icon-24-sound", small: "--icon-16-sound" },
|
|
20314
|
+
"sound-off": {
|
|
20315
|
+
medium: "--icon-24-sound-off",
|
|
20316
|
+
small: "--icon-16-sound-off",
|
|
20317
|
+
},
|
|
19461
20318
|
sun: "--icon-24-sun",
|
|
19462
20319
|
moon: "--icon-24-moon",
|
|
19463
20320
|
edit: { medium: "--icon-24-edit", small: "--icon-16-edit" },
|