@rogieking/figui3 6.32.2 → 6.33.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/fig.js CHANGED
@@ -12668,11 +12668,42 @@ class FigSwatch extends HTMLElement {
12668
12668
  figDefineElement("fig-swatch", FigSwatch);
12669
12669
 
12670
12670
  /* Media */
12671
+ const FIG_IMAGE_FORWARDED_EVENTS = ["load", "error"];
12672
+ const FIG_VIDEO_FORWARDED_EVENTS = [
12673
+ "loadstart",
12674
+ "progress",
12675
+ "suspend",
12676
+ "abort",
12677
+ "error",
12678
+ "emptied",
12679
+ "stalled",
12680
+ "loadedmetadata",
12681
+ "loadeddata",
12682
+ "canplay",
12683
+ "canplaythrough",
12684
+ "playing",
12685
+ "waiting",
12686
+ "seeking",
12687
+ "seeked",
12688
+ "durationchange",
12689
+ "timeupdate",
12690
+ "ratechange",
12691
+ "resize",
12692
+ "volumechange",
12693
+ ];
12694
+ const FIG_MEDIA_FORWARDED_EVENTS = [
12695
+ ...new Set([
12696
+ ...FIG_IMAGE_FORWARDED_EVENTS,
12697
+ ...FIG_VIDEO_FORWARDED_EVENTS,
12698
+ ]),
12699
+ ];
12700
+
12671
12701
  /**
12672
12702
  * @attr {string} src - Media source URL
12673
12703
  * @attr {string} type - "image" (default) or "video" (for fig-media)
12674
12704
  * @attr {string} alt - Alt text for the generated image (default "")
12675
12705
  * @attr {boolean} upload - Show upload overlay (generates fig-input-file)
12706
+ * @attr {boolean} loading-indicator - Set to `false` to disable the generated image loading spinner
12676
12707
  * @attr {string} label - Upload button label (default "Upload")
12677
12708
  * @attr {string} size - small | medium | large | auto (token-sized square)
12678
12709
  * @attr {string} aspect-ratio - CSS aspect-ratio value (default `4/3`)
@@ -12686,6 +12717,14 @@ figDefineElement("fig-swatch", FigSwatch);
12686
12717
  * @attr {string} poster - Video poster image URL
12687
12718
  * @attr {string} aria-label - Accessible label forwarded to generated video
12688
12719
  * @attr {string} aria-labelledby - Accessible label reference forwarded to generated video
12720
+ * @fires load - Image loaded
12721
+ * @fires error - Image or video failed to load
12722
+ * @fires loadstart - Video loading started
12723
+ * @fires loadedmetadata - Video metadata loaded
12724
+ * @fires loadeddata - Video frame data loaded
12725
+ * @fires canplay - Video can begin playback
12726
+ * @fires canplaythrough - Video can likely play through
12727
+ * @fires waiting - Video playback is waiting for data
12689
12728
  *
12690
12729
  * When `controls` are shown and no video `src` is loaded, fig-media sets
12691
12730
  * `disabled` on the associated `fig-media-controls` until media is available.
@@ -12707,6 +12746,11 @@ class FigMedia extends HTMLElement {
12707
12746
  #boundHandleMediaPlay = this.#handleMediaPlay.bind(this);
12708
12747
  #boundHandleMediaPause = this.#handleMediaPause.bind(this);
12709
12748
  #boundHandleMediaEnded = this.#handleMediaEnded.bind(this);
12749
+ #boundForwardMediaEvent = this.#forwardMediaEvent.bind(this);
12750
+ #loadingSpinner = null;
12751
+ #loadingTimer = null;
12752
+ #loadingSource = null;
12753
+ #ariaBusyBeforeLoading = undefined;
12710
12754
  #controlsEl = null;
12711
12755
  #controlsWiredFor = null;
12712
12756
  #controlsWiredControls = null;
@@ -12721,6 +12765,7 @@ class FigMedia extends HTMLElement {
12721
12765
  "type",
12722
12766
  "alt",
12723
12767
  "upload",
12768
+ "loading-indicator",
12724
12769
  "label",
12725
12770
  "aspect-ratio",
12726
12771
  "fit",
@@ -12812,6 +12857,7 @@ class FigMedia extends HTMLElement {
12812
12857
  this.#ensureMediaElement();
12813
12858
  this.#addMediaElementListeners();
12814
12859
  this.#syncGeneratedMediaElement();
12860
+ this.#syncImageLoadingState();
12815
12861
  this.#syncMediaAccessibility();
12816
12862
  this.#syncCaption();
12817
12863
 
@@ -12826,6 +12872,7 @@ class FigMedia extends HTMLElement {
12826
12872
 
12827
12873
  disconnectedCallback() {
12828
12874
  this.#fileInput?.removeEventListener("change", this.#boundHandleFileInput);
12875
+ this.#finishImageLoading();
12829
12876
  this.#removeMediaElementListeners();
12830
12877
  this.#removeControls();
12831
12878
  if (this.#blobUrl) {
@@ -12837,6 +12884,9 @@ class FigMedia extends HTMLElement {
12837
12884
 
12838
12885
  #removeMediaElementListeners() {
12839
12886
  if (!this.#mediaEl) return;
12887
+ FIG_MEDIA_FORWARDED_EVENTS.forEach((type) => {
12888
+ this.#mediaEl.removeEventListener(type, this.#boundForwardMediaEvent);
12889
+ });
12840
12890
  if (this.#mediaEl.tagName === "VIDEO") {
12841
12891
  this.#mediaEl.removeEventListener("play", this.#boundHandleMediaPlay);
12842
12892
  this.#mediaEl.removeEventListener("pause", this.#boundHandleMediaPause);
@@ -12846,12 +12896,86 @@ class FigMedia extends HTMLElement {
12846
12896
 
12847
12897
  #addMediaElementListeners() {
12848
12898
  this.#removeMediaElementListeners();
12849
- if (this.#mediaEl?.tagName !== "VIDEO") return;
12899
+ if (!this.#mediaEl) return;
12900
+ const forwardedEvents =
12901
+ this.#mediaEl.tagName === "VIDEO"
12902
+ ? FIG_VIDEO_FORWARDED_EVENTS
12903
+ : FIG_IMAGE_FORWARDED_EVENTS;
12904
+ forwardedEvents.forEach((type) => {
12905
+ this.#mediaEl.addEventListener(type, this.#boundForwardMediaEvent);
12906
+ });
12907
+ if (this.#mediaEl.tagName !== "VIDEO") return;
12850
12908
  this.#mediaEl.addEventListener("play", this.#boundHandleMediaPlay);
12851
12909
  this.#mediaEl.addEventListener("pause", this.#boundHandleMediaPause);
12852
12910
  this.#mediaEl.addEventListener("ended", this.#boundHandleMediaEnded);
12853
12911
  }
12854
12912
 
12913
+ #usesDefaultImageLoadingIndicator() {
12914
+ return (
12915
+ this.mediaKind === "image" &&
12916
+ this.getAttribute("loading-indicator") !== "false" &&
12917
+ this.#previewEl?.hasAttribute("data-generated") &&
12918
+ this.#mediaEl?.hasAttribute("data-generated")
12919
+ );
12920
+ }
12921
+
12922
+ #syncImageLoadingState() {
12923
+ const src = this.#currentMediaSrc();
12924
+ if (!src || !this.#usesDefaultImageLoadingIndicator()) {
12925
+ this.#finishImageLoading();
12926
+ return;
12927
+ }
12928
+ if (
12929
+ this.#mediaEl?.tagName === "IMG" &&
12930
+ this.#mediaEl.complete &&
12931
+ this.#mediaEl.currentSrc
12932
+ ) {
12933
+ this.#finishImageLoading();
12934
+ return;
12935
+ }
12936
+ if (this.#loadingSource === src) return;
12937
+
12938
+ this.#finishImageLoading();
12939
+ this.#loadingSource = src;
12940
+ this.#ariaBusyBeforeLoading = this.getAttribute("aria-busy");
12941
+ this.setAttribute("aria-busy", "true");
12942
+ this.#loadingTimer = setTimeout(() => {
12943
+ this.#loadingTimer = null;
12944
+ if (
12945
+ this.#loadingSource !== src ||
12946
+ !this.isConnected ||
12947
+ !this.#usesDefaultImageLoadingIndicator()
12948
+ ) {
12949
+ return;
12950
+ }
12951
+ const spinner = document.createElement("fig-spinner");
12952
+ spinner.setAttribute("data-generated", "");
12953
+ spinner.setAttribute("data-loading-indicator", "");
12954
+ spinner.setAttribute("slot", "overlay");
12955
+ spinner.setAttribute("aria-hidden", "true");
12956
+ this.append(spinner);
12957
+ this.#loadingSpinner = spinner;
12958
+ }, 150);
12959
+ }
12960
+
12961
+ #finishImageLoading() {
12962
+ if (this.#loadingTimer !== null) {
12963
+ clearTimeout(this.#loadingTimer);
12964
+ this.#loadingTimer = null;
12965
+ }
12966
+ this.#loadingSpinner?.remove();
12967
+ this.#loadingSpinner = null;
12968
+ this.#loadingSource = null;
12969
+ if (this.#ariaBusyBeforeLoading !== undefined) {
12970
+ if (this.#ariaBusyBeforeLoading === null) {
12971
+ this.removeAttribute("aria-busy");
12972
+ } else {
12973
+ this.setAttribute("aria-busy", this.#ariaBusyBeforeLoading);
12974
+ }
12975
+ this.#ariaBusyBeforeLoading = undefined;
12976
+ }
12977
+ }
12978
+
12855
12979
  #ensurePreviewElement() {
12856
12980
  if (this.#previewEl?.isConnected) return;
12857
12981
  const existing = this.querySelector(":scope > fig-preview");
@@ -12884,6 +13008,7 @@ class FigMedia extends HTMLElement {
12884
13008
  if (this.#previewEl && userEl.parentElement !== this.#previewEl) {
12885
13009
  this.#previewEl.append(userEl);
12886
13010
  }
13011
+ this.#addMediaElementListeners();
12887
13012
  this.#syncMediaAccessibility();
12888
13013
  return;
12889
13014
  }
@@ -12907,7 +13032,6 @@ class FigMedia extends HTMLElement {
12907
13032
  video.preload = "auto";
12908
13033
  this.#previewEl.append(video);
12909
13034
  this.#mediaEl = video;
12910
- this.#addMediaElementListeners();
12911
13035
  const seekToFirstFrame = () => {
12912
13036
  if (this.#mediaEl?.autoplay) return;
12913
13037
  try {
@@ -12925,6 +13049,7 @@ class FigMedia extends HTMLElement {
12925
13049
  this.#previewEl.append(img);
12926
13050
  this.#mediaEl = img;
12927
13051
  }
13052
+ this.#addMediaElementListeners();
12928
13053
  }
12929
13054
 
12930
13055
  #syncMediaAccessibility() {
@@ -13012,6 +13137,7 @@ class FigMedia extends HTMLElement {
13012
13137
  }
13013
13138
  if (this.#mediaEl.tagName === "IMG") {
13014
13139
  this.#syncMediaAccessibility();
13140
+ this.#syncImageLoadingState();
13015
13141
  return;
13016
13142
  }
13017
13143
  const poster = this.getAttribute("poster");
@@ -13029,6 +13155,7 @@ class FigMedia extends HTMLElement {
13029
13155
  this.#syncMediaAccessibility();
13030
13156
  this.#syncControlsVisibility();
13031
13157
  this.#syncControlsDisabled();
13158
+ this.#syncImageLoadingState();
13032
13159
  }
13033
13160
 
13034
13161
  get mediaEl() {
@@ -13321,6 +13448,37 @@ class FigMedia extends HTMLElement {
13321
13448
  );
13322
13449
  }
13323
13450
 
13451
+ #forwardMediaEvent(event) {
13452
+ const media = this.#mediaEl;
13453
+ if (!media || event.currentTarget !== media) return;
13454
+ if (
13455
+ media.tagName === "IMG" &&
13456
+ (event.type === "load" || event.type === "error")
13457
+ ) {
13458
+ this.#finishImageLoading();
13459
+ }
13460
+ const detail = {
13461
+ src: this.#currentMediaSrc(),
13462
+ media,
13463
+ originalEvent: event,
13464
+ };
13465
+ if (media.tagName === "VIDEO") {
13466
+ detail.currentTime = media.currentTime;
13467
+ detail.duration = media.duration;
13468
+ detail.readyState = media.readyState;
13469
+ detail.networkState = media.networkState;
13470
+ detail.error = media.error;
13471
+ }
13472
+ this.dispatchEvent(
13473
+ new CustomEvent(event.type, {
13474
+ bubbles: true,
13475
+ cancelable: false,
13476
+ composed: true,
13477
+ detail,
13478
+ }),
13479
+ );
13480
+ }
13481
+
13324
13482
  #handleMediaPlay() {
13325
13483
  this.#emitPlaybackEvent("play");
13326
13484
  }
@@ -13390,6 +13548,10 @@ class FigMedia extends HTMLElement {
13390
13548
  }
13391
13549
  }
13392
13550
 
13551
+ if (name === "loading-indicator") {
13552
+ this.#syncImageLoadingState();
13553
+ }
13554
+
13393
13555
  if (name === "aspect-ratio") {
13394
13556
  if (newValue) {
13395
13557
  this.style.setProperty("--fig-media-aspect-ratio", newValue);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.32.2",
3
+ "version": "6.33.0",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "MIT",