@rogieking/figui3 4.0.0 → 4.1.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.
Files changed (3) hide show
  1. package/components.css +9 -310
  2. package/fig.js +182 -1831
  3. package/package.json +1 -1
package/fig.js CHANGED
@@ -4472,268 +4472,6 @@ class FigField extends HTMLElement {
4472
4472
  }
4473
4473
  customElements.define("fig-field", FigField);
4474
4474
 
4475
- /* Field + Slider wrapper */
4476
- class FigFieldSlider extends HTMLElement {
4477
- #field = null;
4478
- #label = null;
4479
- #slider = null;
4480
- #observer = null;
4481
- #managedSliderAttrs = new Set();
4482
- #steppersSyncFrame = 0;
4483
- #boundHandleSliderInput = null;
4484
- #boundHandleSliderChange = null;
4485
- #ignoredSliderAttrs = new Set(["variant", "color", "text", "full"]);
4486
-
4487
- static get observedAttributes() {
4488
- return ["label", "direction"];
4489
- }
4490
-
4491
- connectedCallback() {
4492
- if (!this.#field) {
4493
- this.#initialize();
4494
- }
4495
-
4496
- this.#syncField();
4497
- this.#syncSliderAttributes();
4498
- this.#bindSliderEvents();
4499
-
4500
- if (!this.#observer) {
4501
- this.#observer = new MutationObserver((mutations) => {
4502
- let syncField = false;
4503
- let syncSlider = false;
4504
-
4505
- for (const mutation of mutations) {
4506
- if (mutation.type === "attributes") {
4507
- if (
4508
- mutation.attributeName &&
4509
- this.#ignoredSliderAttrs.has(mutation.attributeName)
4510
- ) {
4511
- continue;
4512
- }
4513
- if (
4514
- mutation.attributeName === "label" ||
4515
- mutation.attributeName === "direction"
4516
- ) {
4517
- syncField = true;
4518
- } else {
4519
- syncSlider = true;
4520
- }
4521
- }
4522
- }
4523
-
4524
- if (syncField) this.#syncField();
4525
- if (syncSlider) this.#syncSliderAttributes();
4526
- });
4527
- }
4528
-
4529
- this.#observer.observe(this, { attributes: true });
4530
- }
4531
-
4532
- disconnectedCallback() {
4533
- this.#observer?.disconnect();
4534
- if (this.#steppersSyncFrame) {
4535
- cancelAnimationFrame(this.#steppersSyncFrame);
4536
- this.#steppersSyncFrame = 0;
4537
- }
4538
- this.#unbindSliderEvents();
4539
- }
4540
-
4541
- attributeChangedCallback(name, oldValue, newValue) {
4542
- if (oldValue === newValue || !this.#field) return;
4543
- if (name === "label" || name === "direction") {
4544
- this.#syncField();
4545
- }
4546
- }
4547
-
4548
- #initialize() {
4549
- const initialChildren = Array.from(this.childNodes).filter((node) => {
4550
- return (
4551
- node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim())
4552
- );
4553
- });
4554
-
4555
- const field = document.createElement("fig-field");
4556
- const label = document.createElement("label");
4557
- const slider = document.createElement("fig-slider");
4558
- slider.setAttribute("text", "true");
4559
- for (const attrName of this.#getForwardedSliderAttrNames()) {
4560
- const value = this.getAttribute(attrName);
4561
- slider.setAttribute(attrName, value ?? "");
4562
- }
4563
-
4564
- field.append(label, slider);
4565
-
4566
- this.#field = field;
4567
- this.#label = label;
4568
- this.#slider = slider;
4569
-
4570
- this.replaceChildren(field);
4571
-
4572
- for (const node of initialChildren) {
4573
- this.#slider.appendChild(node);
4574
- }
4575
- }
4576
-
4577
- #syncField() {
4578
- if (!this.#field || !this.#label) return;
4579
- const hasLabelAttr = this.hasAttribute("label");
4580
- const rawLabel = this.getAttribute("label");
4581
- const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
4582
-
4583
- if (isBlankLabel) {
4584
- if (this.#label.parentElement === this.#field) {
4585
- this.#label.remove();
4586
- }
4587
- } else {
4588
- this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
4589
- if (this.#label.parentElement !== this.#field) {
4590
- this.#field.prepend(this.#label);
4591
- }
4592
- }
4593
-
4594
- this.#field.setAttribute(
4595
- "direction",
4596
- this.getAttribute("direction") || "horizontal",
4597
- );
4598
- }
4599
-
4600
- #syncSliderAttributes() {
4601
- if (!this.#slider) return;
4602
- const hostAttrs = this.#getForwardedSliderAttrNames();
4603
-
4604
- const nextManaged = new Set(hostAttrs.filter((name) => name !== "text"));
4605
-
4606
- for (const attrName of this.#managedSliderAttrs) {
4607
- if (!nextManaged.has(attrName)) {
4608
- this.#slider.removeAttribute(attrName);
4609
- }
4610
- }
4611
-
4612
- for (const attrName of hostAttrs) {
4613
- if (attrName === "text") continue;
4614
- const value = this.getAttribute(attrName);
4615
- this.#slider.setAttribute(attrName, value ?? "");
4616
- }
4617
-
4618
- this.#slider.removeAttribute("variant");
4619
- this.#slider.removeAttribute("color");
4620
- this.#slider.removeAttribute("transform");
4621
- this.#slider.removeAttribute("full");
4622
- this.#slider.setAttribute("text", "true");
4623
-
4624
- const sliderType = (this.getAttribute("type") || "range").toLowerCase();
4625
- if (sliderType === "delta" || sliderType === "stepper") {
4626
- this.#slider.setAttribute(
4627
- "default",
4628
- this.getAttribute("default") ?? "50",
4629
- );
4630
- } else if (!this.hasAttribute("default")) {
4631
- this.#slider.removeAttribute("default");
4632
- }
4633
- if (sliderType === "stepper") {
4634
- this.#slider.setAttribute("step", this.getAttribute("step") ?? "10");
4635
- } else if (!this.hasAttribute("step")) {
4636
- this.#slider.removeAttribute("step");
4637
- }
4638
- if (sliderType === "opacity") {
4639
- this.#slider.style.setProperty(
4640
- "--color",
4641
- "var(--figma-color-bg-tertiary)",
4642
- );
4643
- } else {
4644
- this.#slider.style.removeProperty("--color");
4645
- }
4646
-
4647
- this.#managedSliderAttrs = nextManaged;
4648
- this.#queueSteppersSync();
4649
- }
4650
-
4651
- #getForwardedSliderAttrNames() {
4652
- const reserved = new Set([
4653
- "label",
4654
- "direction",
4655
- "oninput",
4656
- "onchange",
4657
- "steppers",
4658
- ]);
4659
- return this.getAttributeNames().filter(
4660
- (name) => !reserved.has(name) && !this.#ignoredSliderAttrs.has(name),
4661
- );
4662
- }
4663
-
4664
- #queueSteppersSync() {
4665
- if (this.#steppersSyncFrame) {
4666
- cancelAnimationFrame(this.#steppersSyncFrame);
4667
- }
4668
- this.#steppersSyncFrame = requestAnimationFrame(() => {
4669
- this.#steppersSyncFrame = 0;
4670
- this.#syncSteppersToNumberInput();
4671
- });
4672
- }
4673
-
4674
- #syncSteppersToNumberInput() {
4675
- if (!this.#slider) return;
4676
- const numberInput = this.#slider.querySelector("fig-input-number");
4677
- if (!numberInput) return;
4678
-
4679
- const hasSteppers =
4680
- this.hasAttribute("steppers") &&
4681
- this.getAttribute("steppers") !== "false";
4682
- if (!hasSteppers) {
4683
- numberInput.removeAttribute("steppers");
4684
- return;
4685
- }
4686
-
4687
- const steppersValue = this.getAttribute("steppers");
4688
- numberInput.setAttribute("steppers", steppersValue ?? "");
4689
- }
4690
-
4691
- #bindSliderEvents() {
4692
- if (!this.#slider) return;
4693
- if (!this.#boundHandleSliderInput) {
4694
- this.#boundHandleSliderInput = this.#forwardSliderEvent.bind(
4695
- this,
4696
- "input",
4697
- );
4698
- }
4699
- if (!this.#boundHandleSliderChange) {
4700
- this.#boundHandleSliderChange = this.#forwardSliderEvent.bind(
4701
- this,
4702
- "change",
4703
- );
4704
- }
4705
- this.#slider.addEventListener("input", this.#boundHandleSliderInput);
4706
- this.#slider.addEventListener("change", this.#boundHandleSliderChange);
4707
- }
4708
-
4709
- #unbindSliderEvents() {
4710
- if (!this.#slider) return;
4711
- if (this.#boundHandleSliderInput) {
4712
- this.#slider.removeEventListener("input", this.#boundHandleSliderInput);
4713
- }
4714
- if (this.#boundHandleSliderChange) {
4715
- this.#slider.removeEventListener("change", this.#boundHandleSliderChange);
4716
- }
4717
- }
4718
-
4719
- #forwardSliderEvent(type, event) {
4720
- event.stopPropagation();
4721
- const detail =
4722
- event instanceof CustomEvent && event.detail !== undefined
4723
- ? event.detail
4724
- : this.#slider?.value;
4725
- this.dispatchEvent(
4726
- new CustomEvent(type, {
4727
- detail,
4728
- bubbles: true,
4729
- cancelable: true,
4730
- composed: true,
4731
- }),
4732
- );
4733
- }
4734
- }
4735
- customElements.define("fig-field-slider", FigFieldSlider);
4736
-
4737
4475
  /* Color swatch */
4738
4476
  class FigInputColor extends HTMLElement {
4739
4477
  rgba;
@@ -7695,142 +7433,206 @@ figDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
7695
7433
  * @attr {string} options - Comma-separated list of dropdown options
7696
7434
  * @attr {string} placeholder - Placeholder text for the input
7697
7435
  * @attr {string} value - The current input value
7436
+ * @attr {boolean} disabled - Disables the input and dropdown button
7437
+ * @attr {string} experimental - Feature flag passed to internal fig-dropdown
7698
7438
  */
7699
7439
  class FigComboInput extends HTMLElement {
7440
+ static observedAttributes = [
7441
+ "options",
7442
+ "placeholder",
7443
+ "value",
7444
+ "disabled",
7445
+ "experimental",
7446
+ ];
7447
+
7700
7448
  #usesCustomDropdown = false;
7701
- #boundHandleSelectInput = null;
7449
+ #input = null;
7450
+ #dropdown = null;
7451
+ #button = null;
7452
+ #customDropdown = null;
7453
+ #internalUpdate = false;
7702
7454
 
7703
- constructor() {
7704
- super();
7705
- this.#boundHandleSelectInput = this.handleSelectInput.bind(this);
7455
+ #boundHandleDropdownInput = this.#handleDropdownInput.bind(this);
7456
+ #boundHandleTextInput = this.#handleTextInput.bind(this);
7457
+ #boundHandleTextChange = this.#handleTextChange.bind(this);
7458
+
7459
+ get value() {
7460
+ return this.getAttribute("value") || "";
7706
7461
  }
7707
- getOptionsFromAttribute() {
7708
- return (this.getAttribute("options") || "").split(",");
7462
+
7463
+ set value(val) {
7464
+ this.setAttribute("value", val ?? "");
7709
7465
  }
7466
+
7710
7467
  connectedCallback() {
7711
- const customDropdown =
7468
+ this.#customDropdown =
7712
7469
  Array.from(this.children).find(
7713
7470
  (child) => child.tagName === "FIG-DROPDOWN",
7714
7471
  ) || null;
7715
- this.#usesCustomDropdown = customDropdown !== null;
7716
- if (customDropdown) {
7717
- customDropdown.remove();
7472
+ this.#usesCustomDropdown = this.#customDropdown !== null;
7473
+ if (this.#customDropdown) {
7474
+ this.#customDropdown.remove();
7718
7475
  }
7719
7476
 
7720
- this.options = this.getOptionsFromAttribute();
7721
- this.placeholder = this.getAttribute("placeholder") || "";
7722
- this.value = this.getAttribute("value") || "";
7477
+ this.#render();
7478
+ this.#setupListeners();
7479
+
7480
+ if (this.hasAttribute("disabled")) {
7481
+ this.#applyDisabled(true);
7482
+ }
7483
+ }
7484
+
7485
+ disconnectedCallback() {
7486
+ this.#teardownListeners();
7487
+ }
7488
+
7489
+ #render() {
7490
+ const options = this.#getOptions();
7491
+ const placeholder = this.getAttribute("placeholder") || "";
7492
+ const currentValue = this.value;
7723
7493
  const experimental = this.getAttribute("experimental");
7724
- const expAttr = experimental ? `experimental="${experimental}"` : "";
7494
+ const expAttr = experimental ? ` experimental="${experimental}"` : "";
7495
+
7725
7496
  const dropdownHTML = this.#usesCustomDropdown
7726
7497
  ? ""
7727
- : `<fig-dropdown type="dropdown" ${expAttr}>
7728
- ${this.options
7729
- .map((option) => `<option>${option}</option>`)
7730
- .join("")}
7731
- </fig-dropdown>`;
7498
+ : `<fig-dropdown type="dropdown"${expAttr}>${options.map((o) => `<option>${o.trim()}</option>`).join("")}</fig-dropdown>`;
7499
+
7732
7500
  this.innerHTML = `<div class="input-combo">
7733
- <fig-input-text placeholder="${this.placeholder}">
7734
- </fig-input-text>
7735
- <fig-button type="select" variant="input" icon>
7736
- <svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
7737
- <path d='M5.87868 7.12132L8 9.24264L10.1213 7.12132' stroke='currentColor' stroke-opacity="0.9" stroke-linecap='round'/>
7738
- </svg>
7739
- ${dropdownHTML}
7740
- </fig-button>
7741
- </div>`;
7742
- requestAnimationFrame(() => {
7743
- this.input = this.querySelector("fig-input-text");
7744
- const button = this.querySelector("fig-button");
7501
+ <fig-input-text placeholder="${placeholder}" value="${currentValue}"></fig-input-text>
7502
+ <fig-button type="select" variant="input" icon>
7503
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
7504
+ <path d="M5.87868 7.12132L8 9.24264L10.1213 7.12132" stroke="currentColor" stroke-opacity="0.9" stroke-linecap="round"/>
7505
+ </svg>
7506
+ ${dropdownHTML}
7507
+ </fig-button>
7508
+ </div>`;
7745
7509
 
7746
- if (this.#usesCustomDropdown && customDropdown && button) {
7747
- if (!customDropdown.hasAttribute("type")) {
7748
- customDropdown.setAttribute("type", "dropdown");
7749
- }
7750
- if (experimental) {
7751
- customDropdown.setAttribute("experimental", experimental);
7752
- }
7753
- button.append(customDropdown);
7510
+ this.#input = this.querySelector("fig-input-text");
7511
+ this.#button = this.querySelector("fig-button");
7512
+
7513
+ if (this.#usesCustomDropdown && this.#customDropdown && this.#button) {
7514
+ if (!this.#customDropdown.hasAttribute("type")) {
7515
+ this.#customDropdown.setAttribute("type", "dropdown");
7754
7516
  }
7755
- this.dropdown = this.querySelector("fig-dropdown");
7517
+ if (experimental) {
7518
+ this.#customDropdown.setAttribute("experimental", experimental);
7519
+ }
7520
+ this.#button.append(this.#customDropdown);
7521
+ }
7756
7522
 
7757
- this.dropdown?.removeEventListener("input", this.#boundHandleSelectInput);
7758
- this.dropdown?.addEventListener("input", this.#boundHandleSelectInput);
7523
+ this.#dropdown = this.querySelector("fig-dropdown");
7524
+ }
7759
7525
 
7760
- if (this.input) {
7761
- this.input.setAttribute("value", this.value);
7762
- }
7526
+ #setupListeners() {
7527
+ this.#dropdown?.addEventListener("input", this.#boundHandleDropdownInput);
7528
+ this.#input?.addEventListener("input", this.#boundHandleTextInput);
7529
+ this.#input?.addEventListener("change", this.#boundHandleTextChange);
7530
+ }
7763
7531
 
7764
- // Apply initial disabled state
7765
- if (this.hasAttribute("disabled")) {
7766
- this.#applyDisabled(true);
7767
- }
7768
- });
7532
+ #teardownListeners() {
7533
+ this.#dropdown?.removeEventListener("input", this.#boundHandleDropdownInput);
7534
+ this.#input?.removeEventListener("input", this.#boundHandleTextInput);
7535
+ this.#input?.removeEventListener("change", this.#boundHandleTextChange);
7769
7536
  }
7770
- disconnectedCallback() {
7771
- this.dropdown?.removeEventListener("input", this.#boundHandleSelectInput);
7537
+
7538
+ #handleDropdownInput(e) {
7539
+ e.stopPropagation();
7540
+ const val = e.target.closest("fig-dropdown")?.value ?? "";
7541
+ this.#internalUpdate = true;
7542
+ this.setAttribute("value", val);
7543
+ this.#internalUpdate = false;
7544
+ if (this.#input) this.#input.setAttribute("value", val);
7545
+ this.#emitInput();
7546
+ this.#emitChange();
7547
+ }
7548
+
7549
+ #handleTextInput(e) {
7550
+ e.stopPropagation();
7551
+ const val = e.target.value ?? "";
7552
+ this.#internalUpdate = true;
7553
+ this.setAttribute("value", val);
7554
+ this.#internalUpdate = false;
7555
+ this.#emitInput();
7772
7556
  }
7773
- handleSelectInput(e) {
7774
- this.setAttribute("value", e.target.closest("fig-dropdown").value);
7557
+
7558
+ #handleTextChange(e) {
7559
+ e.stopPropagation();
7560
+ const val = e.target.value ?? "";
7561
+ this.#internalUpdate = true;
7562
+ this.setAttribute("value", val);
7563
+ this.#internalUpdate = false;
7564
+ this.#emitChange();
7775
7565
  }
7776
- handleInput(e) {
7777
- this.value = this.input.value;
7566
+
7567
+ #emitInput() {
7568
+ this.dispatchEvent(
7569
+ new CustomEvent("input", {
7570
+ bubbles: true,
7571
+ cancelable: true,
7572
+ detail: { value: this.value },
7573
+ }),
7574
+ );
7778
7575
  }
7779
- static get observedAttributes() {
7780
- return ["options", "placeholder", "value", "disabled", "experimental"];
7576
+
7577
+ #emitChange() {
7578
+ this.dispatchEvent(
7579
+ new CustomEvent("change", {
7580
+ bubbles: true,
7581
+ cancelable: true,
7582
+ detail: { value: this.value },
7583
+ }),
7584
+ );
7781
7585
  }
7782
- focus() {
7783
- this.input.focus();
7586
+
7587
+ #getOptions() {
7588
+ return (this.getAttribute("options") || "")
7589
+ .split(",")
7590
+ .map((s) => s.trim())
7591
+ .filter(Boolean);
7784
7592
  }
7593
+
7785
7594
  #applyDisabled(disabled) {
7786
- if (this.input) {
7787
- if (disabled) {
7788
- this.input.setAttribute("disabled", "");
7789
- } else {
7790
- this.input.removeAttribute("disabled");
7791
- }
7595
+ if (this.#input) {
7596
+ if (disabled) this.#input.setAttribute("disabled", "");
7597
+ else this.#input.removeAttribute("disabled");
7792
7598
  }
7793
- const button = this.querySelector("fig-button");
7794
- if (button) {
7795
- if (disabled) {
7796
- button.setAttribute("disabled", "");
7797
- } else {
7798
- button.removeAttribute("disabled");
7799
- }
7599
+ if (this.#button) {
7600
+ if (disabled) this.#button.setAttribute("disabled", "");
7601
+ else this.#button.removeAttribute("disabled");
7800
7602
  }
7801
7603
  }
7604
+
7605
+ focus() {
7606
+ this.#input?.focus();
7607
+ }
7608
+
7802
7609
  attributeChangedCallback(name, oldValue, newValue) {
7610
+ if (oldValue === newValue) return;
7803
7611
  switch (name) {
7804
7612
  case "options":
7805
- this.options = newValue.split(",");
7806
- if (this.dropdown && !this.#usesCustomDropdown) {
7807
- this.dropdown.innerHTML = this.options
7808
- .map((option) => `<option>${option}</option>`)
7613
+ if (this.#dropdown && !this.#usesCustomDropdown) {
7614
+ const options = this.#getOptions();
7615
+ this.#dropdown.innerHTML = options
7616
+ .map((o) => `<option>${o}</option>`)
7809
7617
  .join("");
7810
7618
  }
7811
7619
  break;
7812
7620
  case "placeholder":
7813
- this.placeholder = newValue;
7814
- if (this.input) {
7815
- this.input.setAttribute("placeholder", newValue);
7816
- }
7621
+ if (this.#input) this.#input.setAttribute("placeholder", newValue || "");
7817
7622
  break;
7818
7623
  case "value":
7819
- this.value = newValue;
7820
- if (this.input) {
7821
- this.input.setAttribute("value", newValue);
7624
+ if (!this.#internalUpdate && this.#input) {
7625
+ this.#input.setAttribute("value", newValue || "");
7822
7626
  }
7823
7627
  break;
7824
7628
  case "disabled":
7825
7629
  this.#applyDisabled(newValue !== null && newValue !== "false");
7826
7630
  break;
7827
7631
  case "experimental":
7828
- if (this.dropdown) {
7829
- if (newValue) {
7830
- this.dropdown.setAttribute("experimental", newValue);
7831
- } else if (!this.#usesCustomDropdown) {
7832
- this.dropdown.removeAttribute("experimental");
7833
- }
7632
+ if (this.#dropdown) {
7633
+ if (newValue) this.#dropdown.setAttribute("experimental", newValue);
7634
+ else if (!this.#usesCustomDropdown)
7635
+ this.#dropdown.removeAttribute("experimental");
7834
7636
  }
7835
7637
  break;
7836
7638
  }
@@ -8336,16 +8138,30 @@ class FigInputFile extends HTMLElement {
8336
8138
 
8337
8139
  #onDragOver = (e) => {
8338
8140
  e.preventDefault();
8339
- this.setAttribute("dragover", "");
8141
+ if (!this.hasAttribute("dragover")) {
8142
+ this.setAttribute("dragover", "");
8143
+ if (this.#uploadBtn) {
8144
+ this.#uploadBtn.dataset.prevText = this.#uploadBtn.textContent;
8145
+ this.#uploadBtn.textContent = "Drop file";
8146
+ }
8147
+ }
8340
8148
  };
8341
8149
 
8342
8150
  #onDragLeave = () => {
8343
8151
  this.removeAttribute("dragover");
8152
+ if (this.#uploadBtn && this.#uploadBtn.dataset.prevText !== undefined) {
8153
+ this.#uploadBtn.textContent = this.#uploadBtn.dataset.prevText;
8154
+ delete this.#uploadBtn.dataset.prevText;
8155
+ }
8344
8156
  };
8345
8157
 
8346
8158
  #onDrop = (e) => {
8347
8159
  e.preventDefault();
8348
8160
  this.removeAttribute("dragover");
8161
+ if (this.#uploadBtn && this.#uploadBtn.dataset.prevText !== undefined) {
8162
+ this.#uploadBtn.textContent = this.#uploadBtn.dataset.prevText;
8163
+ delete this.#uploadBtn.dataset.prevText;
8164
+ }
8349
8165
  if (
8350
8166
  this.hasAttribute("disabled") &&
8351
8167
  this.getAttribute("disabled") !== "false"
@@ -10568,563 +10384,23 @@ class FigInputJoystick extends HTMLElement {
10568
10384
 
10569
10385
  customElements.define("fig-joystick", FigInputJoystick);
10570
10386
 
10571
- /**
10572
- * A custom angle chooser input element.
10573
- * @attr {number} value - The current angle of the handle in degrees.
10574
- * @attr {number} precision - The number of decimal places for the output.
10575
- * @attr {boolean} text - Whether to display a text input for the angle value.
10576
- * @attr {boolean} dial - Whether to display the circular dial control. Defaults to true.
10577
- * @attr {number} adjacent - The adjacent value of the angle.
10578
- * @attr {number} opposite - The opposite value of the angle.
10579
- * @attr {boolean} rotations - Whether to display a rotation count (×N) when rotations > 1. Defaults to false.
10580
- */
10581
- class FigInputAngle extends HTMLElement {
10582
- // Private fields
10583
- #adjacent;
10584
- #opposite;
10585
- #prevRawAngle = null;
10586
- #boundHandleRawChange;
10587
- #boundHandleMouseDown;
10588
- #boundHandleTouchStart;
10589
- #boundHandleKeyDown;
10590
- #boundHandleKeyUp;
10591
- #boundHandleAngleInput;
10592
-
10593
- constructor() {
10594
- super();
10595
-
10596
- this.angle = 0;
10597
- this.#adjacent = 1;
10598
- this.#opposite = 0;
10599
- this.isDragging = false;
10600
- this.isShiftHeld = false;
10601
- this.handle = null;
10602
- this.angleInput = null;
10603
- this.plane = null;
10604
- this.units = "°";
10605
- this.min = null;
10606
- this.max = null;
10607
- this.dial = true;
10608
- this.showRotations = false;
10609
- this.rotationSpan = null;
10610
-
10611
- this.#boundHandleRawChange = this.#handleRawChange.bind(this);
10612
- this.#boundHandleMouseDown = this.#handleMouseDown.bind(this);
10613
- this.#boundHandleTouchStart = this.#handleTouchStart.bind(this);
10614
- this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
10615
- this.#boundHandleKeyUp = this.#handleKeyUp.bind(this);
10616
- this.#boundHandleAngleInput = this.#handleAngleInput.bind(this);
10617
- }
10618
10387
 
10388
+ // FigInputAngle moved to fig-lab.js
10389
+ // FigShimmer
10390
+ class FigShimmer extends HTMLElement {
10619
10391
  connectedCallback() {
10620
- requestAnimationFrame(() => {
10621
- this.precision = this.getAttribute("precision") || 1;
10622
- this.precision = parseInt(this.precision);
10623
- this.text = this.getAttribute("text") === "true";
10624
-
10625
- let rawUnits = this.getAttribute("units") || "°";
10626
- if (rawUnits === "deg") rawUnits = "°";
10627
- this.units = rawUnits;
10628
-
10629
- this.min = this.hasAttribute("min")
10630
- ? Number(this.getAttribute("min"))
10631
- : null;
10632
- this.max = this.hasAttribute("max")
10633
- ? Number(this.getAttribute("max"))
10634
- : null;
10635
- this.dial = this.#readBooleanAttribute("dial", true);
10636
- this.showRotations = this.#readRotationsEnabled();
10637
-
10638
- this.#render();
10639
- this.#setupListeners();
10640
-
10641
- this.#syncHandlePosition();
10642
- if (this.text && this.angleInput) {
10643
- this.angleInput.setAttribute(
10644
- "value",
10645
- this.angle.toFixed(this.precision),
10646
- );
10647
- }
10648
- });
10649
- }
10650
-
10651
- disconnectedCallback() {
10652
- this.#cleanupListeners();
10392
+ const duration = this.getAttribute("duration");
10393
+ if (duration) {
10394
+ this.style.setProperty("--shimmer-duration", duration);
10395
+ }
10653
10396
  }
10654
10397
 
10655
- #render() {
10656
- this.innerHTML = this.#getInnerHTML();
10398
+ static get observedAttributes() {
10399
+ return ["duration", "playing"];
10657
10400
  }
10658
10401
 
10659
- #readBooleanAttribute(name, defaultValue = false) {
10660
- const value = this.getAttribute(name);
10661
- if (value === null) return defaultValue;
10662
- const normalized = value.trim().toLowerCase();
10663
- if (normalized === "" || normalized === "true") return true;
10664
- if (normalized === "false") return false;
10665
- return true;
10666
- }
10667
-
10668
- #readRotationsEnabled() {
10669
- if (this.hasAttribute("rotations")) {
10670
- return this.#readBooleanAttribute("rotations", false);
10671
- }
10672
- // Backward-compat alias
10673
- if (this.hasAttribute("show-rotations")) {
10674
- return this.#readBooleanAttribute("show-rotations", false);
10675
- }
10676
- return false;
10677
- }
10678
-
10679
- #getInnerHTML() {
10680
- const step = this.#getStepForUnit();
10681
- const minAttr = this.min !== null ? `min="${this.min}"` : "";
10682
- const maxAttr = this.max !== null ? `max="${this.max}"` : "";
10683
- return `
10684
- ${
10685
- this.dial
10686
- ? `<div class="fig-input-angle-plane" tabindex="0">
10687
- <div class="fig-input-angle-handle"></div>
10688
- </div>`
10689
- : ""
10690
- }
10691
- ${
10692
- this.text
10693
- ? `<fig-input-number
10694
- name="angle"
10695
- step="${step}"
10696
- value="${this.angle}"
10697
- ${minAttr}
10698
- ${maxAttr}
10699
- units="${this.units}">
10700
- ${this.showRotations ? `<span slot="append" class="fig-input-angle-rotations"></span>` : ""}
10701
- </fig-input-number>`
10702
- : ""
10703
- }
10704
- `;
10705
- }
10706
-
10707
- #getRotationCount() {
10708
- const degrees = Math.abs(this.#toDegrees(this.angle));
10709
- return Math.floor(degrees / 360);
10710
- }
10711
-
10712
- #updateRotationDisplay() {
10713
- if (!this.rotationSpan) return;
10714
- const rotations = this.#getRotationCount();
10715
- if (rotations > 1) {
10716
- this.rotationSpan.textContent = `\u00d7${rotations}`;
10717
- this.rotationSpan.style.display = "";
10718
- } else {
10719
- this.rotationSpan.textContent = "";
10720
- this.rotationSpan.style.display = "none";
10721
- }
10722
- }
10723
-
10724
- #getStepForUnit() {
10725
- switch (this.units) {
10726
- case "rad":
10727
- return 0.01;
10728
- case "turn":
10729
- return 0.001;
10730
- default:
10731
- return 0.1;
10732
- }
10733
- }
10734
-
10735
- // --- Unit conversion helpers ---
10736
-
10737
- #toDegrees(value) {
10738
- switch (this.units) {
10739
- case "rad":
10740
- return (value * 180) / Math.PI;
10741
- case "turn":
10742
- return value * 360;
10743
- default:
10744
- return value;
10745
- }
10746
- }
10747
-
10748
- #fromDegrees(degrees) {
10749
- switch (this.units) {
10750
- case "rad":
10751
- return (degrees * Math.PI) / 180;
10752
- case "turn":
10753
- return degrees / 360;
10754
- default:
10755
- return degrees;
10756
- }
10757
- }
10758
-
10759
- #convertAngle(value, fromUnit, toUnit) {
10760
- // Convert to degrees first
10761
- let degrees;
10762
- switch (fromUnit) {
10763
- case "rad":
10764
- degrees = (value * 180) / Math.PI;
10765
- break;
10766
- case "turn":
10767
- degrees = value * 360;
10768
- break;
10769
- default:
10770
- degrees = value;
10771
- }
10772
- // Convert from degrees to target
10773
- switch (toUnit) {
10774
- case "rad":
10775
- return (degrees * Math.PI) / 180;
10776
- case "turn":
10777
- return degrees / 360;
10778
- default:
10779
- return degrees;
10780
- }
10781
- }
10782
-
10783
- // --- Event listeners ---
10784
-
10785
- #setupListeners() {
10786
- this.handle = this.querySelector(".fig-input-angle-handle");
10787
- this.plane = this.querySelector(".fig-input-angle-plane");
10788
- this.angleInput = this.querySelector("fig-input-number[name='angle']");
10789
- this.rotationSpan = this.querySelector(".fig-input-angle-rotations");
10790
- this.#updateRotationDisplay();
10791
- this.plane?.addEventListener("mousedown", this.#boundHandleMouseDown);
10792
- this.plane?.addEventListener("touchstart", this.#boundHandleTouchStart);
10793
- window.addEventListener("keydown", this.#boundHandleKeyDown);
10794
- window.addEventListener("keyup", this.#boundHandleKeyUp);
10795
- if (this.text && this.angleInput) {
10796
- this.angleInput.addEventListener("input", this.#boundHandleAngleInput);
10797
- }
10798
- this.addEventListener("change", this.#boundHandleRawChange, true);
10799
- }
10800
-
10801
- #cleanupListeners() {
10802
- this.plane?.removeEventListener("mousedown", this.#boundHandleMouseDown);
10803
- this.plane?.removeEventListener("touchstart", this.#boundHandleTouchStart);
10804
- window.removeEventListener("keydown", this.#boundHandleKeyDown);
10805
- window.removeEventListener("keyup", this.#boundHandleKeyUp);
10806
- if (this.text && this.angleInput) {
10807
- this.angleInput.removeEventListener("input", this.#boundHandleAngleInput);
10808
- }
10809
- this.removeEventListener("change", this.#boundHandleRawChange, true);
10810
- }
10811
-
10812
- #handleRawChange(e) {
10813
- // Only intercept native change events from the raw <input> element
10814
- if (!e.target?.matches?.("input")) return;
10815
- const raw = e.target.value;
10816
- const match = raw.match(/^(-?\d*\.?\d+)\s*(turn|rad|deg|°)$/i);
10817
- if (match) {
10818
- const num = parseFloat(match[1]);
10819
- let fromUnit = match[2].toLowerCase();
10820
- if (fromUnit === "deg") fromUnit = "°";
10821
- if (fromUnit !== this.units) {
10822
- const converted = this.#convertAngle(num, fromUnit, this.units);
10823
- e.target.value = String(converted);
10824
- }
10825
- }
10826
- }
10827
-
10828
- #handleAngleInput(e) {
10829
- e.stopPropagation();
10830
- this.angle = Number(e.target.value);
10831
- this.#calculateAdjacentAndOpposite();
10832
- this.#syncHandlePosition();
10833
- this.#updateRotationDisplay();
10834
- this.#emitInputEvent();
10835
- this.#emitChangeEvent();
10836
- }
10837
-
10838
- // --- Angle calculation ---
10839
-
10840
- #calculateAdjacentAndOpposite() {
10841
- const degrees = this.#toDegrees(this.angle);
10842
- const radians = (degrees * Math.PI) / 180;
10843
- this.#adjacent = Math.cos(radians);
10844
- this.#opposite = Math.sin(radians);
10845
- }
10846
-
10847
- #snapToIncrement(angle) {
10848
- if (!this.isShiftHeld) return angle;
10849
- const increment = 45;
10850
- return Math.round(angle / increment) * increment;
10851
- }
10852
-
10853
- #getRawAngle(e) {
10854
- const rect = this.plane.getBoundingClientRect();
10855
- const centerX = rect.left + rect.width / 2;
10856
- const centerY = rect.top + rect.height / 2;
10857
- const deltaX = e.clientX - centerX;
10858
- const deltaY = e.clientY - centerY;
10859
- return (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
10860
- }
10861
-
10862
- #updateAngle(e) {
10863
- let rawAngle = this.#getRawAngle(e);
10864
- // Normalize to 0-360 for snap and positioning
10865
- let normalizedAngle = ((rawAngle % 360) + 360) % 360;
10866
- normalizedAngle = this.#snapToIncrement(normalizedAngle);
10867
-
10868
- const isBounded = this.min !== null || this.max !== null;
10869
-
10870
- if (isBounded) {
10871
- // Bounded: absolute position
10872
- this.angle = this.#fromDegrees(normalizedAngle);
10873
- } else {
10874
- // Unbounded: cumulative winding
10875
- if (this.#prevRawAngle === null) {
10876
- // First event of this drag — snap to clicked position, preserving revolution
10877
- this.#prevRawAngle = normalizedAngle;
10878
- const currentDeg = this.#toDegrees(this.angle);
10879
- const currentMod = ((currentDeg % 360) + 360) % 360;
10880
- let delta = normalizedAngle - currentMod;
10881
- if (delta > 180) delta -= 360;
10882
- if (delta < -180) delta += 360;
10883
- this.angle += this.#fromDegrees(delta);
10884
- } else {
10885
- // Subsequent events — accumulate delta
10886
- let delta = normalizedAngle - this.#prevRawAngle;
10887
- if (delta > 180) delta -= 360;
10888
- if (delta < -180) delta += 360;
10889
- this.angle += this.#fromDegrees(delta);
10890
- this.#prevRawAngle = normalizedAngle;
10891
- }
10892
- }
10893
-
10894
- this.#calculateAdjacentAndOpposite();
10895
-
10896
- this.#syncHandlePosition();
10897
- if (this.text && this.angleInput) {
10898
- this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
10899
- }
10900
- this.#updateRotationDisplay();
10901
-
10902
- this.#emitInputEvent();
10903
- }
10904
-
10905
- // --- Event dispatching ---
10906
-
10907
- #emitInputEvent() {
10908
- this.dispatchEvent(
10909
- new CustomEvent("input", {
10910
- bubbles: true,
10911
- cancelable: true,
10912
- detail: { value: this.value, angle: this.angle },
10913
- }),
10914
- );
10915
- }
10916
-
10917
- #emitChangeEvent() {
10918
- this.dispatchEvent(
10919
- new CustomEvent("change", {
10920
- bubbles: true,
10921
- cancelable: true,
10922
- detail: { value: this.value, angle: this.angle },
10923
- }),
10924
- );
10925
- }
10926
-
10927
- // --- Handle position ---
10928
-
10929
- #syncHandlePosition() {
10930
- if (this.handle) {
10931
- const degrees = this.#toDegrees(this.angle);
10932
- const radians = (degrees * Math.PI) / 180;
10933
- const radius = this.plane.offsetWidth / 2 - this.handle.offsetWidth / 2;
10934
- const x = Math.cos(radians) * radius;
10935
- const y = Math.sin(radians) * radius;
10936
- this.handle.style.transform = `translate(${x}px, ${y}px)`;
10937
- }
10938
- }
10939
-
10940
- // --- Mouse/Touch handlers ---
10941
-
10942
- #handleMouseDown(e) {
10943
- this.isDragging = true;
10944
- this.#prevRawAngle = null;
10945
- this.#updateAngle(e);
10946
-
10947
- const handleMouseMove = (e) => {
10948
- this.plane.classList.add("dragging");
10949
- if (this.isDragging) this.#updateAngle(e);
10950
- };
10951
-
10952
- const handleMouseUp = () => {
10953
- this.isDragging = false;
10954
- this.#prevRawAngle = null;
10955
- this.plane.classList.remove("dragging");
10956
- window.removeEventListener("mousemove", handleMouseMove);
10957
- window.removeEventListener("mouseup", handleMouseUp);
10958
- this.#emitChangeEvent();
10959
- };
10960
-
10961
- window.addEventListener("mousemove", handleMouseMove);
10962
- window.addEventListener("mouseup", handleMouseUp);
10963
- }
10964
-
10965
- #handleTouchStart(e) {
10966
- e.preventDefault();
10967
- this.isDragging = true;
10968
- this.#prevRawAngle = null;
10969
- this.#updateAngle(e.touches[0]);
10970
-
10971
- const handleTouchMove = (e) => {
10972
- this.plane.classList.add("dragging");
10973
- if (this.isDragging) this.#updateAngle(e.touches[0]);
10974
- };
10975
-
10976
- const handleTouchEnd = () => {
10977
- this.isDragging = false;
10978
- this.#prevRawAngle = null;
10979
- this.plane.classList.remove("dragging");
10980
- window.removeEventListener("touchmove", handleTouchMove);
10981
- window.removeEventListener("touchend", handleTouchEnd);
10982
- this.#emitChangeEvent();
10983
- };
10984
-
10985
- window.addEventListener("touchmove", handleTouchMove);
10986
- window.addEventListener("touchend", handleTouchEnd);
10987
- }
10988
-
10989
- // --- Keyboard handlers ---
10990
-
10991
- #handleKeyDown(e) {
10992
- if (e.key === "Shift") this.isShiftHeld = true;
10993
- }
10994
-
10995
- #handleKeyUp(e) {
10996
- if (e.key === "Shift") this.isShiftHeld = false;
10997
- }
10998
-
10999
- focus() {
11000
- this.plane?.focus();
11001
- }
11002
-
11003
- // --- Attributes ---
11004
-
11005
- static get observedAttributes() {
11006
- return [
11007
- "value",
11008
- "precision",
11009
- "text",
11010
- "min",
11011
- "max",
11012
- "units",
11013
- "dial",
11014
- "rotations",
11015
- "show-rotations",
11016
- ];
11017
- }
11018
-
11019
- get value() {
11020
- return this.angle;
11021
- }
11022
-
11023
- get adjacent() {
11024
- return this.#adjacent;
11025
- }
11026
-
11027
- get opposite() {
11028
- return this.#opposite;
11029
- }
11030
-
11031
- set value(value) {
11032
- if (isNaN(value)) {
11033
- console.error("Invalid value: must be a number.");
11034
- return;
11035
- }
11036
- this.angle = value;
11037
- this.#calculateAdjacentAndOpposite();
11038
- this.#syncHandlePosition();
11039
- if (this.angleInput) {
11040
- this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
11041
- }
11042
- this.#updateRotationDisplay();
11043
- }
11044
-
11045
- attributeChangedCallback(name, oldValue, newValue) {
11046
- switch (name) {
11047
- case "value":
11048
- if (this.isDragging) break;
11049
- this.value = Number(newValue);
11050
- break;
11051
- case "precision":
11052
- this.precision = parseInt(newValue);
11053
- break;
11054
- case "text":
11055
- if (newValue !== oldValue) {
11056
- this.text = newValue?.toLowerCase() === "true";
11057
- if (this.isConnected) {
11058
- this.#render();
11059
- this.#setupListeners();
11060
- this.#syncHandlePosition();
11061
- }
11062
- }
11063
- break;
11064
- case "dial":
11065
- this.dial = this.#readBooleanAttribute("dial", true);
11066
- if (this.isConnected) {
11067
- this.#render();
11068
- this.#setupListeners();
11069
- this.#syncHandlePosition();
11070
- }
11071
- break;
11072
- case "units": {
11073
- let units = newValue || "°";
11074
- if (units === "deg") units = "°";
11075
- this.units = units;
11076
- if (this.isConnected) {
11077
- this.#render();
11078
- this.#setupListeners();
11079
- this.#syncHandlePosition();
11080
- }
11081
- break;
11082
- }
11083
- case "min":
11084
- this.min = newValue !== null ? Number(newValue) : null;
11085
- if (this.isConnected) {
11086
- this.#render();
11087
- this.#setupListeners();
11088
- this.#syncHandlePosition();
11089
- }
11090
- break;
11091
- case "max":
11092
- this.max = newValue !== null ? Number(newValue) : null;
11093
- if (this.isConnected) {
11094
- this.#render();
11095
- this.#setupListeners();
11096
- this.#syncHandlePosition();
11097
- }
11098
- break;
11099
- case "rotations":
11100
- case "show-rotations":
11101
- this.showRotations = this.#readRotationsEnabled();
11102
- if (this.isConnected) {
11103
- this.#render();
11104
- this.#setupListeners();
11105
- this.#syncHandlePosition();
11106
- }
11107
- break;
11108
- }
11109
- }
11110
- }
11111
- customElements.define("fig-input-angle", FigInputAngle);
11112
-
11113
- // FigShimmer
11114
- class FigShimmer extends HTMLElement {
11115
- connectedCallback() {
11116
- const duration = this.getAttribute("duration");
11117
- if (duration) {
11118
- this.style.setProperty("--shimmer-duration", duration);
11119
- }
11120
- }
11121
-
11122
- static get observedAttributes() {
11123
- return ["duration", "playing"];
11124
- }
11125
-
11126
- get playing() {
11127
- return this.getAttribute("playing") !== "false";
10402
+ get playing() {
10403
+ return this.getAttribute("playing") !== "false";
11128
10404
  }
11129
10405
 
11130
10406
  set playing(value) {
@@ -12421,9 +11697,9 @@ class FigFillPicker extends HTMLElement {
12421
11697
  <option value="angular">Angular</option>
12422
11698
  </fig-dropdown>
12423
11699
  <fig-tooltip text="Rotate gradient">
12424
- <fig-input-angle class="fig-fill-picker-gradient-angle" value="${
11700
+ <fig-input-number class="fig-fill-picker-gradient-angle" value="${
12425
11701
  (this.#gradient.angle - 90 + 360) % 360
12426
- }"></fig-input-angle>
11702
+ }" min="0" max="360" units="°" wrap></fig-input-number>
12427
11703
  </fig-tooltip>
12428
11704
  <div class="fig-fill-picker-gradient-center input-combo" style="display: none;">
12429
11705
  <fig-input-number min="0" max="100" value="${
@@ -12520,7 +11796,6 @@ class FigFillPicker extends HTMLElement {
12520
11796
  );
12521
11797
 
12522
11798
  // Angle input
12523
- // Convert from fig-input-angle coordinates (0° = right) to CSS coordinates (0° = up)
12524
11799
  const angleInput = container.querySelector(
12525
11800
  ".fig-fill-picker-gradient-angle",
12526
11801
  );
@@ -14401,930 +13676,6 @@ class FigChooser extends HTMLElement {
14401
13676
  }
14402
13677
  customElements.define("fig-chooser", FigChooser);
14403
13678
 
14404
- /* Canvas Control */
14405
- class FigCanvasControl extends HTMLElement {
14406
- static observedAttributes = [
14407
- "type",
14408
- "value",
14409
- "color",
14410
- "name",
14411
- "tooltips",
14412
- "disabled",
14413
- "drag-surface",
14414
- "snapping",
14415
- ];
14416
-
14417
- #x = 50;
14418
- #y = 50;
14419
- #x2 = 75;
14420
- #y2 = 75;
14421
- #radius = 0;
14422
- #radiusIsPercent = false;
14423
- #angle = 0;
14424
- #pointHandle = null;
14425
- #secondHandle = null;
14426
- #angleHandle = null;
14427
- #radiusSvg = null;
14428
- #angleSvg = null;
14429
- #pointTooltip = null;
14430
- #secondTooltip = null;
14431
- #radiusTooltip = null;
14432
- #angleTooltip = null;
14433
- #isDragging = false;
14434
- #isSecondDragging = false;
14435
- #isRadiusDragging = false;
14436
- #isAngleDragging = false;
14437
- #prevBodyCursor = "";
14438
-
14439
- get #type() {
14440
- return this.getAttribute("type") || "point";
14441
- }
14442
-
14443
- get #hasRadius() {
14444
- return this.#type === "point-radius" || this.#type === "point-radius-angle";
14445
- }
14446
-
14447
- get #hasAngle() {
14448
- return this.#type === "point-radius-angle";
14449
- }
14450
-
14451
- get #hasSecondPoint() {
14452
- return this.#type === "point-point";
14453
- }
14454
-
14455
- get #hasLine() {
14456
- return this.#type === "point-radius-angle" || this.#type === "point-point";
14457
- }
14458
-
14459
- get #tooltipsEnabled() {
14460
- const v = this.getAttribute("tooltips");
14461
- return v === null || v !== "false";
14462
- }
14463
-
14464
- get #snappingMode() {
14465
- const raw = this.getAttribute("snapping");
14466
- if (raw === null) return "false";
14467
- const n = raw.trim().toLowerCase();
14468
- if (n === "modifier") return "modifier";
14469
- if (n === "" || n === "true") return "true";
14470
- return "false";
14471
- }
14472
-
14473
- #shouldSnap(shiftKey) {
14474
- const mode = this.#snappingMode;
14475
- if (mode === "true") return true;
14476
- if (mode === "modifier") return !!shiftKey;
14477
- return false;
14478
- }
14479
-
14480
- get #pointTipText() {
14481
- const name = this.getAttribute("name");
14482
- if (name) {
14483
- const parts = name.split(",");
14484
- return parts[0].trim();
14485
- }
14486
- return `${Math.round(this.#x)}%, ${Math.round(this.#y)}%`;
14487
- }
14488
-
14489
- get #secondTipText() {
14490
- const name = this.getAttribute("name");
14491
- if (name) {
14492
- const parts = name.split(",");
14493
- if (parts.length > 1) return parts[1].trim();
14494
- }
14495
- return `${Math.round(this.#x2)}%, ${Math.round(this.#y2)}%`;
14496
- }
14497
-
14498
- get #dragSurface() {
14499
- return this.getAttribute("drag-surface") || "parent";
14500
- }
14501
-
14502
- get #container() {
14503
- const surface = this.#dragSurface;
14504
- if (surface === "parent") return this.parentElement;
14505
- return this.closest(surface);
14506
- }
14507
-
14508
- get #handleDragSurface() {
14509
- const surface = this.#dragSurface;
14510
- if (surface === "parent") {
14511
- const container = this.parentElement;
14512
- if (container) {
14513
- container.setAttribute("data-fig-canvas-control-surface", "");
14514
- return "[data-fig-canvas-control-surface]";
14515
- }
14516
- }
14517
- return surface;
14518
- }
14519
-
14520
- #resolveRadius(containerWidth) {
14521
- if (this.#radiusIsPercent) return (this.#radius / 100) * containerWidth;
14522
- return this.#radius;
14523
- }
14524
-
14525
- #formatRadius() {
14526
- if (this.#radiusIsPercent) return `Radius ${Math.round(this.#radius)}%`;
14527
- return `Radius ${Math.round(this.#radius)}`;
14528
- }
14529
-
14530
- connectedCallback() {
14531
- this.#parseValue();
14532
- this.#render();
14533
- }
14534
-
14535
- disconnectedCallback() {
14536
- this.#teardownRadiusDrag();
14537
- }
14538
-
14539
- attributeChangedCallback(name, oldVal, newVal) {
14540
- if (oldVal === newVal) return;
14541
- if (
14542
- name === "value" &&
14543
- !this.#isDragging &&
14544
- !this.#isSecondDragging &&
14545
- !this.#isRadiusDragging &&
14546
- !this.#isAngleDragging
14547
- ) {
14548
- this.#parseValue();
14549
- if (this.#pointHandle) this.#syncPositions();
14550
- else this.#render();
14551
- }
14552
- if (name === "type") {
14553
- this.#parseValue();
14554
- this.#render();
14555
- }
14556
- if (name === "color" && this.#pointHandle) {
14557
- if (newVal) this.#pointHandle.setAttribute("color", newVal);
14558
- else this.#pointHandle.removeAttribute("color");
14559
- }
14560
- if (name === "disabled") {
14561
- this.#render();
14562
- }
14563
- if (name === "tooltips") {
14564
- this.#render();
14565
- }
14566
- if (name === "snapping" && this.#pointHandle) {
14567
- this.#pointHandle.setAttribute("drag-snapping", newVal || "false");
14568
- if (this.#secondHandle)
14569
- this.#secondHandle.setAttribute("drag-snapping", newVal || "false");
14570
- }
14571
- if (name === "name") {
14572
- if (this.#pointTooltip)
14573
- this.#pointTooltip.setAttribute("text", this.#pointTipText);
14574
- if (this.#secondTooltip)
14575
- this.#secondTooltip.setAttribute("text", this.#secondTipText);
14576
- }
14577
- }
14578
-
14579
- #parseValue() {
14580
- const raw = this.getAttribute("value");
14581
- if (!raw) return;
14582
- try {
14583
- const v = JSON.parse(raw);
14584
- if (typeof v.x === "number") this.#x = v.x;
14585
- if (typeof v.y === "number") this.#y = v.y;
14586
- if (v.radius !== undefined) {
14587
- const rs = String(v.radius);
14588
- if (rs.endsWith("%")) {
14589
- this.#radiusIsPercent = true;
14590
- this.#radius = parseFloat(rs);
14591
- } else {
14592
- this.#radiusIsPercent = false;
14593
- this.#radius = parseFloat(rs);
14594
- }
14595
- if (!Number.isFinite(this.#radius)) this.#radius = 0;
14596
- }
14597
- if (typeof v.angle === "number") this.#angle = v.angle;
14598
- if (typeof v.x2 === "number") this.#x2 = v.x2;
14599
- if (typeof v.y2 === "number") this.#y2 = v.y2;
14600
- } catch {
14601
- /* ignore */
14602
- }
14603
- }
14604
-
14605
- get value() {
14606
- const v = { x: this.#x, y: this.#y };
14607
- if (this.#type === "color") {
14608
- const color =
14609
- this.getAttribute("color") || this.#pointHandle?.getAttribute("color");
14610
- if (color) v.color = color;
14611
- }
14612
- if (this.#hasRadius) {
14613
- v.radius = this.#radiusIsPercent ? `${this.#radius}%` : this.#radius;
14614
- }
14615
- if (this.#hasAngle) v.angle = this.#angle;
14616
- if (this.#hasSecondPoint) {
14617
- v.x2 = this.#x2;
14618
- v.y2 = this.#y2;
14619
- }
14620
- return v;
14621
- }
14622
-
14623
- set value(val) {
14624
- if (typeof val === "object") {
14625
- this.setAttribute("value", JSON.stringify(val));
14626
- } else if (typeof val === "string") {
14627
- this.setAttribute("value", val);
14628
- }
14629
- }
14630
-
14631
- #render() {
14632
- this.innerHTML = "";
14633
- this.#pointHandle = null;
14634
- this.#secondHandle = null;
14635
- this.#angleHandle = null;
14636
- this.#radiusSvg = null;
14637
- this.#angleSvg = null;
14638
- this.#pointTooltip = null;
14639
- this.#secondTooltip = null;
14640
- this.#radiusTooltip = null;
14641
- this.#angleTooltip = null;
14642
-
14643
- const disabled = this.hasAttribute("disabled");
14644
- const type = this.#type;
14645
- const tooltips = this.#tooltipsEnabled;
14646
-
14647
- const handleSurface = this.#handleDragSurface;
14648
-
14649
- const handle = document.createElement("fig-handle");
14650
- handle.setAttribute("drag", "true");
14651
- handle.setAttribute("drag-surface", handleSurface);
14652
- handle.setAttribute("drag-axes", "x,y");
14653
- handle.setAttribute("drag-snapping", this.#snappingMode);
14654
- handle.setAttribute("value", `${this.#x}% ${this.#y}%`);
14655
- if (disabled) handle.setAttribute("disabled", "");
14656
- if (type === "color") {
14657
- handle.setAttribute("type", "color");
14658
- const color = this.getAttribute("color");
14659
- if (color) handle.setAttribute("color", color);
14660
- }
14661
- if (this.#hasSecondPoint) {
14662
- handle.setAttribute("hit-area", "12 circle");
14663
- handle.setAttribute("hit-area-mode", "delegate");
14664
- }
14665
- this.#pointHandle = handle;
14666
-
14667
- if (this.#hasRadius) {
14668
- this.#createRadiusSvg();
14669
- }
14670
-
14671
- if (this.#hasLine) {
14672
- this.#createAngleSvg();
14673
- }
14674
-
14675
- if (tooltips) {
14676
- const tip = document.createElement("fig-tooltip");
14677
- tip.setAttribute("action", "manual");
14678
- tip.setAttribute("theme", "brand");
14679
- tip.setAttribute("pointer", "false");
14680
- tip.setAttribute("text", this.#pointTipText);
14681
- tip.appendChild(handle);
14682
- this.appendChild(tip);
14683
- this.#pointTooltip = tip;
14684
- } else {
14685
- this.appendChild(handle);
14686
- }
14687
-
14688
- if (this.#hasAngle) {
14689
- this.#createAngleHandle(disabled, tooltips, handleSurface);
14690
- }
14691
-
14692
- if (this.#hasSecondPoint) {
14693
- this.#createSecondHandle(disabled, tooltips, handleSurface);
14694
- }
14695
-
14696
- this.#setupEventListeners();
14697
- requestAnimationFrame(() => this.#syncPositions());
14698
- }
14699
-
14700
- #createRadiusSvg() {
14701
- const ns = "http://www.w3.org/2000/svg";
14702
- const svg = document.createElementNS(ns, "svg");
14703
- svg.classList.add("fig-canvas-control-radius");
14704
- svg.setAttribute("overflow", "visible");
14705
- const hitCircle = document.createElementNS(ns, "circle");
14706
- hitCircle.classList.add("fig-canvas-control-radius-hit");
14707
- svg.appendChild(hitCircle);
14708
- const circle = document.createElementNS(ns, "circle");
14709
- svg.appendChild(circle);
14710
- this.#radiusSvg = svg;
14711
-
14712
- if (this.#tooltipsEnabled) {
14713
- const tip = document.createElement("fig-tooltip");
14714
- tip.setAttribute("action", "manual");
14715
- tip.setAttribute("theme", "brand");
14716
- tip.setAttribute("pointer", "false");
14717
- tip.setAttribute("text", this.#formatRadius());
14718
- tip.appendChild(svg);
14719
- this.appendChild(tip);
14720
- this.#radiusTooltip = tip;
14721
- } else {
14722
- this.appendChild(svg);
14723
- }
14724
-
14725
- this.#setupRadiusDrag(hitCircle);
14726
- }
14727
-
14728
- #createAngleSvg() {
14729
- const ns = "http://www.w3.org/2000/svg";
14730
- const svg = document.createElementNS(ns, "svg");
14731
- svg.classList.add("fig-canvas-control-angle-svg");
14732
- svg.setAttribute("overflow", "visible");
14733
- svg.style.position = "absolute";
14734
- svg.style.pointerEvents = "none";
14735
- const line = document.createElementNS(ns, "line");
14736
- line.classList.add("fig-canvas-control-angle-line");
14737
- svg.appendChild(line);
14738
- this.#angleSvg = svg;
14739
- this.appendChild(svg);
14740
- }
14741
-
14742
- #createAngleHandle(disabled, tooltips, handleSurface) {
14743
- const handle = document.createElement("fig-handle");
14744
- handle.setAttribute("drag", "true");
14745
- handle.setAttribute("drag-surface", handleSurface);
14746
- handle.setAttribute("drag-axes", "x,y");
14747
- handle.setAttribute("size", "small");
14748
- handle.setAttribute("hit-area", "12 circle");
14749
- handle.setAttribute("hit-area-mode", "delegate");
14750
- if (disabled) handle.setAttribute("disabled", "");
14751
- this.#angleHandle = handle;
14752
-
14753
- if (tooltips) {
14754
- const tip = document.createElement("fig-tooltip");
14755
- tip.setAttribute("action", "manual");
14756
- tip.setAttribute("theme", "brand");
14757
- tip.setAttribute("pointer", "false");
14758
- tip.setAttribute("text", `${Math.round(this.#angle)}°`);
14759
- tip.appendChild(handle);
14760
- this.appendChild(tip);
14761
- this.#angleTooltip = tip;
14762
- } else {
14763
- this.appendChild(handle);
14764
- }
14765
- }
14766
-
14767
- #createSecondHandle(disabled, tooltips, handleSurface) {
14768
- const handle = document.createElement("fig-handle");
14769
- handle.setAttribute("drag", "true");
14770
- handle.setAttribute("drag-surface", handleSurface);
14771
- handle.setAttribute("drag-axes", "x,y");
14772
- handle.setAttribute("drag-snapping", this.#snappingMode);
14773
- handle.setAttribute("hit-area", "12 circle");
14774
- handle.setAttribute("hit-area-mode", "delegate");
14775
- handle.setAttribute("value", `${this.#x2}% ${this.#y2}%`);
14776
- if (disabled) handle.setAttribute("disabled", "");
14777
- this.#secondHandle = handle;
14778
-
14779
- if (tooltips) {
14780
- const tip = document.createElement("fig-tooltip");
14781
- tip.setAttribute("action", "manual");
14782
- tip.setAttribute("theme", "brand");
14783
- tip.setAttribute("pointer", "false");
14784
- tip.setAttribute("text", this.#secondTipText);
14785
- tip.appendChild(handle);
14786
- this.appendChild(tip);
14787
- this.#secondTooltip = tip;
14788
- } else {
14789
- this.appendChild(handle);
14790
- }
14791
- }
14792
-
14793
- #resizeCursorSvg(deg) {
14794
- const r = Math.round(deg);
14795
- return `url("data:image/svg+xml,%3Csvg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='rotate(${r} 16 16)'%3E%3Cg filter='url(%23f)'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11.1212 16.9998L11.5607 17.4394C12.1465 18.0252 12.1464 18.975 11.5606 19.5607C10.9748 20.1465 10.0251 20.1465 9.4393 19.5606L6.4393 16.5604C5.85354 15.9746 5.85357 15.0249 6.43938 14.4391L9.43938 11.4393C10.0252 10.8535 10.9749 10.8536 11.5607 11.4394C12.1465 12.0252 12.1464 12.9749 11.5606 13.5607L11.1215 13.9998L20.8786 13.9999L20.4394 13.5607C19.8536 12.9749 19.8535 12.0252 20.4393 11.4394C21.0251 10.8536 21.9749 10.8536 22.5606 11.4394L25.5606 14.4393C25.842 14.7206 26 15.1021 26 15.4999C26 15.8978 25.842 16.2793 25.5607 16.5606L22.5607 19.5607C21.9749 20.1465 21.0251 20.1465 20.4393 19.5607C19.8536 18.9749 19.8535 18.0252 20.4393 17.4394L20.8788 16.9999L11.1212 16.9998Z' fill='white'/%3E%3C/g%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.8536 12.1465C11.0488 12.3417 11.0488 12.6583 10.8535 12.8536L8.70715 14.9998L23.2929 14.9999L21.1465 12.8536C20.9512 12.6583 20.9512 12.3417 21.1464 12.1465C21.3417 11.9512 21.6583 11.9512 21.8535 12.1465L24.8535 15.1464C24.9473 15.2402 25 15.3673 25 15.4999C25 15.6326 24.9473 15.7597 24.8536 15.8535L21.8536 18.8536C21.6583 19.0488 21.3417 19.0488 21.1465 18.8536C20.9512 18.6583 20.9512 18.3417 21.1464 18.1465L23.2929 15.9999L8.70705 15.9998L10.8536 18.1465C11.0488 18.3417 11.0488 18.6583 10.8535 18.8536C10.6583 19.0488 10.3417 19.0488 10.1464 18.8535L7.14643 15.8533C6.95118 15.658 6.95119 15.3415 7.14646 15.1462L10.1465 12.1464C10.3417 11.9512 10.6583 11.9512 10.8536 12.1465Z' fill='black'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='f' x='3' y='9' width='26' height='15' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeFlood flood-opacity='0' result='a'/%3E%3CfeColorMatrix in='SourceAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0' result='b'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='1.5'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0'/%3E%3CfeBlend in2='a' result='c'/%3E%3CfeBlend in='SourceGraphic' in2='c'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E") 16 16, nwse-resize`;
14796
- }
14797
-
14798
- #rotateCursorSvg(deg) {
14799
- const r = Math.round(deg - 45);
14800
- return `url("data:image/svg+xml,%3Csvg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='rotate(${r} 16 16)'%3E%3Cg filter='url(%23f)'%3E%3Cpath d='M12.5607 22.4393L12.0216 21.9002C17.1558 21.2216 21.2216 17.1558 21.9002 12.0216L22.4393 12.5607C23.0251 13.1464 23.9749 13.1464 24.5607 12.5607C25.1464 11.9749 25.1464 11.0251 24.5607 10.4393L21.5607 7.43934C20.9749 6.85355 20.0251 6.85355 19.4393 7.43934L16.4393 10.4393C15.8536 11.0251 15.8536 11.9749 16.4393 12.5607C17.0251 13.1464 17.9749 13.1464 18.5607 12.5607L18.8056 12.3157C18.1013 15.5527 15.5527 18.1013 12.3157 18.8056L12.5607 18.5607C13.1464 17.9749 13.1464 17.0251 12.5607 16.4393C11.9749 15.8536 11.0251 15.8536 10.4393 16.4393L7.43934 19.4393C6.85356 20.0251 6.85356 20.9749 7.43934 21.5607L10.4393 24.5607C11.0251 25.1464 11.9749 25.1464 12.5607 24.5607C13.1464 23.9749 13.1464 23.0251 12.5607 22.4393Z' fill='white'/%3E%3C/g%3E%3Cpath d='M23.8536 11.8536C23.6583 12.0488 23.3417 12.0488 23.1464 11.8536L21 9.70711V10.5C21 16.299 16.299 21 10.5 21H9.70711L11.8536 23.1464C12.0488 23.3417 12.0488 23.6583 11.8536 23.8536C11.6583 24.0488 11.3417 24.0488 11.1464 23.8536L8.14645 20.8536C7.95119 20.6583 7.95119 20.3417 8.14645 20.1464L11.1464 17.1464C11.3417 16.9512 11.6583 16.9512 11.8536 17.1464C12.0488 17.3417 12.0488 17.6583 11.8536 17.8536L9.70711 20H10.5C15.7467 20 20 15.7467 20 10.5V9.70711L17.8536 11.8536C17.6583 12.0488 17.3417 12.0488 17.1464 11.8536C16.9512 11.6583 16.9512 11.3417 17.1464 11.1464L20.1464 8.14645C20.3417 7.95119 20.6583 7.95119 20.8536 8.14645L23.8536 11.1464C24.0488 11.3417 24.0488 11.6583 23.8536 11.8536Z' fill='black'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='f' x='4' y='5' width='24' height='24' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeFlood flood-opacity='0' result='a'/%3E%3CfeColorMatrix in='SourceAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0' result='b'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='1.5'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0'/%3E%3CfeBlend in2='a' result='c'/%3E%3CfeBlend in='SourceGraphic' in2='c'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E") 16 16, pointer`;
14801
- }
14802
-
14803
- #syncAngleCursor() {
14804
- if (!this.#angleHandle || !this.#hasAngle) return;
14805
- const hitArea = this.#angleHandle.querySelector(".fig-handle-hit-area");
14806
- if (!hitArea) return;
14807
- hitArea.style.cursor = this.#rotateCursorSvg(this.#angle);
14808
- }
14809
-
14810
- #pointPointLineDeg() {
14811
- return (Math.atan2(this.#y2 - this.#y, this.#x2 - this.#x) * 180) / Math.PI;
14812
- }
14813
-
14814
- #syncPointPointCursors() {
14815
- if (!this.#hasSecondPoint) return;
14816
- const deg = this.#pointPointLineDeg();
14817
- const setHitCursor = (handle, rotateDeg) => {
14818
- if (!handle) return;
14819
- const hitArea = handle.querySelector(".fig-handle-hit-area");
14820
- if (hitArea) hitArea.style.cursor = this.#rotateCursorSvg(rotateDeg);
14821
- };
14822
- setHitCursor(this.#pointHandle, deg + 180);
14823
- setHitCursor(this.#secondHandle, deg);
14824
- }
14825
-
14826
- #positionHandle(handle, xPct, yPct, rect) {
14827
- const hw = handle.offsetWidth / 2;
14828
- const hh = handle.offsetHeight / 2;
14829
- handle.style.left = `${(xPct / 100) * rect.width - hw}px`;
14830
- handle.style.top = `${(yPct / 100) * rect.height - hh}px`;
14831
- }
14832
-
14833
- #syncPositions() {
14834
- const container = this.#container;
14835
- if (!container || !this.#pointHandle) return;
14836
- const rect = container.getBoundingClientRect();
14837
-
14838
- this.#positionHandle(this.#pointHandle, this.#x, this.#y, rect);
14839
-
14840
- if (this.#radiusSvg) {
14841
- const cx = (this.#x / 100) * rect.width;
14842
- const cy = (this.#y / 100) * rect.height;
14843
- const r = this.#resolveRadius(rect.width);
14844
- const svg = this.#radiusSvg;
14845
- const d = Math.max(r * 2, 1);
14846
- svg.style.position = "absolute";
14847
- svg.style.width = `${d}px`;
14848
- svg.style.height = `${d}px`;
14849
- svg.style.left = `${cx - r}px`;
14850
- svg.style.top = `${cy - r}px`;
14851
- svg.setAttribute("viewBox", `0 0 ${d} ${d}`);
14852
- const circles = svg.querySelectorAll("circle");
14853
- for (const c of circles) {
14854
- c.setAttribute("cx", String(r));
14855
- c.setAttribute("cy", String(r));
14856
- c.setAttribute("r", String(Math.max(r - 1, 0)));
14857
- }
14858
- }
14859
-
14860
- if (this.#angleSvg && this.#hasLine) {
14861
- const cx = (this.#x / 100) * rect.width;
14862
- const cy = (this.#y / 100) * rect.height;
14863
- let lx2, ly2;
14864
- if (this.#hasSecondPoint) {
14865
- lx2 = (this.#x2 / 100) * rect.width;
14866
- ly2 = (this.#y2 / 100) * rect.height;
14867
- } else {
14868
- const r = this.#resolveRadius(rect.width);
14869
- const angleRad = (this.#angle * Math.PI) / 180;
14870
- lx2 = cx + r * Math.cos(angleRad);
14871
- ly2 = cy + r * Math.sin(angleRad);
14872
- }
14873
-
14874
- const svg = this.#angleSvg;
14875
- svg.style.width = `${rect.width}px`;
14876
- svg.style.height = `${rect.height}px`;
14877
- svg.style.left = "0";
14878
- svg.style.top = "0";
14879
- svg.setAttribute("viewBox", `0 0 ${rect.width} ${rect.height}`);
14880
- const line = svg.querySelector("line");
14881
- if (line) {
14882
- line.setAttribute("x1", String(cx));
14883
- line.setAttribute("y1", String(cy));
14884
- line.setAttribute("x2", String(lx2));
14885
- line.setAttribute("y2", String(ly2));
14886
- }
14887
- }
14888
-
14889
- if (this.#angleHandle && this.#hasAngle) {
14890
- const cx = (this.#x / 100) * rect.width;
14891
- const cy = (this.#y / 100) * rect.height;
14892
- const r = this.#resolveRadius(rect.width);
14893
- const angleRad = (this.#angle * Math.PI) / 180;
14894
- const ax = cx + r * Math.cos(angleRad);
14895
- const ay = cy + r * Math.sin(angleRad);
14896
- const pxPct = rect.width > 0 ? (ax / rect.width) * 100 : 0;
14897
- const pyPct = rect.height > 0 ? (ay / rect.height) * 100 : 0;
14898
- this.#positionHandle(this.#angleHandle, pxPct, pyPct, rect);
14899
- }
14900
-
14901
- if (this.#secondHandle && this.#hasSecondPoint) {
14902
- this.#positionHandle(this.#secondHandle, this.#x2, this.#y2, rect);
14903
- }
14904
-
14905
- this.#syncAngleCursor();
14906
- this.#syncPointPointCursors();
14907
- }
14908
-
14909
- #emitInput() {
14910
- this.dispatchEvent(
14911
- new CustomEvent("input", { bubbles: true, detail: this.value }),
14912
- );
14913
- }
14914
-
14915
- #emitChange() {
14916
- this.dispatchEvent(
14917
- new CustomEvent("change", { bubbles: true, detail: this.value }),
14918
- );
14919
- }
14920
-
14921
- #syncValueAttribute() {
14922
- this.setAttribute("value", JSON.stringify(this.value));
14923
- }
14924
-
14925
- #setupEventListeners() {
14926
- if (!this.#pointHandle) return;
14927
-
14928
- this.#pointHandle.addEventListener("input", (e) => {
14929
- e.stopPropagation();
14930
- if (e.detail?.color) {
14931
- this.setAttribute("color", e.detail.color);
14932
- this.#emitInput();
14933
- return;
14934
- }
14935
- if (!this.#isDragging && this.#hasSecondPoint) {
14936
- this.#prevBodyCursor = document.body.style.cursor;
14937
- }
14938
- this.#isDragging = true;
14939
- const px = e.detail?.px ?? this.#x / 100;
14940
- const py = e.detail?.py ?? this.#y / 100;
14941
- this.#x = Math.round(Math.max(0, Math.min(100, px * 100)));
14942
- this.#y = Math.round(Math.max(0, Math.min(100, py * 100)));
14943
- if (this.#pointTooltip && this.#type !== "color") {
14944
- this.#pointTooltip.setAttribute("text", this.#pointTipText);
14945
- this.#pointTooltip.setAttribute("show", "true");
14946
- this.#pointTooltip.showPopup?.();
14947
- }
14948
- this.#syncPositions();
14949
- if (this.#hasSecondPoint) {
14950
- document.body.style.cursor = this.#resizeCursorSvg(
14951
- this.#pointPointLineDeg(),
14952
- );
14953
- }
14954
- this.#emitInput();
14955
- });
14956
-
14957
- this.#pointHandle.addEventListener("change", (e) => {
14958
- e.stopPropagation();
14959
- if (e.detail?.color) {
14960
- this.setAttribute("color", e.detail.color);
14961
- this.#emitChange();
14962
- return;
14963
- }
14964
- const px = e.detail?.px ?? this.#x / 100;
14965
- const py = e.detail?.py ?? this.#y / 100;
14966
- this.#x = Math.round(Math.max(0, Math.min(100, px * 100)));
14967
- this.#y = Math.round(Math.max(0, Math.min(100, py * 100)));
14968
- if (this.#pointTooltip && this.#type !== "color")
14969
- this.#pointTooltip.removeAttribute("show");
14970
- if (this.#hasSecondPoint) {
14971
- document.body.style.cursor = this.#prevBodyCursor ?? "";
14972
- }
14973
- this.#syncPositions();
14974
- this.#syncValueAttribute();
14975
- this.#emitChange();
14976
- requestAnimationFrame(() => {
14977
- this.#isDragging = false;
14978
- });
14979
- });
14980
-
14981
- if (this.#angleHandle) {
14982
- this.#angleHandle.addEventListener("input", (e) => {
14983
- e.stopPropagation();
14984
- this.#isAngleDragging = true;
14985
- this.classList.add("fig-canvas-control-ring-active");
14986
- const container = this.#container;
14987
- if (!container) return;
14988
- const rect = container.getBoundingClientRect();
14989
- const cx = (this.#x / 100) * rect.width;
14990
- const cy = (this.#y / 100) * rect.height;
14991
- const hx = e.detail?.x ?? 0;
14992
- const hy = e.detail?.y ?? 0;
14993
- const hw = this.#angleHandle.offsetWidth / 2;
14994
- const hh = this.#angleHandle.offsetHeight / 2;
14995
- const dx = hx + hw - cx;
14996
- const dy = hy + hh - cy;
14997
- let angle = (Math.atan2(dy, dx) * 180) / Math.PI;
14998
- if (this.#shouldSnap(e.detail?.shiftKey)) {
14999
- angle = Math.round(angle / 15) * 15;
15000
- }
15001
- this.#angle = angle;
15002
-
15003
- let dist = Math.sqrt(dx * dx + dy * dy);
15004
- if (this.#shouldSnap(e.detail?.shiftKey)) {
15005
- const step = this.#radiusIsPercent ? 5 : 10;
15006
- if (this.#radiusIsPercent) {
15007
- let pct = (dist / rect.width) * 100;
15008
- pct = Math.round(pct / step) * step;
15009
- dist = (pct / 100) * rect.width;
15010
- } else {
15011
- dist = Math.round(dist / step) * step;
15012
- }
15013
- }
15014
- if (this.#radiusIsPercent) {
15015
- this.#radius = Math.max(0, (dist / rect.width) * 100);
15016
- } else {
15017
- this.#radius = Math.max(0, dist);
15018
- }
15019
-
15020
- if (this.#angleTooltip) {
15021
- this.#angleTooltip.setAttribute(
15022
- "text",
15023
- `Angle ${Math.round(this.#angle)}°`,
15024
- );
15025
- this.#angleTooltip.setAttribute("show", "true");
15026
- this.#angleTooltip.showPopup?.();
15027
- }
15028
- this.#syncPositions();
15029
- this.#emitInput();
15030
- });
15031
-
15032
- this.#angleHandle.addEventListener("change", (e) => {
15033
- e.stopPropagation();
15034
- this.classList.remove("fig-canvas-control-ring-active");
15035
- if (this.#angleTooltip) this.#angleTooltip.removeAttribute("show");
15036
- this.#syncPositions();
15037
- this.#syncValueAttribute();
15038
- this.#emitChange();
15039
- requestAnimationFrame(() => {
15040
- this.#isAngleDragging = false;
15041
- });
15042
- });
15043
-
15044
- this.#angleHandle.addEventListener("hitareadown", (e) => {
15045
- e.stopPropagation();
15046
- const origEvent = e.detail?.originalEvent;
15047
- if (!origEvent) return;
15048
- origEvent.preventDefault();
15049
- this.#isAngleDragging = true;
15050
- this.classList.add("fig-canvas-control-ring-active");
15051
- const container = this.#container;
15052
- if (!container) return;
15053
-
15054
- if (this.#angleTooltip) {
15055
- this.#angleTooltip.setAttribute("show", "true");
15056
- this.#angleTooltip.showPopup?.();
15057
- }
15058
-
15059
- const prevBodyCursor = document.body.style.cursor;
15060
- let lastCursorDeg = Math.round(this.#angle);
15061
- document.body.style.cursor = this.#rotateCursorSvg(lastCursorDeg);
15062
-
15063
- const onMove = (ev) => {
15064
- const rect = container.getBoundingClientRect();
15065
- const cx = (this.#x / 100) * rect.width;
15066
- const cy = (this.#y / 100) * rect.height;
15067
- const dx = ev.clientX - rect.left - cx;
15068
- const dy = ev.clientY - rect.top - cy;
15069
- let angle = (Math.atan2(dy, dx) * 180) / Math.PI;
15070
- if (this.#shouldSnap(ev.shiftKey)) {
15071
- angle = Math.round(angle / 15) * 15;
15072
- }
15073
- this.#angle = angle;
15074
- if (this.#angleTooltip)
15075
- this.#angleTooltip.setAttribute(
15076
- "text",
15077
- `Angle ${Math.round(angle)}°`,
15078
- );
15079
- this.#syncPositions();
15080
- const curDeg = Math.round(angle);
15081
- if (curDeg !== lastCursorDeg) {
15082
- lastCursorDeg = curDeg;
15083
- document.body.style.cursor = this.#rotateCursorSvg(curDeg);
15084
- }
15085
- this.#emitInput();
15086
- };
15087
-
15088
- const onUp = () => {
15089
- this.#isAngleDragging = false;
15090
- this.classList.remove("fig-canvas-control-ring-active");
15091
- document.body.style.cursor = prevBodyCursor;
15092
- if (this.#angleTooltip) this.#angleTooltip.removeAttribute("show");
15093
- this.#syncValueAttribute();
15094
- this.#emitChange();
15095
- window.removeEventListener("pointermove", onMove);
15096
- window.removeEventListener("pointerup", onUp);
15097
- };
15098
-
15099
- window.addEventListener("pointermove", onMove);
15100
- window.addEventListener("pointerup", onUp);
15101
- });
15102
- }
15103
-
15104
- if (this.#secondHandle) {
15105
- this.#secondHandle.addEventListener("input", (e) => {
15106
- e.stopPropagation();
15107
- if (!this.#isSecondDragging) {
15108
- this.#prevBodyCursor = document.body.style.cursor;
15109
- }
15110
- this.#isSecondDragging = true;
15111
- const px = e.detail?.px ?? this.#x2 / 100;
15112
- const py = e.detail?.py ?? this.#y2 / 100;
15113
- this.#x2 = Math.round(Math.max(0, Math.min(100, px * 100)));
15114
- this.#y2 = Math.round(Math.max(0, Math.min(100, py * 100)));
15115
- if (this.#secondTooltip) {
15116
- this.#secondTooltip.setAttribute("text", this.#secondTipText);
15117
- this.#secondTooltip.setAttribute("show", "true");
15118
- this.#secondTooltip.showPopup?.();
15119
- }
15120
- this.#syncPositions();
15121
- document.body.style.cursor = this.#resizeCursorSvg(
15122
- this.#pointPointLineDeg(),
15123
- );
15124
- this.#emitInput();
15125
- });
15126
-
15127
- this.#secondHandle.addEventListener("change", (e) => {
15128
- e.stopPropagation();
15129
- document.body.style.cursor = this.#prevBodyCursor ?? "";
15130
- if (this.#secondTooltip) this.#secondTooltip.removeAttribute("show");
15131
- this.#syncPositions();
15132
- this.#syncValueAttribute();
15133
- this.#emitChange();
15134
- requestAnimationFrame(() => {
15135
- this.#isSecondDragging = false;
15136
- });
15137
- });
15138
-
15139
- this.#setupPointPointHitArea(this.#pointHandle, true);
15140
- this.#setupPointPointHitArea(this.#secondHandle, false);
15141
- }
15142
- }
15143
-
15144
- #setupPointPointHitArea(handle, isFirst) {
15145
- if (!handle) return;
15146
- handle.addEventListener("hitareadown", (e) => {
15147
- e.stopPropagation();
15148
- const origEvent = e.detail?.originalEvent;
15149
- if (!origEvent) return;
15150
- origEvent.preventDefault();
15151
- this.#isDragging = true;
15152
- const container = this.#container;
15153
- if (!container) return;
15154
- const rect = container.getBoundingClientRect();
15155
-
15156
- const pivotX = isFirst ? this.#x2 : this.#x;
15157
- const pivotY = isFirst ? this.#y2 : this.#y;
15158
- const movingX = isFirst ? this.#x : this.#x2;
15159
- const movingY = isFirst ? this.#y : this.#y2;
15160
- const pcx = (pivotX / 100) * rect.width;
15161
- const pcy = (pivotY / 100) * rect.height;
15162
- const mcx = (movingX / 100) * rect.width;
15163
- const mcy = (movingY / 100) * rect.height;
15164
- const fixedLen = Math.sqrt((mcx - pcx) ** 2 + (mcy - pcy) ** 2);
15165
-
15166
- const tooltip = isFirst ? this.#pointTooltip : this.#secondTooltip;
15167
- if (tooltip) {
15168
- tooltip.setAttribute("show", "true");
15169
- tooltip.showPopup?.();
15170
- }
15171
-
15172
- const prevBodyCursor = document.body.style.cursor;
15173
- const initDeg = this.#pointPointLineDeg();
15174
- let lastCursorDeg = Math.round(isFirst ? initDeg + 180 : initDeg);
15175
- document.body.style.cursor = this.#rotateCursorSvg(lastCursorDeg);
15176
-
15177
- const onMove = (ev) => {
15178
- const r = container.getBoundingClientRect();
15179
- const px = (pivotX / 100) * r.width;
15180
- const py = (pivotY / 100) * r.height;
15181
- const dx = ev.clientX - r.left - px;
15182
- const dy = ev.clientY - r.top - py;
15183
- let angle = Math.atan2(dy, dx);
15184
- if (this.#shouldSnap(ev.shiftKey)) {
15185
- const snapDeg = Math.round((angle * 180) / Math.PI / 15) * 15;
15186
- angle = (snapDeg * Math.PI) / 180;
15187
- }
15188
- const nx = px + fixedLen * Math.cos(angle);
15189
- const ny = py + fixedLen * Math.sin(angle);
15190
- const newPctX = Math.max(0, Math.min(100, (nx / r.width) * 100));
15191
- const newPctY = Math.max(0, Math.min(100, (ny / r.height) * 100));
15192
- if (isFirst) {
15193
- this.#x = newPctX;
15194
- this.#y = newPctY;
15195
- } else {
15196
- this.#x2 = newPctX;
15197
- this.#y2 = newPctY;
15198
- }
15199
- this.#syncPositions();
15200
- const curDeg = Math.round(
15201
- isFirst ? this.#pointPointLineDeg() + 180 : this.#pointPointLineDeg(),
15202
- );
15203
- if (curDeg !== lastCursorDeg) {
15204
- lastCursorDeg = curDeg;
15205
- document.body.style.cursor = this.#rotateCursorSvg(curDeg);
15206
- }
15207
- this.#emitInput();
15208
- };
15209
-
15210
- const onUp = () => {
15211
- this.#isDragging = false;
15212
- document.body.style.cursor = prevBodyCursor;
15213
- if (tooltip) tooltip.removeAttribute("show");
15214
- this.#syncValueAttribute();
15215
- this.#emitChange();
15216
- window.removeEventListener("pointermove", onMove);
15217
- window.removeEventListener("pointerup", onUp);
15218
- };
15219
-
15220
- window.addEventListener("pointermove", onMove);
15221
- window.addEventListener("pointerup", onUp);
15222
- });
15223
- }
15224
-
15225
- #setupRadiusDrag(circle) {
15226
- if (!circle) return;
15227
- circle.addEventListener("pointermove", (e) => {
15228
- if (this.#isRadiusDragging) return;
15229
- const container = this.#container;
15230
- if (!container) return;
15231
- const rect = container.getBoundingClientRect();
15232
- const cx = (this.#x / 100) * rect.width;
15233
- const cy = (this.#y / 100) * rect.height;
15234
- const deg =
15235
- (Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx) *
15236
- 180) /
15237
- Math.PI;
15238
- circle.style.cursor = this.#resizeCursorSvg(deg);
15239
- });
15240
- const onDown = (e) => {
15241
- if (this.hasAttribute("disabled")) return;
15242
- e.preventDefault();
15243
- e.stopPropagation();
15244
- this.#isRadiusDragging = true;
15245
- this.classList.add("fig-canvas-control-ring-active");
15246
- const container = this.#container;
15247
- if (!container) return;
15248
-
15249
- if (this.#radiusTooltip) {
15250
- this.#radiusTooltip.setAttribute("show", "true");
15251
- this.#radiusTooltip.showPopup?.();
15252
- }
15253
-
15254
- const prevBodyCursor = document.body.style.cursor;
15255
- circle.style.pointerEvents = "none";
15256
- const rect0 = container.getBoundingClientRect();
15257
- const cx0 = (this.#x / 100) * rect0.width;
15258
- const cy0 = (this.#y / 100) * rect0.height;
15259
- const initDeg =
15260
- (Math.atan2(e.clientY - rect0.top - cy0, e.clientX - rect0.left - cx0) *
15261
- 180) /
15262
- Math.PI;
15263
- let lastCursorDeg = Math.round(initDeg);
15264
- document.body.style.cursor = this.#resizeCursorSvg(lastCursorDeg);
15265
-
15266
- const onMove = (ev) => {
15267
- const rect = container.getBoundingClientRect();
15268
- const cx = (this.#x / 100) * rect.width;
15269
- const cy = (this.#y / 100) * rect.height;
15270
- const dx = ev.clientX - rect.left - cx;
15271
- const dy = ev.clientY - rect.top - cy;
15272
- const curDeg = Math.round((Math.atan2(dy, dx) * 180) / Math.PI);
15273
- if (curDeg !== lastCursorDeg) {
15274
- lastCursorDeg = curDeg;
15275
- document.body.style.cursor = this.#resizeCursorSvg(curDeg);
15276
- }
15277
- let dist = Math.sqrt(dx * dx + dy * dy);
15278
- if (this.#shouldSnap(ev.shiftKey)) {
15279
- const step = this.#radiusIsPercent ? 5 : 10;
15280
- if (this.#radiusIsPercent) {
15281
- let pct = (dist / rect.width) * 100;
15282
- pct = Math.round(pct / step) * step;
15283
- dist = (pct / 100) * rect.width;
15284
- } else {
15285
- dist = Math.round(dist / step) * step;
15286
- }
15287
- }
15288
- if (this.#radiusIsPercent) {
15289
- this.#radius = Math.max(0, (dist / rect.width) * 100);
15290
- } else {
15291
- this.#radius = Math.max(0, dist);
15292
- }
15293
- if (this.#radiusTooltip)
15294
- this.#radiusTooltip.setAttribute("text", this.#formatRadius());
15295
- this.#syncPositions();
15296
- this.#emitInput();
15297
- };
15298
-
15299
- const onUp = () => {
15300
- this.#isRadiusDragging = false;
15301
- this.classList.remove("fig-canvas-control-ring-active");
15302
- circle.style.pointerEvents = "";
15303
- document.body.style.cursor = prevBodyCursor;
15304
- if (this.#radiusTooltip) this.#radiusTooltip.removeAttribute("show");
15305
- this.#syncValueAttribute();
15306
- this.#emitChange();
15307
- window.removeEventListener("pointermove", onMove);
15308
- window.removeEventListener("pointerup", onUp);
15309
- };
15310
-
15311
- window.addEventListener("pointermove", onMove);
15312
- window.addEventListener("pointerup", onUp);
15313
- };
15314
- circle.addEventListener("pointerdown", onDown);
15315
- this._radiusDragCleanup = () =>
15316
- circle.removeEventListener("pointerdown", onDown);
15317
- }
15318
-
15319
- #teardownRadiusDrag() {
15320
- if (this._radiusDragCleanup) {
15321
- this._radiusDragCleanup();
15322
- this._radiusDragCleanup = null;
15323
- }
15324
- }
15325
- }
15326
- customElements.define("fig-canvas-control", FigCanvasControl);
15327
-
15328
13679
  /* Handle */
15329
13680
  class FigHandle extends HTMLElement {
15330
13681
  static observedAttributes = [