@rogieking/figui3 8.3.0 → 8.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/fig-lab.js CHANGED
@@ -18,6 +18,51 @@ function figLabBooleanAttribute(element, name) {
18
18
  return element.hasAttribute(name) && element.getAttribute(name) !== "false";
19
19
  }
20
20
 
21
+ const FIG_LAB_SVG_NAMESPACE = "http://www.w3.org/2000/svg";
22
+
23
+ function figLabAppendChildren(parent, children) {
24
+ const append = (child) => {
25
+ if (child === null || child === undefined || child === false) return;
26
+ if (Array.isArray(child)) {
27
+ child.forEach(append);
28
+ return;
29
+ }
30
+ parent.append(child instanceof Node ? child : String(child));
31
+ };
32
+ append(children);
33
+ return parent;
34
+ }
35
+
36
+ function figLabSetAttributes(element, attributes = {}) {
37
+ for (const [name, value] of Object.entries(attributes)) {
38
+ if (value === null || value === undefined || value === false) continue;
39
+ if (name === "className") {
40
+ if (element.namespaceURI === FIG_LAB_SVG_NAMESPACE) {
41
+ element.setAttribute("class", String(value));
42
+ } else {
43
+ element.className = String(value);
44
+ }
45
+ } else if (value === true) {
46
+ element.setAttribute(name, "");
47
+ } else {
48
+ element.setAttribute(name, String(value));
49
+ }
50
+ }
51
+ return element;
52
+ }
53
+
54
+ function figLabCreateElement(tagName, attributes, children) {
55
+ const element = document.createElement(tagName);
56
+ figLabSetAttributes(element, attributes);
57
+ return figLabAppendChildren(element, children);
58
+ }
59
+
60
+ function figLabCreateSvgElement(tagName, attributes, children) {
61
+ const element = document.createElementNS(FIG_LAB_SVG_NAMESPACE, tagName);
62
+ figLabSetAttributes(element, attributes);
63
+ return figLabAppendChildren(element, children);
64
+ }
65
+
21
66
  function figLabSyncDisabledControls(host, controls) {
22
67
  const disabled = figLabBooleanAttribute(host, "disabled");
23
68
  for (const control of controls) {
@@ -5058,7 +5103,7 @@ class FigCanvasControl extends HTMLElement {
5058
5103
 
5059
5104
  #render() {
5060
5105
  this.#cancelActiveGesture();
5061
- this.innerHTML = "";
5106
+ this.replaceChildren();
5062
5107
  this.#pointHandle = null;
5063
5108
  this.#secondHandle = null;
5064
5109
  this.#angleHandle = null;
@@ -6431,19 +6476,11 @@ class PropskitOscillator extends HTMLElement {
6431
6476
  return Math.round(value * scale) / scale;
6432
6477
  }
6433
6478
 
6434
- static #escapeAttribute(value) {
6435
- return String(value)
6436
- .replace(/&/g, "&")
6437
- .replace(/"/g, """)
6438
- .replace(/</g, "&lt;")
6439
- .replace(/>/g, "&gt;");
6440
- }
6441
-
6442
6479
  static #labelForType(type) {
6443
6480
  return PropskitOscillator.TYPES.find((item) => item.value === type)?.name || "Wave";
6444
6481
  }
6445
6482
 
6446
- static waveIcon(type, size = 24) {
6483
+ static #waveIconPath(type, size = 24) {
6447
6484
  const samples = 32;
6448
6485
  const pad = 5;
6449
6486
  const draw = size - pad * 2;
@@ -6455,6 +6492,30 @@ class PropskitOscillator extends HTMLElement {
6455
6492
  const y = pad + (1 - (value + 1) / 2) * draw;
6456
6493
  d += `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`;
6457
6494
  }
6495
+ return d;
6496
+ }
6497
+
6498
+ static #createWaveIcon(type, size = 24) {
6499
+ return figLabCreateSvgElement(
6500
+ "svg",
6501
+ {
6502
+ width: size,
6503
+ height: size,
6504
+ viewBox: `0 0 ${size} ${size}`,
6505
+ fill: "none",
6506
+ },
6507
+ figLabCreateSvgElement("path", {
6508
+ d: PropskitOscillator.#waveIconPath(type, size),
6509
+ stroke: "currentColor",
6510
+ "stroke-width": "1",
6511
+ "stroke-linecap": "round",
6512
+ "stroke-linejoin": "round",
6513
+ }),
6514
+ );
6515
+ }
6516
+
6517
+ static waveIcon(type, size = 24) {
6518
+ const d = PropskitOscillator.#waveIconPath(type, size);
6458
6519
  return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
6459
6520
  }
6460
6521
 
@@ -6478,7 +6539,7 @@ class PropskitOscillator extends HTMLElement {
6478
6539
  #render() {
6479
6540
  this.#cancelDrag();
6480
6541
  this.#stopPlayhead();
6481
- this.innerHTML = this.#getInnerHTML();
6542
+ this.replaceChildren(...this.#createContent());
6482
6543
  this.#cacheRefs();
6483
6544
  this.#syncViewportSize();
6484
6545
  this.#updateWaveform();
@@ -6487,79 +6548,201 @@ class PropskitOscillator extends HTMLElement {
6487
6548
  figLabConnectPropskitResetMenu(this);
6488
6549
  }
6489
6550
 
6490
- #getInnerHTML() {
6551
+ #createOscillatorHandle(type, label) {
6552
+ return figLabCreateSvgElement(
6553
+ "foreignObject",
6554
+ {
6555
+ className: `propskit-oscillator-handle propskit-oscillator-${type}-handle`,
6556
+ "data-handle": type,
6557
+ width: "20",
6558
+ height: "20",
6559
+ },
6560
+ figLabCreateElement(
6561
+ "div",
6562
+ { className: "propskit-oscillator-handle-inner" },
6563
+ figLabCreateElement(
6564
+ "fig-tooltip",
6565
+ { text: label },
6566
+ figLabCreateElement("fig-handle", {
6567
+ size: "small",
6568
+ "aria-label": `Oscillator ${type} handle`,
6569
+ }),
6570
+ ),
6571
+ ),
6572
+ );
6573
+ }
6574
+
6575
+ #createContent() {
6491
6576
  const interactive = this.#isEditEnabled() && !this.#isDisabled();
6492
- const disabled = interactive ? "" : " disabled";
6493
-
6494
- return `<div class="propskit-oscillator-svg-container">
6495
- <svg viewBox="0 0 ${this.#drawWidth} ${this.#drawHeight}" class="propskit-oscillator-svg">
6496
- <rect class="propskit-oscillator-bounds" x="0" y="0" width="${this.#drawWidth}" height="${this.#drawHeight}"></rect>
6497
- <line class="propskit-oscillator-baseline"></line>
6498
- <path class="propskit-oscillator-path"></path>
6499
- <circle class="propskit-oscillator-playhead"></circle>
6500
- ${interactive ? `<foreignObject class="propskit-oscillator-handle propskit-oscillator-amplitude-handle" data-handle="amplitude" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Amplitude"><fig-handle size="small" aria-label="Oscillator amplitude handle"></fig-handle></fig-tooltip></div></foreignObject>
6501
- <foreignObject class="propskit-oscillator-handle propskit-oscillator-frequency-handle" data-handle="frequency" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Frequency"><fig-handle size="small" aria-label="Oscillator frequency handle"></fig-handle></fig-tooltip></div></foreignObject>` : ""}
6502
- </svg>
6503
- </div>
6504
- ${this.#isEditEnabled() ? this.#getWaveControlsHTML(disabled) : ""}`;
6505
- }
6506
-
6507
- #getWaveControlsHTML(disabled) {
6508
- return `<div class="propskit-oscillator-waves">
6509
- ${this.#waves.map((wave, index) => this.#getWaveRowHTML(wave, index, disabled)).join("")}
6510
- </div>`;
6511
- }
6512
-
6513
- #getWaveRowHTML(wave, index, disabled) {
6514
- const removeDisabled = disabled || this.#waves.length <= 1 ? " disabled" : "";
6515
- const active = index === this.#activeWaveIndex ? " data-active" : "";
6577
+ const svgChildren = [
6578
+ figLabCreateSvgElement("rect", {
6579
+ className: "propskit-oscillator-bounds",
6580
+ x: "0",
6581
+ y: "0",
6582
+ width: this.#drawWidth,
6583
+ height: this.#drawHeight,
6584
+ }),
6585
+ figLabCreateSvgElement("line", {
6586
+ className: "propskit-oscillator-baseline",
6587
+ }),
6588
+ figLabCreateSvgElement("path", {
6589
+ className: "propskit-oscillator-path",
6590
+ }),
6591
+ figLabCreateSvgElement("circle", {
6592
+ className: "propskit-oscillator-playhead",
6593
+ }),
6594
+ ];
6595
+ if (interactive) {
6596
+ svgChildren.push(
6597
+ this.#createOscillatorHandle("amplitude", "Amplitude"),
6598
+ this.#createOscillatorHandle("frequency", "Frequency"),
6599
+ );
6600
+ }
6601
+ const svg = figLabCreateSvgElement(
6602
+ "svg",
6603
+ {
6604
+ viewBox: `0 0 ${this.#drawWidth} ${this.#drawHeight}`,
6605
+ className: "propskit-oscillator-svg",
6606
+ },
6607
+ svgChildren,
6608
+ );
6609
+ const content = [
6610
+ figLabCreateElement(
6611
+ "div",
6612
+ { className: "propskit-oscillator-svg-container" },
6613
+ svg,
6614
+ ),
6615
+ ];
6616
+ if (this.#isEditEnabled()) {
6617
+ content.push(
6618
+ figLabCreateElement(
6619
+ "div",
6620
+ { className: "propskit-oscillator-waves" },
6621
+ this.#waves.map((wave, index) =>
6622
+ this.#createWaveRow(wave, index, !interactive),
6623
+ ),
6624
+ ),
6625
+ );
6626
+ }
6627
+ return content;
6628
+ }
6629
+
6630
+ #createWaveRow(wave, index, disabled) {
6631
+ const removeDisabled = disabled || this.#waves.length <= 1;
6632
+ const active = index === this.#activeWaveIndex;
6516
6633
  const label = PropskitOscillator.#labelForType(wave.type);
6517
- const open = this.#expandedWaveIndices.has(index) ? ' open="true"' : ' open="false"';
6518
- return `<fig-group class="propskit-oscillator-wave" collapsible borderless compact="true"${open} data-wave-index="${index}">
6519
- <fig-header borderless>
6520
- <h3>${label}</h3>
6521
- <fig-tooltip text="Remove form">
6522
- <fig-button class="propskit-oscillator-remove-button" variant="ghost" icon data-wave-index="${index}" aria-label="Remove form"${removeDisabled}><fig-icon name="minus"></fig-icon></fig-button>
6523
- </fig-tooltip>
6524
- ${this.#getWaveTypeMenuHTML(disabled, index)}
6525
- </fig-header>
6526
- <div class="propskit-oscillator-fields" data-wave-index="${index}"${active}>
6527
- ${this.#getNumberFieldHTML(index, "frequency", "Frequency", 0.1, 16, 0.1, "")}
6528
- ${this.#getNumberFieldHTML(index, "amplitude", "Amplitude", -4, 4, 0.1, "")}
6529
- ${this.#getNumberFieldHTML(index, "phase", "Phase", -360, 360, 1, "°")}
6530
- ${this.#getNumberFieldHTML(index, "offset", "Offset", -4, 4, 0.1, "")}
6531
- </div>
6532
- </fig-group>`;
6533
- }
6534
-
6535
- #getWaveTypeMenuHTML(disabled, index) {
6536
- const disabledAttr = disabled ? " disabled" : "";
6537
- const items = PropskitOscillator.TYPES.map((type) => {
6538
- return `<fig-menu-item value="${type.value}">
6539
- ${PropskitOscillator.waveIcon(type.value, 24)}
6540
- <span>${type.name}</span>
6541
- </fig-menu-item>`;
6542
- }).join("");
6543
- const indexAttr = ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
6544
- return `<fig-menu class="propskit-oscillator-add-type" position="bottom right"${indexAttr}${disabledAttr}>
6545
- <fig-tooltip text="Add form">
6546
- <fig-button class="propskit-oscillator-add-type-button" variant="ghost" icon fig-menu-trigger aria-label="Add form"${disabledAttr}><fig-icon name="plus"></fig-icon></fig-button>
6547
- </fig-tooltip>
6548
- ${items}
6549
- </fig-menu>`;
6550
- }
6551
-
6552
- #getNumberFieldHTML(index, name, label, min, max, step, units) {
6553
- const disabled = this.#isDisabled() ? " disabled" : "";
6554
- const typeAttrs =
6555
- name === "amplitude" || name === "offset"
6556
- ? ' type="delta" default="0"'
6557
- : "";
6558
- const unitsAttr = units
6559
- ? ` units="${PropskitOscillator.#escapeAttribute(units)}"`
6560
- : "";
6634
+ const removeButton = figLabCreateElement(
6635
+ "fig-tooltip",
6636
+ { text: "Remove form" },
6637
+ figLabCreateElement(
6638
+ "fig-button",
6639
+ {
6640
+ className: "propskit-oscillator-remove-button",
6641
+ variant: "ghost",
6642
+ icon: true,
6643
+ "data-wave-index": index,
6644
+ "aria-label": "Remove form",
6645
+ disabled: removeDisabled,
6646
+ },
6647
+ figLabCreateElement("fig-icon", { name: "minus" }),
6648
+ ),
6649
+ );
6650
+ const header = figLabCreateElement(
6651
+ "fig-header",
6652
+ { borderless: true },
6653
+ [
6654
+ figLabCreateElement("h3", {}, label),
6655
+ removeButton,
6656
+ this.#createWaveTypeMenu(disabled, index),
6657
+ ],
6658
+ );
6659
+ const fields = figLabCreateElement(
6660
+ "div",
6661
+ {
6662
+ className: "propskit-oscillator-fields",
6663
+ "data-wave-index": index,
6664
+ "data-active": active,
6665
+ },
6666
+ [
6667
+ this.#createNumberField(index, "frequency", "Frequency", 0.1, 16, 0.1, ""),
6668
+ this.#createNumberField(index, "amplitude", "Amplitude", -4, 4, 0.1, ""),
6669
+ this.#createNumberField(index, "phase", "Phase", -360, 360, 1, "°"),
6670
+ this.#createNumberField(index, "offset", "Offset", -4, 4, 0.1, ""),
6671
+ ],
6672
+ );
6673
+ return figLabCreateElement(
6674
+ "fig-group",
6675
+ {
6676
+ className: "propskit-oscillator-wave",
6677
+ collapsible: true,
6678
+ borderless: true,
6679
+ compact: "true",
6680
+ open: this.#expandedWaveIndices.has(index) ? "true" : "false",
6681
+ "data-wave-index": index,
6682
+ },
6683
+ [header, fields],
6684
+ );
6685
+ }
6686
+
6687
+ #createWaveTypeMenu(disabled, index) {
6688
+ const trigger = figLabCreateElement(
6689
+ "fig-tooltip",
6690
+ { text: "Add form" },
6691
+ figLabCreateElement(
6692
+ "fig-button",
6693
+ {
6694
+ className: "propskit-oscillator-add-type-button",
6695
+ variant: "ghost",
6696
+ icon: true,
6697
+ "fig-menu-trigger": true,
6698
+ "aria-label": "Add form",
6699
+ disabled,
6700
+ },
6701
+ figLabCreateElement("fig-icon", { name: "plus" }),
6702
+ ),
6703
+ );
6704
+ const items = PropskitOscillator.TYPES.map((type) =>
6705
+ figLabCreateElement(
6706
+ "fig-menu-item",
6707
+ { value: type.value },
6708
+ [
6709
+ PropskitOscillator.#createWaveIcon(type.value, 24),
6710
+ figLabCreateElement("span", {}, type.name),
6711
+ ],
6712
+ ),
6713
+ );
6714
+ return figLabCreateElement(
6715
+ "fig-menu",
6716
+ {
6717
+ className: "propskit-oscillator-add-type",
6718
+ position: "bottom right",
6719
+ "data-wave-index": index,
6720
+ disabled,
6721
+ },
6722
+ [trigger, items],
6723
+ );
6724
+ }
6725
+
6726
+ #createNumberField(index, name, label, min, max, step, units) {
6561
6727
  const wave = this.#waves[index] || PropskitOscillator.#defaultWave();
6562
- return `<propskit-slider class="propskit-oscillator-field" label="${label}" direction="horizontal" name="${name}" data-wave-index="${index}" value="${this.#round(wave[name])}" min="${min}" max="${max}" step="${step}" precision="${this.#precision}" elastic="false"${typeAttrs}${unitsAttr}${disabled}></propskit-slider>`;
6728
+ const isDelta = name === "amplitude" || name === "offset";
6729
+ return figLabCreateElement("propskit-slider", {
6730
+ className: "propskit-oscillator-field",
6731
+ label,
6732
+ direction: "horizontal",
6733
+ name,
6734
+ "data-wave-index": index,
6735
+ value: this.#round(wave[name]),
6736
+ min,
6737
+ max,
6738
+ step,
6739
+ precision: this.#precision,
6740
+ elastic: "false",
6741
+ type: isDelta ? "delta" : null,
6742
+ default: isDelta ? "0" : null,
6743
+ units: units || null,
6744
+ disabled: this.#isDisabled(),
6745
+ });
6563
6746
  }
6564
6747
 
6565
6748
  #cacheRefs() {
@@ -7236,7 +7419,61 @@ class FigInputAngle extends HTMLElement {
7236
7419
  #render() {
7237
7420
  this.#cancelGesture();
7238
7421
  this.#cleanupListeners();
7239
- this.innerHTML = this.#getInnerHTML();
7422
+ const step = this.#getStepForUnit();
7423
+ const disabled = this.#isDisabled();
7424
+ const name =
7425
+ this.getAttribute("aria-label") || this.getAttribute("name") || "Angle";
7426
+ const ariaMin = this.min ?? this.#fromDegrees(0);
7427
+ const ariaMax = this.max ?? this.#fromDegrees(360);
7428
+ const children = [];
7429
+
7430
+ if (this.dial) {
7431
+ children.push(
7432
+ figLabCreateElement(
7433
+ "div",
7434
+ {
7435
+ className: "fig-input-angle-plane",
7436
+ role: "slider",
7437
+ tabindex: disabled ? "-1" : "0",
7438
+ "aria-label": name,
7439
+ "aria-valuemin": ariaMin,
7440
+ "aria-valuemax": ariaMax,
7441
+ "aria-valuenow": this.angle,
7442
+ "aria-valuetext": `${this.angle.toFixed(this.precision)}${this.units}`,
7443
+ "aria-disabled": disabled ? "true" : null,
7444
+ },
7445
+ figLabCreateElement("div", {
7446
+ className: "fig-input-angle-handle",
7447
+ }),
7448
+ ),
7449
+ );
7450
+ }
7451
+
7452
+ if (this.text) {
7453
+ children.push(
7454
+ figLabCreateElement(
7455
+ "fig-input-number",
7456
+ {
7457
+ name: "angle",
7458
+ step,
7459
+ value: this.angle,
7460
+ min: this.min,
7461
+ max: this.max,
7462
+ units: this.units,
7463
+ "aria-label": name,
7464
+ disabled,
7465
+ },
7466
+ this.showRotations
7467
+ ? figLabCreateElement("span", {
7468
+ slot: "append",
7469
+ className: "fig-input-angle-rotations",
7470
+ })
7471
+ : null,
7472
+ ),
7473
+ );
7474
+ }
7475
+
7476
+ this.replaceChildren(...children);
7240
7477
  }
7241
7478
 
7242
7479
  #readBooleanAttribute(name, defaultValue = false) {
@@ -7258,62 +7495,11 @@ class FigInputAngle extends HTMLElement {
7258
7495
  return false;
7259
7496
  }
7260
7497
 
7261
- #getInnerHTML() {
7262
- const step = this.#getStepForUnit();
7263
- const minAttr = this.min !== null ? `min="${this.min}"` : "";
7264
- const maxAttr = this.max !== null ? `max="${this.max}"` : "";
7265
- const disabled = this.#isDisabled();
7266
- const name =
7267
- this.getAttribute("aria-label") || this.getAttribute("name") || "Angle";
7268
- const ariaMin = this.min ?? this.#fromDegrees(0);
7269
- const ariaMax = this.max ?? this.#fromDegrees(360);
7270
- return `
7271
- ${
7272
- this.dial
7273
- ? `<div class="fig-input-angle-plane"
7274
- role="slider"
7275
- tabindex="${disabled ? -1 : 0}"
7276
- aria-label="${FigInputAngle.#escapeAttribute(name)}"
7277
- aria-valuemin="${ariaMin}"
7278
- aria-valuemax="${ariaMax}"
7279
- aria-valuenow="${this.angle}"
7280
- aria-valuetext="${FigInputAngle.#escapeAttribute(`${this.angle.toFixed(this.precision)}${this.units}`)}"
7281
- ${disabled ? 'aria-disabled="true"' : ""}>
7282
- <div class="fig-input-angle-handle"></div>
7283
- </div>`
7284
- : ""
7285
- }
7286
- ${
7287
- this.text
7288
- ? `<fig-input-number
7289
- name="angle"
7290
- step="${step}"
7291
- value="${this.angle}"
7292
- ${minAttr}
7293
- ${maxAttr}
7294
- units="${this.units}"
7295
- aria-label="${FigInputAngle.#escapeAttribute(name)}"
7296
- ${disabled ? "disabled" : ""}>
7297
- ${this.showRotations ? `<span slot="append" class="fig-input-angle-rotations"></span>` : ""}
7298
- </fig-input-number>`
7299
- : ""
7300
- }
7301
- `;
7302
- }
7303
-
7304
7498
  #getRotationCount() {
7305
7499
  const degrees = Math.abs(this.#toDegrees(this.angle));
7306
7500
  return Math.floor(degrees / 360);
7307
7501
  }
7308
7502
 
7309
- static #escapeAttribute(value) {
7310
- return String(value)
7311
- .replace(/&/g, "&amp;")
7312
- .replace(/"/g, "&quot;")
7313
- .replace(/</g, "&lt;")
7314
- .replace(/>/g, "&gt;");
7315
- }
7316
-
7317
7503
  #isDisabled() {
7318
7504
  return figLabBooleanAttribute(this, "disabled");
7319
7505
  }