@rogieking/figui3 1.9.3 → 1.9.5
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/AUDIT.md +183 -0
- package/components.css +165 -92
- package/example.html +134 -1
- package/fig.js +425 -48
- package/package.json +1 -1
package/AUDIT.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Code Audit Report for fig.js
|
|
2
|
+
|
|
3
|
+
## Critical Bugs
|
|
4
|
+
|
|
5
|
+
### 1. **Infinite Recursion in FigButton.type getter (Line 58-59)**
|
|
6
|
+
**Location:** `FigButton` class, lines 58-59
|
|
7
|
+
**Issue:** The getter returns `this.type`, which calls itself infinitely
|
|
8
|
+
```javascript
|
|
9
|
+
get type() {
|
|
10
|
+
return this.type; // ❌ Infinite recursion!
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
**Fix:** Should return `this.getAttribute("type")` or use a private field
|
|
14
|
+
```javascript
|
|
15
|
+
get type() {
|
|
16
|
+
return this.getAttribute("type") || "button";
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. **Impossible Logic Condition (Multiple locations)**
|
|
21
|
+
**Locations:**
|
|
22
|
+
- Line 103 (FigButton)
|
|
23
|
+
- Line 1043 (FigSlider)
|
|
24
|
+
- Line 1304 (FigInputText)
|
|
25
|
+
|
|
26
|
+
**Issue:** The condition `newValue === undefined && newValue !== null` is logically impossible. A value cannot be both `undefined` and not `null` simultaneously.
|
|
27
|
+
|
|
28
|
+
**Current code:**
|
|
29
|
+
```javascript
|
|
30
|
+
this.disabled = this.input.disabled =
|
|
31
|
+
newValue === "true" ||
|
|
32
|
+
(newValue === undefined && newValue !== null);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Fix:** Should be:
|
|
36
|
+
```javascript
|
|
37
|
+
this.disabled = this.input.disabled =
|
|
38
|
+
newValue !== null && newValue !== "false";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Documentation Issues
|
|
42
|
+
|
|
43
|
+
### 3. **Missing Documentation for Utility Functions**
|
|
44
|
+
**Location:** Lines 1-6
|
|
45
|
+
**Issue:** `figUniqueId()` and `figSupportsPopover()` lack JSDoc comments
|
|
46
|
+
|
|
47
|
+
**Fix:** Add documentation:
|
|
48
|
+
```javascript
|
|
49
|
+
/**
|
|
50
|
+
* Generates a unique ID string using timestamp and random values
|
|
51
|
+
* @returns {string} A unique identifier
|
|
52
|
+
*/
|
|
53
|
+
function figUniqueId() {
|
|
54
|
+
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Checks if the browser supports the native popover API
|
|
59
|
+
* @returns {boolean} True if popover is supported
|
|
60
|
+
*/
|
|
61
|
+
function figSupportsPopover() {
|
|
62
|
+
return HTMLElement.prototype.hasOwnProperty("popover");
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 4. **Incomplete Documentation for FigButton**
|
|
67
|
+
**Location:** Line 10
|
|
68
|
+
**Issue:** Documentation mentions "button", "toggle", "submit" but code also supports "link" type (line 81)
|
|
69
|
+
|
|
70
|
+
**Fix:** Update documentation:
|
|
71
|
+
```javascript
|
|
72
|
+
* @attr {string} type - The button type: "button" (default), "toggle", "submit", or "link"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 5. **Missing Documentation for FigButton Attributes**
|
|
76
|
+
**Location:** FigButton class
|
|
77
|
+
**Issue:** Missing documentation for `href` and `target` attributes used in link type
|
|
78
|
+
|
|
79
|
+
**Fix:** Add to JSDoc:
|
|
80
|
+
```javascript
|
|
81
|
+
* @attr {string} href - URL for link type buttons
|
|
82
|
+
* @attr {string} target - Target window for link type buttons
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 6. **Missing Documentation for FigInputNumber**
|
|
86
|
+
**Location:** Line 1358
|
|
87
|
+
**Issue:** Missing `composed` property in CustomEvent documentation (though it's used in code)
|
|
88
|
+
|
|
89
|
+
## Potential Issues
|
|
90
|
+
|
|
91
|
+
### 7. **Unused Function**
|
|
92
|
+
**Location:** Line 4-6
|
|
93
|
+
**Issue:** `figSupportsPopover()` is defined but never used in the codebase
|
|
94
|
+
|
|
95
|
+
**Recommendation:** Either use it or remove it
|
|
96
|
+
|
|
97
|
+
### 8. **Inconsistent Disabled Attribute Handling**
|
|
98
|
+
**Location:** Multiple components
|
|
99
|
+
**Issue:** Different components handle disabled attributes slightly differently:
|
|
100
|
+
- Some check `newValue === "true" || (newValue === undefined && newValue !== null)`
|
|
101
|
+
- Others check `newValue !== null && newValue !== "false"`
|
|
102
|
+
|
|
103
|
+
**Recommendation:** Standardize on one approach (preferably the second, which is correct)
|
|
104
|
+
|
|
105
|
+
### 9. **Missing Error Handling**
|
|
106
|
+
**Location:** Multiple locations
|
|
107
|
+
**Issue:** No error handling for:
|
|
108
|
+
- `querySelector` calls that might return null
|
|
109
|
+
- `getAttribute` calls that might return unexpected values
|
|
110
|
+
- Event listener setup failures
|
|
111
|
+
|
|
112
|
+
**Example:** Line 54 - `this.button` might be null if querySelector fails
|
|
113
|
+
|
|
114
|
+
### 10. **Potential Memory Leak**
|
|
115
|
+
**Location:** FigTooltip, line 310
|
|
116
|
+
**Issue:** Event listener added in `destroy()` method:
|
|
117
|
+
```javascript
|
|
118
|
+
destroy() {
|
|
119
|
+
if (this.popup) {
|
|
120
|
+
this.popup.remove();
|
|
121
|
+
}
|
|
122
|
+
document.body.addEventListener("click", this.hidePopupOutsideClick); // ❌ Added but never removed
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 11. **Inconsistent Value Type Handling**
|
|
127
|
+
**Location:** FigInputNumber, line 1393-1394
|
|
128
|
+
**Issue:** Value is converted to Number but can be empty string:
|
|
129
|
+
```javascript
|
|
130
|
+
this.value = valueAttr !== null && valueAttr !== "" ? Number(valueAttr) : "";
|
|
131
|
+
```
|
|
132
|
+
This creates inconsistent types (Number vs String)
|
|
133
|
+
|
|
134
|
+
### 12. **Missing Input Validation**
|
|
135
|
+
**Location:** FigInputNumber, line 1551
|
|
136
|
+
**Issue:** No validation that `numericValue` is a valid number before division:
|
|
137
|
+
```javascript
|
|
138
|
+
this.value = Number(numericValue) / (this.transform || 1);
|
|
139
|
+
```
|
|
140
|
+
If `numericValue` is empty or invalid, this could result in NaN
|
|
141
|
+
|
|
142
|
+
## Code Quality Issues
|
|
143
|
+
|
|
144
|
+
### 13. **Magic Numbers**
|
|
145
|
+
**Location:** Multiple locations
|
|
146
|
+
**Issue:** Hard-coded values without explanation:
|
|
147
|
+
- Line 246: `delay = 500` (default delay)
|
|
148
|
+
- Line 1617: `precision = 2` (default precision)
|
|
149
|
+
- Line 1635: `Math.round(num * 100) / 100` (rounding logic)
|
|
150
|
+
|
|
151
|
+
**Recommendation:** Extract to named constants or document them
|
|
152
|
+
|
|
153
|
+
### 14. **Inconsistent Naming**
|
|
154
|
+
**Location:** FigSlider
|
|
155
|
+
**Issue:** Method named `#handleTextInput` but now works with `figInputNumber`
|
|
156
|
+
|
|
157
|
+
**Recommendation:** Rename to `#handleNumberInput` for clarity
|
|
158
|
+
|
|
159
|
+
### 15. **Missing Type Checks**
|
|
160
|
+
**Location:** Multiple locations
|
|
161
|
+
**Issue:** No type validation for attributes that should be numbers:
|
|
162
|
+
- `min`, `max`, `step`, `transform` attributes
|
|
163
|
+
|
|
164
|
+
**Recommendation:** Add validation or use `Number()` with NaN checks
|
|
165
|
+
|
|
166
|
+
## Summary
|
|
167
|
+
|
|
168
|
+
**Critical Bugs:** 2
|
|
169
|
+
**Documentation Issues:** 4
|
|
170
|
+
**Potential Issues:** 6
|
|
171
|
+
**Code Quality Issues:** 3
|
|
172
|
+
|
|
173
|
+
**Total Issues Found:** 15
|
|
174
|
+
|
|
175
|
+
## Priority Fixes
|
|
176
|
+
|
|
177
|
+
1. **HIGH:** Fix infinite recursion in FigButton.type getter
|
|
178
|
+
2. **HIGH:** Fix impossible logic condition in disabled attribute handling
|
|
179
|
+
3. **MEDIUM:** Add missing documentation
|
|
180
|
+
4. **MEDIUM:** Fix memory leak in FigTooltip.destroy()
|
|
181
|
+
5. **LOW:** Standardize disabled attribute handling
|
|
182
|
+
6. **LOW:** Add input validation
|
|
183
|
+
|
package/components.css
CHANGED
|
@@ -513,7 +513,8 @@ input[type="password"],
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
fig-input-text
|
|
516
|
+
fig-input-text,
|
|
517
|
+
fig-input-number {
|
|
517
518
|
&:has([slot="append"]) input[type="number"] {
|
|
518
519
|
&::-webkit-outer-spin-button,
|
|
519
520
|
&::-webkit-inner-spin-button {
|
|
@@ -572,109 +573,170 @@ input[type="text"][list] {
|
|
|
572
573
|
}
|
|
573
574
|
}
|
|
574
575
|
|
|
575
|
-
/* Not enough support for this yet
|
|
576
576
|
@supports (appearance: base-select) {
|
|
577
|
-
|
|
578
|
-
|
|
577
|
+
fig-dropdown[variant="neue"] {
|
|
578
|
+
select {
|
|
579
|
+
appearance: base-select;
|
|
580
|
+
--option-height: 1.5rem;
|
|
579
581
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
display: block;
|
|
583
|
-
width: 1rem;
|
|
584
|
-
height: 1rem;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
option {
|
|
588
|
-
display: flex;
|
|
589
|
-
gap: var(--spacer-1);
|
|
590
|
-
padding: 0 var(--spacer-4) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
|
|
591
|
-
font-weight: var(--body-medium-fontWeight);
|
|
592
|
-
color: var(--figma-color-text-menu);
|
|
593
|
-
position: relative;
|
|
594
|
-
&[hidden] {
|
|
595
|
-
display: none;
|
|
596
|
-
}
|
|
597
|
-
&:not(:checked):before {
|
|
598
|
-
content: "";
|
|
582
|
+
option::checkmark {
|
|
583
|
+
content: var(--icon-checkmark);
|
|
599
584
|
display: block;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
border-radius: var(--radius-medium);
|
|
603
|
-
z-index: -1;
|
|
604
|
-
background-color: transparent;
|
|
585
|
+
width: 1rem;
|
|
586
|
+
height: 1rem;
|
|
605
587
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
588
|
+
|
|
589
|
+
option {
|
|
590
|
+
display: flex;
|
|
591
|
+
gap: var(--spacer-1);
|
|
592
|
+
padding: 0 var(--spacer-4) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
|
|
593
|
+
font-weight: var(--body-medium-fontWeight);
|
|
594
|
+
color: var(--figma-color-text-menu);
|
|
595
|
+
position: relative;
|
|
596
|
+
&[hidden] {
|
|
597
|
+
display: none;
|
|
598
|
+
}
|
|
599
|
+
&:not(:checked):before {
|
|
600
|
+
content: "";
|
|
601
|
+
display: block;
|
|
602
|
+
position: absolute;
|
|
603
|
+
inset: 0 var(--spacer-2);
|
|
604
|
+
border-radius: var(--radius-medium);
|
|
605
|
+
z-index: -1;
|
|
610
606
|
background-color: transparent;
|
|
611
|
-
|
|
607
|
+
}
|
|
608
|
+
&:not(:disabled) {
|
|
609
|
+
&:hover,
|
|
610
|
+
&:active,
|
|
611
|
+
&:focus {
|
|
612
|
+
background-color: transparent;
|
|
613
|
+
outline: 0;
|
|
614
|
+
&:before {
|
|
615
|
+
background-color: var(--figma-color-bg-menu-hover);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
optgroup {
|
|
622
|
+
color: var(--figma-color-text-menu-secondary);
|
|
623
|
+
text-align: left;
|
|
624
|
+
position: relative;
|
|
625
|
+
padding: 0 var(--spacer-1) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
|
|
626
|
+
font-weight: var(--body-medium-fontWeight);
|
|
627
|
+
&::-internal-optgroup-label {
|
|
628
|
+
display: none;
|
|
629
|
+
}
|
|
630
|
+
legend {
|
|
631
|
+
padding: var(--spacer-1, 0.3rem) var(--spacer-1, 1rem);
|
|
632
|
+
}
|
|
633
|
+
option {
|
|
634
|
+
margin: 0 calc(var(--spacer-1) * -1);
|
|
635
|
+
margin-left: calc((var(--spacer-1) * 2 + var(--spacer-1)) * -1);
|
|
636
|
+
}
|
|
637
|
+
&:not(:first-child) {
|
|
638
|
+
margin-top: var(--spacer-2);
|
|
639
|
+
padding-top: var(--spacer-2);
|
|
640
|
+
|
|
612
641
|
&:before {
|
|
613
|
-
|
|
642
|
+
content: "";
|
|
643
|
+
display: block;
|
|
644
|
+
position: absolute;
|
|
645
|
+
left: 0;
|
|
646
|
+
right: 0;
|
|
647
|
+
top: 1px;
|
|
648
|
+
height: 1px;
|
|
649
|
+
background-color: var(--figma-color-border-menu);
|
|
650
|
+
margin-bottom: var(--spacer-2);
|
|
614
651
|
}
|
|
615
652
|
}
|
|
616
653
|
}
|
|
654
|
+
option[hidden="true"]:first-child + optgroup {
|
|
655
|
+
margin-top: 0;
|
|
656
|
+
padding-top: 0;
|
|
657
|
+
&:before {
|
|
658
|
+
display: none;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
::picker-icon {
|
|
663
|
+
display: none;
|
|
617
664
|
}
|
|
618
665
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
position:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
666
|
+
::picker(select) {
|
|
667
|
+
position-area: auto;
|
|
668
|
+
align-self: auto;
|
|
669
|
+
position-try-fallbacks: none;
|
|
670
|
+
max-block-size: 100vh;
|
|
671
|
+
appearance: base-select;
|
|
672
|
+
scrollbar-width: thin;
|
|
673
|
+
outline: 0;
|
|
674
|
+
scrollbar-color: var(--figma-color-text-menu-tertiary)
|
|
675
|
+
var(--figma-color-bg-menu);
|
|
676
|
+
border-radius: var(--radius-large);
|
|
677
|
+
border: 0;
|
|
678
|
+
background-color: var(--figma-color-bg-menu);
|
|
679
|
+
padding: var(--spacer-2) 0;
|
|
680
|
+
box-shadow: var(--figma-elevation-400-menu-panel);
|
|
681
|
+
}
|
|
682
|
+
select {
|
|
683
|
+
&:has(:nth-child(1):checked) {
|
|
684
|
+
&::picker(select) {
|
|
685
|
+
top: calc(var(--option-height) * -1 - var(--spacer-2));
|
|
686
|
+
}
|
|
627
687
|
}
|
|
628
|
-
|
|
629
|
-
|
|
688
|
+
|
|
689
|
+
&:has(:nth-child(2):checked) {
|
|
690
|
+
&::picker(select) {
|
|
691
|
+
top: calc(var(--option-height) * -2 - var(--spacer-2));
|
|
692
|
+
}
|
|
630
693
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
694
|
+
|
|
695
|
+
&:has(:nth-child(3):checked) {
|
|
696
|
+
&::picker(select) {
|
|
697
|
+
top: calc(var(--option-height) * -3 - var(--spacer-2));
|
|
698
|
+
}
|
|
634
699
|
}
|
|
635
|
-
&:not(:first-child) {
|
|
636
|
-
margin-top: var(--spacer-2);
|
|
637
|
-
padding-top: var(--spacer-2);
|
|
638
700
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
position: absolute;
|
|
643
|
-
left: 0;
|
|
644
|
-
right: 0;
|
|
645
|
-
top: 1px;
|
|
646
|
-
height: 1px;
|
|
647
|
-
background-color: var(--figma-color-border-menu);
|
|
648
|
-
margin-bottom: var(--spacer-2);
|
|
701
|
+
&:has(:nth-child(4):checked) {
|
|
702
|
+
&::picker(select) {
|
|
703
|
+
top: calc(var(--option-height) * -4 - var(--spacer-2));
|
|
649
704
|
}
|
|
650
705
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
706
|
+
&:has(:nth-child(5):checked) {
|
|
707
|
+
&::picker(select) {
|
|
708
|
+
top: calc(var(--option-height) * -5 - var(--spacer-2));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
&:has(:nth-child(6):checked) {
|
|
712
|
+
&::picker(select) {
|
|
713
|
+
top: calc(var(--option-height) * -6 - var(--spacer-2));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
&:has(:nth-child(7):checked) {
|
|
717
|
+
&::picker(select) {
|
|
718
|
+
top: calc(var(--option-height) * -7 - var(--spacer-2));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
&:has(:nth-child(8):checked) {
|
|
722
|
+
&::picker(select) {
|
|
723
|
+
top: calc(var(--option-height) * -8 - var(--spacer-2));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
&:has(:nth-child(9):checked) {
|
|
727
|
+
&::picker(select) {
|
|
728
|
+
top: calc(var(--option-height) * -9 - var(--spacer-2));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
&:has(:nth-child(10):checked) {
|
|
732
|
+
&::picker(select) {
|
|
733
|
+
top: calc(var(--option-height) * -10 - var(--spacer-2));
|
|
734
|
+
}
|
|
657
735
|
}
|
|
658
736
|
}
|
|
659
737
|
}
|
|
660
|
-
::picker-icon {
|
|
661
|
-
display: none;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
::picker(select) {
|
|
665
|
-
appearance: base-select;
|
|
666
|
-
scrollbar-width: thin;
|
|
667
|
-
outline: 0;
|
|
668
|
-
scrollbar-color: var(--figma-color-text-menu-tertiary)
|
|
669
|
-
var(--figma-color-bg-menu);
|
|
670
|
-
border-radius: var(--radius-large);
|
|
671
|
-
border: 0;
|
|
672
|
-
background-color: var(--figma-color-bg-menu);
|
|
673
|
-
padding: var(--spacer-2) 0;
|
|
674
|
-
box-shadow: var(--figma-elevation-400-menu-panel);
|
|
675
|
-
}
|
|
676
738
|
}
|
|
677
|
-
|
|
739
|
+
|
|
678
740
|
input[type="text"][list]:hover,
|
|
679
741
|
input[type="text"][list]:active,
|
|
680
742
|
input[type="text"][list]:focus,
|
|
@@ -1913,7 +1975,8 @@ fig-slider {
|
|
|
1913
1975
|
box-shadow: none;
|
|
1914
1976
|
background-color: var(--figma-color-bg-tertiary);
|
|
1915
1977
|
}
|
|
1916
|
-
fig-input-text
|
|
1978
|
+
fig-input-text,
|
|
1979
|
+
fig-input-number {
|
|
1917
1980
|
border-top-left-radius: 0;
|
|
1918
1981
|
border-bottom-left-radius: 0;
|
|
1919
1982
|
border-left: 1px solid var(--figma-color-bg);
|
|
@@ -1921,7 +1984,8 @@ fig-slider {
|
|
|
1921
1984
|
|
|
1922
1985
|
&:hover,
|
|
1923
1986
|
&:focus-within {
|
|
1924
|
-
fig-input-text
|
|
1987
|
+
fig-input-text,
|
|
1988
|
+
fig-input-number {
|
|
1925
1989
|
height: auto;
|
|
1926
1990
|
}
|
|
1927
1991
|
}
|
|
@@ -2235,7 +2299,8 @@ hstack,
|
|
|
2235
2299
|
flex-wrap: nowrap;
|
|
2236
2300
|
}
|
|
2237
2301
|
|
|
2238
|
-
fig-input-text
|
|
2302
|
+
fig-input-text,
|
|
2303
|
+
fig-input-number {
|
|
2239
2304
|
background-color: var(--figma-color-bg-secondary);
|
|
2240
2305
|
border: 0;
|
|
2241
2306
|
border-radius: var(--radius-medium);
|
|
@@ -2322,8 +2387,8 @@ fig-input-text {
|
|
|
2322
2387
|
|
|
2323
2388
|
fig-input-color {
|
|
2324
2389
|
& fig-input-text:not([type]),
|
|
2325
|
-
& fig-input-text[type="text"]
|
|
2326
|
-
|
|
2390
|
+
& fig-input-text[type="text"],
|
|
2391
|
+
& fig-input-number {
|
|
2327
2392
|
display: inline-flex;
|
|
2328
2393
|
flex-direction: column;
|
|
2329
2394
|
align-items: stretch;
|
|
@@ -2334,10 +2399,17 @@ fig-input-color {
|
|
|
2334
2399
|
}
|
|
2335
2400
|
}
|
|
2336
2401
|
|
|
2337
|
-
& fig-input-text[type="number"]
|
|
2338
|
-
|
|
2402
|
+
& fig-input-text[type="number"],
|
|
2403
|
+
& fig-input-number {
|
|
2404
|
+
width: 3rem;
|
|
2339
2405
|
display: inline-flex;
|
|
2340
2406
|
}
|
|
2407
|
+
& > * {
|
|
2408
|
+
flex: 1;
|
|
2409
|
+
fig-input-text {
|
|
2410
|
+
flex: 1;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2341
2413
|
}
|
|
2342
2414
|
|
|
2343
2415
|
fig-slider {
|
|
@@ -2347,8 +2419,9 @@ fig-slider {
|
|
|
2347
2419
|
flex-grow: 1;
|
|
2348
2420
|
}
|
|
2349
2421
|
|
|
2350
|
-
& fig-input-text[type="number"]
|
|
2351
|
-
|
|
2422
|
+
& fig-input-text[type="number"],
|
|
2423
|
+
& fig-input-number {
|
|
2424
|
+
width: 3rem;
|
|
2352
2425
|
}
|
|
2353
2426
|
}
|
|
2354
2427
|
|
package/example.html
CHANGED
|
@@ -27,15 +27,43 @@
|
|
|
27
27
|
<fig-spinner></fig-spinner>
|
|
28
28
|
</fig-header>
|
|
29
29
|
|
|
30
|
+
<br /><br /><br /><br /><br /><br /><br /><br />
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
30
34
|
|
|
31
35
|
<fig-field direction="horizontal">
|
|
32
36
|
<label>Position</label>
|
|
33
|
-
<fig-dropdown value="outside"
|
|
37
|
+
<fig-dropdown value="outside"
|
|
38
|
+
variant="neue">
|
|
34
39
|
<option value="inside">Inside</option>
|
|
35
40
|
<option value="center">Center</option>
|
|
36
41
|
<option value="outside">Outside</option>
|
|
37
42
|
</fig-dropdown>
|
|
38
43
|
</fig-field>
|
|
44
|
+
<fig-field direction="horizontal"
|
|
45
|
+
style="width: 320px;">
|
|
46
|
+
<label>Color</label>
|
|
47
|
+
<fig-input-color alpha="true"
|
|
48
|
+
full
|
|
49
|
+
text="true"
|
|
50
|
+
value="#000000"></fig-input-color>
|
|
51
|
+
</fig-field>
|
|
52
|
+
<fig-field direction="horizontal"
|
|
53
|
+
style="width: 320px;">
|
|
54
|
+
<label>Opacity</label>
|
|
55
|
+
<fig-slider alpha="true"
|
|
56
|
+
text="true"
|
|
57
|
+
value="50"
|
|
58
|
+
full
|
|
59
|
+
variant="neue"
|
|
60
|
+
min="0"
|
|
61
|
+
max="100"
|
|
62
|
+
step="1"
|
|
63
|
+
units="%"></fig-slider>
|
|
64
|
+
</fig-field>
|
|
65
|
+
|
|
66
|
+
<br /><br /><br /><br /><br /><br /><br /><br />
|
|
39
67
|
|
|
40
68
|
|
|
41
69
|
|
|
@@ -675,6 +703,111 @@
|
|
|
675
703
|
<span slot="prepend">X</span>
|
|
676
704
|
</fig-input-text>
|
|
677
705
|
</fig-field>
|
|
706
|
+
<fig-header>
|
|
707
|
+
<h2>Number input</h2>
|
|
708
|
+
</fig-header>
|
|
709
|
+
<fig-field>
|
|
710
|
+
<label>Basic number input</label>
|
|
711
|
+
<fig-input-number value="100"></fig-input-number>
|
|
712
|
+
</fig-field>
|
|
713
|
+
<fig-field>
|
|
714
|
+
<label>With units (percentage)</label>
|
|
715
|
+
<fig-input-number value="100"
|
|
716
|
+
units="%"></fig-input-number>
|
|
717
|
+
</fig-field>
|
|
718
|
+
<fig-field>
|
|
719
|
+
<label>With units (degrees)</label>
|
|
720
|
+
<fig-input-number value="45"
|
|
721
|
+
units="°"
|
|
722
|
+
min="0"
|
|
723
|
+
max="360"
|
|
724
|
+
step="1"></fig-input-number>
|
|
725
|
+
</fig-field>
|
|
726
|
+
<fig-field>
|
|
727
|
+
<label>With prefix unit (currency)</label>
|
|
728
|
+
<fig-input-number value="50"
|
|
729
|
+
units="$"
|
|
730
|
+
unit-position="prefix"
|
|
731
|
+
min="0"
|
|
732
|
+
step="0.01"></fig-input-number>
|
|
733
|
+
</fig-field>
|
|
734
|
+
<fig-field>
|
|
735
|
+
<label>With transform (like slider)</label>
|
|
736
|
+
<fig-input-number value="0.5"
|
|
737
|
+
units="%"
|
|
738
|
+
transform="100"
|
|
739
|
+
min="0"
|
|
740
|
+
max="1"
|
|
741
|
+
step="0.01"></fig-input-number>
|
|
742
|
+
</fig-field>
|
|
743
|
+
<fig-field>
|
|
744
|
+
<label>With min/max constraints</label>
|
|
745
|
+
<fig-input-number value="25"
|
|
746
|
+
units="px"
|
|
747
|
+
min="0"
|
|
748
|
+
max="1000"
|
|
749
|
+
step="1"></fig-input-number>
|
|
750
|
+
</fig-field>
|
|
751
|
+
<fig-field>
|
|
752
|
+
<label>With placeholder</label>
|
|
753
|
+
<fig-input-number placeholder="Enter value"
|
|
754
|
+
units="%"
|
|
755
|
+
min="0"
|
|
756
|
+
max="100"></fig-input-number>
|
|
757
|
+
</fig-field>
|
|
758
|
+
<fig-field>
|
|
759
|
+
<label>Disabled</label>
|
|
760
|
+
<fig-input-number value="75"
|
|
761
|
+
units="%"
|
|
762
|
+
disabled></fig-input-number>
|
|
763
|
+
</fig-field>
|
|
764
|
+
<fig-field>
|
|
765
|
+
<label>With name attribute</label>
|
|
766
|
+
<fig-input-number value="42"
|
|
767
|
+
units="px"
|
|
768
|
+
name="width"></fig-input-number>
|
|
769
|
+
</fig-field>
|
|
770
|
+
<fig-field>
|
|
771
|
+
<label>Small step (decimals)</label>
|
|
772
|
+
<fig-input-number value="1.5"
|
|
773
|
+
units="rem"
|
|
774
|
+
min="0"
|
|
775
|
+
max="10"
|
|
776
|
+
step="0.1"></fig-input-number>
|
|
777
|
+
</fig-field>
|
|
778
|
+
<fig-field>
|
|
779
|
+
<label>Large range with transform</label>
|
|
780
|
+
<fig-input-number value="0.75"
|
|
781
|
+
units="%"
|
|
782
|
+
transform="100"
|
|
783
|
+
min="0"
|
|
784
|
+
max="1"
|
|
785
|
+
step="0.01"></fig-input-number>
|
|
786
|
+
</fig-field>
|
|
787
|
+
<fig-field>
|
|
788
|
+
<label>Negative values allowed</label>
|
|
789
|
+
<fig-input-number value="-10"
|
|
790
|
+
units="px"
|
|
791
|
+
min="-100"
|
|
792
|
+
max="100"
|
|
793
|
+
step="1"></fig-input-number>
|
|
794
|
+
</fig-field>
|
|
795
|
+
<fig-field>
|
|
796
|
+
<label>With append slot</label>
|
|
797
|
+
<fig-input-number value="90"
|
|
798
|
+
units="°"
|
|
799
|
+
min="0"
|
|
800
|
+
max="360">
|
|
801
|
+
<span slot="append">deg</span>
|
|
802
|
+
</fig-input-number>
|
|
803
|
+
</fig-field>
|
|
804
|
+
<fig-field>
|
|
805
|
+
<label>With prepend slot</label>
|
|
806
|
+
<fig-input-number value="50"
|
|
807
|
+
units="%">
|
|
808
|
+
<span slot="prepend">O</span>
|
|
809
|
+
</fig-input-number>
|
|
810
|
+
</fig-field>
|
|
678
811
|
<fig-header>
|
|
679
812
|
<h2>Color input</h2>
|
|
680
813
|
</fig-header>
|
package/fig.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a unique ID string using timestamp and random values
|
|
3
|
+
* @returns {string} A unique identifier
|
|
4
|
+
*/
|
|
1
5
|
function figUniqueId() {
|
|
2
6
|
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
|
3
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the browser supports the native popover API
|
|
10
|
+
* @returns {boolean} True if popover is supported
|
|
11
|
+
*/
|
|
4
12
|
function figSupportsPopover() {
|
|
5
13
|
return HTMLElement.prototype.hasOwnProperty("popover");
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
17
|
* A custom button element that supports different types and states.
|
|
10
|
-
* @attr {string} type - The button type: "button" (default), "toggle", or "
|
|
18
|
+
* @attr {string} type - The button type: "button" (default), "toggle", "submit", or "link"
|
|
11
19
|
* @attr {boolean} selected - Whether the button is in a selected state
|
|
12
20
|
* @attr {boolean} disabled - Whether the button is disabled
|
|
21
|
+
* @attr {string} href - URL for link type buttons
|
|
22
|
+
* @attr {string} target - Target window for link type buttons (e.g., "_blank")
|
|
13
23
|
*/
|
|
14
24
|
class FigButton extends HTMLElement {
|
|
15
25
|
type;
|
|
@@ -56,7 +66,7 @@ class FigButton extends HTMLElement {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
get type() {
|
|
59
|
-
return this.type;
|
|
69
|
+
return this.getAttribute("type") || "button";
|
|
60
70
|
}
|
|
61
71
|
set type(value) {
|
|
62
72
|
this.setAttribute("type", value);
|
|
@@ -99,8 +109,7 @@ class FigButton extends HTMLElement {
|
|
|
99
109
|
switch (name) {
|
|
100
110
|
case "disabled":
|
|
101
111
|
this.disabled = this.button.disabled =
|
|
102
|
-
newValue
|
|
103
|
-
(newValue === undefined && newValue !== null);
|
|
112
|
+
newValue !== null && newValue !== "false";
|
|
104
113
|
break;
|
|
105
114
|
case "type":
|
|
106
115
|
this.type = newValue;
|
|
@@ -237,6 +246,7 @@ customElements.define("fig-dropdown", FigDropdown);
|
|
|
237
246
|
class FigTooltip extends HTMLElement {
|
|
238
247
|
#boundHideOnChromeOpen;
|
|
239
248
|
#boundHideOnDragStart;
|
|
249
|
+
#boundHidePopupOutsideClick;
|
|
240
250
|
#touchTimeout;
|
|
241
251
|
#isTouching = false;
|
|
242
252
|
constructor() {
|
|
@@ -248,6 +258,7 @@ class FigTooltip extends HTMLElement {
|
|
|
248
258
|
// Bind methods that will be used as event listeners
|
|
249
259
|
this.#boundHideOnChromeOpen = this.#hideOnChromeOpen.bind(this);
|
|
250
260
|
this.#boundHideOnDragStart = this.hidePopup.bind(this);
|
|
261
|
+
this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
|
|
251
262
|
}
|
|
252
263
|
connectedCallback() {
|
|
253
264
|
this.setup();
|
|
@@ -265,6 +276,14 @@ class FigTooltip extends HTMLElement {
|
|
|
265
276
|
// Remove mousedown listener
|
|
266
277
|
this.removeEventListener("mousedown", this.#boundHideOnDragStart);
|
|
267
278
|
|
|
279
|
+
// Remove click outside listener for click action
|
|
280
|
+
if (this.action === "click") {
|
|
281
|
+
document.body.removeEventListener(
|
|
282
|
+
"click",
|
|
283
|
+
this.#boundHidePopupOutsideClick
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
268
287
|
// Clean up touch-related timers and listeners
|
|
269
288
|
clearTimeout(this.#touchTimeout);
|
|
270
289
|
if (this.action === "hover") {
|
|
@@ -307,7 +326,13 @@ class FigTooltip extends HTMLElement {
|
|
|
307
326
|
if (this.popup) {
|
|
308
327
|
this.popup.remove();
|
|
309
328
|
}
|
|
310
|
-
|
|
329
|
+
// Remove the click outside listener if it was added
|
|
330
|
+
if (this.action === "click") {
|
|
331
|
+
document.body.removeEventListener(
|
|
332
|
+
"click",
|
|
333
|
+
this.#boundHidePopupOutsideClick
|
|
334
|
+
);
|
|
335
|
+
}
|
|
311
336
|
}
|
|
312
337
|
isTouchDevice() {
|
|
313
338
|
return (
|
|
@@ -344,10 +369,7 @@ class FigTooltip extends HTMLElement {
|
|
|
344
369
|
});
|
|
345
370
|
} else if (this.action === "click") {
|
|
346
371
|
this.addEventListener("click", this.showDelayedPopup.bind(this));
|
|
347
|
-
document.body.addEventListener(
|
|
348
|
-
"click",
|
|
349
|
-
this.hidePopupOutsideClick.bind(this)
|
|
350
|
-
);
|
|
372
|
+
document.body.addEventListener("click", this.#boundHidePopupOutsideClick);
|
|
351
373
|
|
|
352
374
|
// Touch support for better mobile responsiveness
|
|
353
375
|
this.addEventListener("touchstart", this.showDelayedPopup.bind(this), {
|
|
@@ -665,10 +687,12 @@ class FigTab extends HTMLElement {
|
|
|
665
687
|
requestAnimationFrame(() => {
|
|
666
688
|
if (typeof this.getAttribute("content") === "string") {
|
|
667
689
|
this.content = document.querySelector(this.getAttribute("content"));
|
|
668
|
-
if (this
|
|
669
|
-
this
|
|
670
|
-
|
|
671
|
-
|
|
690
|
+
if (this.content) {
|
|
691
|
+
if (this.#selected) {
|
|
692
|
+
this.content.style.display = "block";
|
|
693
|
+
} else {
|
|
694
|
+
this.content.style.display = "none";
|
|
695
|
+
}
|
|
672
696
|
}
|
|
673
697
|
}
|
|
674
698
|
});
|
|
@@ -889,20 +913,15 @@ class FigSlider extends HTMLElement {
|
|
|
889
913
|
</div>`;
|
|
890
914
|
if (this.text) {
|
|
891
915
|
html = `${slider}
|
|
892
|
-
<fig-input-
|
|
916
|
+
<fig-input-number
|
|
893
917
|
placeholder="##"
|
|
894
|
-
type="number"
|
|
895
918
|
min="${this.min}"
|
|
896
919
|
max="${this.max}"
|
|
897
920
|
transform="${this.transform}"
|
|
898
921
|
step="${this.step}"
|
|
899
|
-
value="${this.value}"
|
|
900
|
-
${
|
|
901
|
-
|
|
902
|
-
? `<span slot="append">${this.units}</span>`
|
|
903
|
-
: ""
|
|
904
|
-
}
|
|
905
|
-
</fig-input-text>`;
|
|
922
|
+
value="${this.value}"
|
|
923
|
+
${this.units ? `units="${this.units}"` : ""}>
|
|
924
|
+
</fig-input-number>`;
|
|
906
925
|
} else {
|
|
907
926
|
html = slider;
|
|
908
927
|
}
|
|
@@ -923,7 +942,7 @@ class FigSlider extends HTMLElement {
|
|
|
923
942
|
}
|
|
924
943
|
|
|
925
944
|
this.datalist = this.querySelector("datalist");
|
|
926
|
-
this.
|
|
945
|
+
this.figInputNumber = this.querySelector("fig-input-number");
|
|
927
946
|
if (this.datalist) {
|
|
928
947
|
this.inputContainer.append(this.datalist);
|
|
929
948
|
this.datalist.setAttribute(
|
|
@@ -959,12 +978,15 @@ class FigSlider extends HTMLElement {
|
|
|
959
978
|
defaultOption.setAttribute("default", "true");
|
|
960
979
|
}
|
|
961
980
|
}
|
|
962
|
-
if (this.
|
|
963
|
-
this.
|
|
981
|
+
if (this.figInputNumber) {
|
|
982
|
+
this.figInputNumber.removeEventListener(
|
|
983
|
+
"input",
|
|
984
|
+
this.#boundHandleTextInput
|
|
985
|
+
);
|
|
986
|
+
this.figInputNumber.addEventListener(
|
|
964
987
|
"input",
|
|
965
988
|
this.#boundHandleTextInput
|
|
966
989
|
);
|
|
967
|
-
this.figInputText.addEventListener("input", this.#boundHandleTextInput);
|
|
968
990
|
}
|
|
969
991
|
|
|
970
992
|
this.#syncValue();
|
|
@@ -976,8 +998,8 @@ class FigSlider extends HTMLElement {
|
|
|
976
998
|
}
|
|
977
999
|
|
|
978
1000
|
#handleTextInput() {
|
|
979
|
-
if (this.
|
|
980
|
-
this.value = this.input.value = this.
|
|
1001
|
+
if (this.figInputNumber) {
|
|
1002
|
+
this.value = this.input.value = this.figInputNumber.value;
|
|
981
1003
|
this.#syncProperties();
|
|
982
1004
|
this.dispatchEvent(
|
|
983
1005
|
new CustomEvent("input", { detail: this.value, bubbles: true })
|
|
@@ -1000,8 +1022,8 @@ class FigSlider extends HTMLElement {
|
|
|
1000
1022
|
let val = this.input.value;
|
|
1001
1023
|
this.value = val;
|
|
1002
1024
|
this.#syncProperties();
|
|
1003
|
-
if (this.
|
|
1004
|
-
this.
|
|
1025
|
+
if (this.figInputNumber) {
|
|
1026
|
+
this.figInputNumber.setAttribute("value", val);
|
|
1005
1027
|
}
|
|
1006
1028
|
}
|
|
1007
1029
|
|
|
@@ -1039,23 +1061,22 @@ class FigSlider extends HTMLElement {
|
|
|
1039
1061
|
break;
|
|
1040
1062
|
case "disabled":
|
|
1041
1063
|
this.disabled = this.input.disabled =
|
|
1042
|
-
newValue
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
this.
|
|
1046
|
-
this.figInputText.setAttribute("disabled", this.disabled);
|
|
1064
|
+
newValue !== null && newValue !== "false";
|
|
1065
|
+
if (this.figInputNumber) {
|
|
1066
|
+
this.figInputNumber.disabled = this.disabled;
|
|
1067
|
+
this.figInputNumber.setAttribute("disabled", this.disabled);
|
|
1047
1068
|
}
|
|
1048
1069
|
break;
|
|
1049
1070
|
case "value":
|
|
1050
1071
|
this.value = newValue;
|
|
1051
|
-
if (this.
|
|
1052
|
-
this.
|
|
1072
|
+
if (this.figInputNumber) {
|
|
1073
|
+
this.figInputNumber.setAttribute("value", newValue);
|
|
1053
1074
|
}
|
|
1054
1075
|
break;
|
|
1055
1076
|
case "transform":
|
|
1056
1077
|
this.transform = Number(newValue) || 1;
|
|
1057
|
-
if (this.
|
|
1058
|
-
this.
|
|
1078
|
+
if (this.figInputNumber) {
|
|
1079
|
+
this.figInputNumber.setAttribute("transform", this.transform);
|
|
1059
1080
|
}
|
|
1060
1081
|
break;
|
|
1061
1082
|
case "min":
|
|
@@ -1300,8 +1321,7 @@ class FigInputText extends HTMLElement {
|
|
|
1300
1321
|
switch (name) {
|
|
1301
1322
|
case "disabled":
|
|
1302
1323
|
this.disabled = this.input.disabled =
|
|
1303
|
-
newValue
|
|
1304
|
-
(newValue === undefined && newValue !== null);
|
|
1324
|
+
newValue !== null && newValue !== "false";
|
|
1305
1325
|
break;
|
|
1306
1326
|
case "transform":
|
|
1307
1327
|
if (this.type === "number") {
|
|
@@ -1341,6 +1361,362 @@ class FigInputText extends HTMLElement {
|
|
|
1341
1361
|
}
|
|
1342
1362
|
window.customElements.define("fig-input-text", FigInputText);
|
|
1343
1363
|
|
|
1364
|
+
/**
|
|
1365
|
+
* A custom numeric input element that uses type="text" with inputmode="decimal".
|
|
1366
|
+
* Supports units display and all standard number input attributes.
|
|
1367
|
+
* @attr {string} value - The current numeric value
|
|
1368
|
+
* @attr {string} placeholder - Placeholder text
|
|
1369
|
+
* @attr {boolean} disabled - Whether the input is disabled
|
|
1370
|
+
* @attr {number} min - Minimum value
|
|
1371
|
+
* @attr {number} max - Maximum value
|
|
1372
|
+
* @attr {number} step - Step increment
|
|
1373
|
+
* @attr {number} transform - A multiplier for displayed number values
|
|
1374
|
+
* @attr {string} units - Unit string to append/prepend to displayed value (e.g., "%", "°", "$")
|
|
1375
|
+
* @attr {string} unit-position - Position of unit: "suffix" (default) or "prefix"
|
|
1376
|
+
* @attr {string} name - Form field name
|
|
1377
|
+
*/
|
|
1378
|
+
class FigInputNumber extends HTMLElement {
|
|
1379
|
+
#boundMouseMove;
|
|
1380
|
+
#boundMouseUp;
|
|
1381
|
+
#boundMouseDown;
|
|
1382
|
+
#boundInputChange;
|
|
1383
|
+
#boundInput;
|
|
1384
|
+
#boundFocus;
|
|
1385
|
+
#boundBlur;
|
|
1386
|
+
#units;
|
|
1387
|
+
#unitPosition;
|
|
1388
|
+
|
|
1389
|
+
constructor() {
|
|
1390
|
+
super();
|
|
1391
|
+
// Pre-bind the event handlers once
|
|
1392
|
+
this.#boundMouseMove = this.#handleMouseMove.bind(this);
|
|
1393
|
+
this.#boundMouseUp = this.#handleMouseUp.bind(this);
|
|
1394
|
+
this.#boundMouseDown = this.#handleMouseDown.bind(this);
|
|
1395
|
+
this.#boundInputChange = (e) => {
|
|
1396
|
+
e.stopPropagation();
|
|
1397
|
+
this.#handleInputChange(e);
|
|
1398
|
+
};
|
|
1399
|
+
this.#boundInput = (e) => {
|
|
1400
|
+
e.stopPropagation();
|
|
1401
|
+
this.#handleInput(e);
|
|
1402
|
+
};
|
|
1403
|
+
this.#boundFocus = (e) => {
|
|
1404
|
+
this.#handleFocus(e);
|
|
1405
|
+
};
|
|
1406
|
+
this.#boundBlur = (e) => {
|
|
1407
|
+
this.#handleBlur(e);
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
connectedCallback() {
|
|
1412
|
+
const valueAttr = this.getAttribute("value");
|
|
1413
|
+
this.value =
|
|
1414
|
+
valueAttr !== null && valueAttr !== "" ? Number(valueAttr) : "";
|
|
1415
|
+
this.placeholder = this.getAttribute("placeholder") || "";
|
|
1416
|
+
this.name = this.getAttribute("name") || null;
|
|
1417
|
+
this.#units = this.getAttribute("units") || "";
|
|
1418
|
+
this.#unitPosition = this.getAttribute("unit-position") || "suffix";
|
|
1419
|
+
|
|
1420
|
+
if (this.getAttribute("step")) {
|
|
1421
|
+
this.step = Number(this.getAttribute("step"));
|
|
1422
|
+
}
|
|
1423
|
+
if (this.getAttribute("min")) {
|
|
1424
|
+
this.min = Number(this.getAttribute("min"));
|
|
1425
|
+
}
|
|
1426
|
+
if (this.getAttribute("max")) {
|
|
1427
|
+
this.max = Number(this.getAttribute("max"));
|
|
1428
|
+
}
|
|
1429
|
+
this.transform = Number(this.getAttribute("transform") || 1);
|
|
1430
|
+
|
|
1431
|
+
let html = `<input
|
|
1432
|
+
type="text"
|
|
1433
|
+
inputmode="decimal"
|
|
1434
|
+
${this.name ? `name="${this.name}"` : ""}
|
|
1435
|
+
placeholder="${this.placeholder}"
|
|
1436
|
+
value="${this.#formatWithUnit(this.value)}" />`;
|
|
1437
|
+
|
|
1438
|
+
//child nodes hack
|
|
1439
|
+
requestAnimationFrame(() => {
|
|
1440
|
+
let append = this.querySelector("[slot=append]");
|
|
1441
|
+
let prepend = this.querySelector("[slot=prepend]");
|
|
1442
|
+
|
|
1443
|
+
this.innerHTML = html;
|
|
1444
|
+
|
|
1445
|
+
if (prepend) {
|
|
1446
|
+
prepend.addEventListener("click", this.focus.bind(this));
|
|
1447
|
+
this.prepend(prepend);
|
|
1448
|
+
}
|
|
1449
|
+
if (append) {
|
|
1450
|
+
append.addEventListener("click", this.focus.bind(this));
|
|
1451
|
+
this.append(append);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
this.input = this.querySelector("input");
|
|
1455
|
+
|
|
1456
|
+
if (this.getAttribute("min")) {
|
|
1457
|
+
this.min = Number(this.getAttribute("min"));
|
|
1458
|
+
}
|
|
1459
|
+
if (this.getAttribute("max")) {
|
|
1460
|
+
this.max = Number(this.getAttribute("max"));
|
|
1461
|
+
}
|
|
1462
|
+
if (this.getAttribute("step")) {
|
|
1463
|
+
this.step = Number(this.getAttribute("step"));
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Set disabled state if present
|
|
1467
|
+
if (this.hasAttribute("disabled")) {
|
|
1468
|
+
const disabledAttr = this.getAttribute("disabled");
|
|
1469
|
+
this.disabled = this.input.disabled = disabledAttr !== "false";
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
this.addEventListener("pointerdown", this.#boundMouseDown);
|
|
1473
|
+
this.input.removeEventListener("change", this.#boundInputChange);
|
|
1474
|
+
this.input.addEventListener("change", this.#boundInputChange);
|
|
1475
|
+
this.input.removeEventListener("input", this.#boundInput);
|
|
1476
|
+
this.input.addEventListener("input", this.#boundInput);
|
|
1477
|
+
this.input.removeEventListener("focus", this.#boundFocus);
|
|
1478
|
+
this.input.addEventListener("focus", this.#boundFocus);
|
|
1479
|
+
this.input.removeEventListener("blur", this.#boundBlur);
|
|
1480
|
+
this.input.addEventListener("blur", this.#boundBlur);
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
focus() {
|
|
1485
|
+
this.input.focus();
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
#getNumericValue(str) {
|
|
1489
|
+
if (!str) return "";
|
|
1490
|
+
if (!this.#units) {
|
|
1491
|
+
// No units, just extract numeric value
|
|
1492
|
+
let value = str.replace(/[^\d.-]/g, "");
|
|
1493
|
+
// Prevent multiple decimal points
|
|
1494
|
+
const parts = value.split(".");
|
|
1495
|
+
if (parts.length > 2) {
|
|
1496
|
+
value = parts[0] + "." + parts.slice(1).join("");
|
|
1497
|
+
}
|
|
1498
|
+
return value;
|
|
1499
|
+
}
|
|
1500
|
+
let value = str.replace(this.#units, "").trim();
|
|
1501
|
+
value = value.replace(/[^\d.-]/g, "");
|
|
1502
|
+
// Prevent multiple decimal points
|
|
1503
|
+
const parts = value.split(".");
|
|
1504
|
+
if (parts.length > 2) {
|
|
1505
|
+
value = parts[0] + "." + parts.slice(1).join("");
|
|
1506
|
+
}
|
|
1507
|
+
return value;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
#formatWithUnit(numericValue) {
|
|
1511
|
+
if (
|
|
1512
|
+
numericValue === "" ||
|
|
1513
|
+
numericValue === null ||
|
|
1514
|
+
numericValue === undefined
|
|
1515
|
+
)
|
|
1516
|
+
return "";
|
|
1517
|
+
// numericValue is the internal (non-transformed) value
|
|
1518
|
+
// For display, we apply transform and format
|
|
1519
|
+
let displayValue = Number(numericValue) * (this.transform || 1);
|
|
1520
|
+
if (isNaN(displayValue)) return "";
|
|
1521
|
+
displayValue = this.#formatNumber(displayValue);
|
|
1522
|
+
if (!this.#units) return displayValue.toString();
|
|
1523
|
+
if (this.#unitPosition === "prefix") {
|
|
1524
|
+
return this.#units + displayValue;
|
|
1525
|
+
} else {
|
|
1526
|
+
return displayValue + this.#units;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
#transformNumber(value) {
|
|
1531
|
+
if (value === "" || value === null || value === undefined) return "";
|
|
1532
|
+
let transformed = Number(value) * (this.transform || 1);
|
|
1533
|
+
transformed = this.#formatNumber(transformed);
|
|
1534
|
+
return transformed.toString();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
#handleFocus(e) {
|
|
1538
|
+
setTimeout(() => {
|
|
1539
|
+
const value = e.target.value;
|
|
1540
|
+
if (value && this.#units) {
|
|
1541
|
+
if (this.#unitPosition === "prefix") {
|
|
1542
|
+
e.target.setSelectionRange(this.#units.length, value.length);
|
|
1543
|
+
} else {
|
|
1544
|
+
const unitPos = value.indexOf(this.#units);
|
|
1545
|
+
if (unitPos > -1) {
|
|
1546
|
+
e.target.setSelectionRange(0, unitPos);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}, 0);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
#handleBlur(e) {
|
|
1554
|
+
let numericValue = this.#getNumericValue(e.target.value);
|
|
1555
|
+
if (numericValue !== "") {
|
|
1556
|
+
let val = Number(numericValue) / (this.transform || 1);
|
|
1557
|
+
val = this.#sanitizeInput(val, false);
|
|
1558
|
+
this.value = val;
|
|
1559
|
+
e.target.value = this.#formatWithUnit(this.value);
|
|
1560
|
+
} else {
|
|
1561
|
+
this.value = "";
|
|
1562
|
+
e.target.value = "";
|
|
1563
|
+
}
|
|
1564
|
+
this.dispatchEvent(
|
|
1565
|
+
new CustomEvent("change", { detail: this.value, bubbles: true })
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
#handleInput(e) {
|
|
1570
|
+
let numericValue = this.#getNumericValue(e.target.value);
|
|
1571
|
+
if (numericValue !== "") {
|
|
1572
|
+
this.value = Number(numericValue) / (this.transform || 1);
|
|
1573
|
+
} else {
|
|
1574
|
+
this.value = "";
|
|
1575
|
+
}
|
|
1576
|
+
this.dispatchEvent(
|
|
1577
|
+
new CustomEvent("input", { detail: this.value, bubbles: true })
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
#handleInputChange(e) {
|
|
1582
|
+
e.stopPropagation();
|
|
1583
|
+
let numericValue = this.#getNumericValue(e.target.value);
|
|
1584
|
+
if (numericValue !== "") {
|
|
1585
|
+
let val = Number(numericValue) / (this.transform || 1);
|
|
1586
|
+
val = this.#sanitizeInput(val, false);
|
|
1587
|
+
this.value = val;
|
|
1588
|
+
e.target.value = this.#formatWithUnit(this.value);
|
|
1589
|
+
} else {
|
|
1590
|
+
this.value = "";
|
|
1591
|
+
e.target.value = "";
|
|
1592
|
+
}
|
|
1593
|
+
this.dispatchEvent(
|
|
1594
|
+
new CustomEvent("input", { detail: this.value, bubbles: true })
|
|
1595
|
+
);
|
|
1596
|
+
this.dispatchEvent(
|
|
1597
|
+
new CustomEvent("change", { detail: this.value, bubbles: true })
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
#handleMouseMove(e) {
|
|
1602
|
+
if (this.disabled) return;
|
|
1603
|
+
if (e.altKey) {
|
|
1604
|
+
let step = (this.step || 1) * e.movementX;
|
|
1605
|
+
let numericValue = this.#getNumericValue(this.input.value);
|
|
1606
|
+
let value = Number(numericValue) / (this.transform || 1) + step;
|
|
1607
|
+
value = this.#sanitizeInput(value, false);
|
|
1608
|
+
this.value = value;
|
|
1609
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
1610
|
+
this.dispatchEvent(new CustomEvent("input", { bubbles: true }));
|
|
1611
|
+
this.dispatchEvent(new CustomEvent("change", { bubbles: true }));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
#handleMouseDown(e) {
|
|
1616
|
+
if (this.disabled) return;
|
|
1617
|
+
if (e.altKey) {
|
|
1618
|
+
this.input.style.cursor =
|
|
1619
|
+
this.style.cursor =
|
|
1620
|
+
document.body.style.cursor =
|
|
1621
|
+
"ew-resize";
|
|
1622
|
+
this.style.userSelect = "none";
|
|
1623
|
+
// Use the pre-bound handlers
|
|
1624
|
+
window.addEventListener("pointermove", this.#boundMouseMove);
|
|
1625
|
+
window.addEventListener("pointerup", this.#boundMouseUp);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
#handleMouseUp(e) {
|
|
1630
|
+
this.input.style.cursor =
|
|
1631
|
+
this.style.cursor =
|
|
1632
|
+
document.body.style.cursor =
|
|
1633
|
+
"";
|
|
1634
|
+
this.style.userSelect = "all";
|
|
1635
|
+
// Remove the pre-bound handlers
|
|
1636
|
+
window.removeEventListener("pointermove", this.#boundMouseMove);
|
|
1637
|
+
window.removeEventListener("pointerup", this.#boundMouseUp);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
#sanitizeInput(value, transform = true) {
|
|
1641
|
+
let sanitized = Number(value);
|
|
1642
|
+
if (isNaN(sanitized)) return "";
|
|
1643
|
+
if (typeof this.min === "number") {
|
|
1644
|
+
sanitized = Math.max(this.min, sanitized);
|
|
1645
|
+
}
|
|
1646
|
+
if (typeof this.max === "number") {
|
|
1647
|
+
sanitized = Math.min(this.max, sanitized);
|
|
1648
|
+
}
|
|
1649
|
+
sanitized = this.#formatNumber(sanitized);
|
|
1650
|
+
return sanitized;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
#formatNumber(num, precision = 2) {
|
|
1654
|
+
// Check if the number has any decimal places after rounding
|
|
1655
|
+
const rounded = Math.round(num * 100) / 100;
|
|
1656
|
+
return Number.isInteger(rounded) ? rounded : rounded.toFixed(precision);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
static get observedAttributes() {
|
|
1660
|
+
return [
|
|
1661
|
+
"value",
|
|
1662
|
+
"placeholder",
|
|
1663
|
+
"disabled",
|
|
1664
|
+
"step",
|
|
1665
|
+
"min",
|
|
1666
|
+
"max",
|
|
1667
|
+
"transform",
|
|
1668
|
+
"name",
|
|
1669
|
+
"units",
|
|
1670
|
+
"unit-position",
|
|
1671
|
+
];
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1675
|
+
if (this.input) {
|
|
1676
|
+
switch (name) {
|
|
1677
|
+
case "disabled":
|
|
1678
|
+
this.disabled = this.input.disabled =
|
|
1679
|
+
newValue !== null && newValue !== "false";
|
|
1680
|
+
break;
|
|
1681
|
+
case "units":
|
|
1682
|
+
this.#units = newValue || "";
|
|
1683
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
1684
|
+
break;
|
|
1685
|
+
case "unit-position":
|
|
1686
|
+
this.#unitPosition = newValue || "suffix";
|
|
1687
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
1688
|
+
break;
|
|
1689
|
+
case "transform":
|
|
1690
|
+
this.transform = Number(newValue) || 1;
|
|
1691
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
1692
|
+
break;
|
|
1693
|
+
case "value":
|
|
1694
|
+
let value =
|
|
1695
|
+
newValue !== null && newValue !== "" ? Number(newValue) : "";
|
|
1696
|
+
if (value !== "") {
|
|
1697
|
+
value = this.#sanitizeInput(value, false);
|
|
1698
|
+
}
|
|
1699
|
+
this.value = value;
|
|
1700
|
+
this.input.value = this.#formatWithUnit(this.value);
|
|
1701
|
+
break;
|
|
1702
|
+
case "min":
|
|
1703
|
+
case "max":
|
|
1704
|
+
case "step":
|
|
1705
|
+
this[name] = Number(newValue);
|
|
1706
|
+
break;
|
|
1707
|
+
case "name":
|
|
1708
|
+
this[name] = this.input[name] = newValue;
|
|
1709
|
+
this.input.setAttribute("name", newValue);
|
|
1710
|
+
break;
|
|
1711
|
+
default:
|
|
1712
|
+
this[name] = this.input[name] = newValue;
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
window.customElements.define("fig-input-number", FigInputNumber);
|
|
1719
|
+
|
|
1344
1720
|
/* Avatar */
|
|
1345
1721
|
class FigAvatar extends HTMLElement {
|
|
1346
1722
|
constructor() {
|
|
@@ -1438,14 +1814,13 @@ class FigInputColor extends HTMLElement {
|
|
|
1438
1814
|
</fig-input-text>`;
|
|
1439
1815
|
if (this.getAttribute("alpha") === "true") {
|
|
1440
1816
|
label += `<fig-tooltip text="Opacity">
|
|
1441
|
-
<fig-input-
|
|
1817
|
+
<fig-input-number
|
|
1442
1818
|
placeholder="##"
|
|
1443
|
-
type="number"
|
|
1444
1819
|
min="0"
|
|
1445
1820
|
max="100"
|
|
1446
|
-
value="${this.alpha}"
|
|
1447
|
-
|
|
1448
|
-
</fig-input-
|
|
1821
|
+
value="${this.alpha}"
|
|
1822
|
+
units="%">
|
|
1823
|
+
</fig-input-number>
|
|
1449
1824
|
</fig-tooltip>`;
|
|
1450
1825
|
}
|
|
1451
1826
|
html = `<div class="input-combo">
|
|
@@ -1460,7 +1835,7 @@ class FigInputColor extends HTMLElement {
|
|
|
1460
1835
|
requestAnimationFrame(() => {
|
|
1461
1836
|
this.#swatch = this.querySelector("fig-chit[type=color]");
|
|
1462
1837
|
this.#textInput = this.querySelector("fig-input-text:not([type=number])");
|
|
1463
|
-
this.#alphaInput = this.querySelector("fig-input-
|
|
1838
|
+
this.#alphaInput = this.querySelector("fig-input-number");
|
|
1464
1839
|
|
|
1465
1840
|
this.#swatch.disabled = this.hasAttribute("disabled");
|
|
1466
1841
|
this.#swatch.addEventListener("input", this.#handleInput.bind(this));
|
|
@@ -1522,7 +1897,9 @@ class FigInputColor extends HTMLElement {
|
|
|
1522
1897
|
#handleAlphaInput(event) {
|
|
1523
1898
|
//do not propagate to onInput handler for web component
|
|
1524
1899
|
event.stopPropagation();
|
|
1525
|
-
|
|
1900
|
+
// fig-input-number stores numeric value internally, ensure it's a number
|
|
1901
|
+
const alphaValue = Number(event.target.value) || 0;
|
|
1902
|
+
const alpha = Math.round((alphaValue / 100) * 255);
|
|
1526
1903
|
const alphaHex = alpha.toString(16).padStart(2, "0");
|
|
1527
1904
|
this.#setValues(this.hexOpaque + alphaHex);
|
|
1528
1905
|
this.#emitInputEvent();
|