@rogieking/figui3 1.9.2 → 1.9.4
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 +158 -90
- package/example.html +134 -0
- package/fig.js +444 -46
- 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,9 @@ 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 {
|
|
2392
|
+
flex-basis: 6em;
|
|
2327
2393
|
display: inline-flex;
|
|
2328
2394
|
flex-direction: column;
|
|
2329
2395
|
align-items: stretch;
|
|
@@ -2334,7 +2400,8 @@ fig-input-color {
|
|
|
2334
2400
|
}
|
|
2335
2401
|
}
|
|
2336
2402
|
|
|
2337
|
-
& fig-input-text[type="number"]
|
|
2403
|
+
& fig-input-text[type="number"],
|
|
2404
|
+
& fig-input-number {
|
|
2338
2405
|
width: 3.5rem;
|
|
2339
2406
|
display: inline-flex;
|
|
2340
2407
|
}
|
|
@@ -2347,7 +2414,8 @@ fig-slider {
|
|
|
2347
2414
|
flex-grow: 1;
|
|
2348
2415
|
}
|
|
2349
2416
|
|
|
2350
|
-
& fig-input-text[type="number"]
|
|
2417
|
+
& fig-input-text[type="number"],
|
|
2418
|
+
& fig-input-number {
|
|
2351
2419
|
width: 3.5rem;
|
|
2352
2420
|
}
|
|
2353
2421
|
}
|
package/example.html
CHANGED
|
@@ -27,6 +27,28 @@
|
|
|
27
27
|
<fig-spinner></fig-spinner>
|
|
28
28
|
</fig-header>
|
|
29
29
|
|
|
30
|
+
<br /><br /><br /><br /><br /><br /><br /><br />
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<fig-field direction="horizontal">
|
|
34
|
+
<label>Position</label>
|
|
35
|
+
<fig-dropdown value="outside"
|
|
36
|
+
variant="neue">
|
|
37
|
+
<option value="inside">Inside</option>
|
|
38
|
+
<option value="center">Center</option>
|
|
39
|
+
<option value="outside">Outside</option>
|
|
40
|
+
</fig-dropdown>
|
|
41
|
+
</fig-field>
|
|
42
|
+
|
|
43
|
+
<fig-field direction="horizontal">
|
|
44
|
+
<label>Position</label>
|
|
45
|
+
<fig-dropdown value="outside">
|
|
46
|
+
<option value="inside">Inside</option>
|
|
47
|
+
<option value="center">Center</option>
|
|
48
|
+
<option value="outside">Outside</option>
|
|
49
|
+
</fig-dropdown>
|
|
50
|
+
</fig-field>
|
|
51
|
+
|
|
30
52
|
|
|
31
53
|
|
|
32
54
|
|
|
@@ -665,6 +687,111 @@
|
|
|
665
687
|
<span slot="prepend">X</span>
|
|
666
688
|
</fig-input-text>
|
|
667
689
|
</fig-field>
|
|
690
|
+
<fig-header>
|
|
691
|
+
<h2>Number input</h2>
|
|
692
|
+
</fig-header>
|
|
693
|
+
<fig-field>
|
|
694
|
+
<label>Basic number input</label>
|
|
695
|
+
<fig-input-number value="100"></fig-input-number>
|
|
696
|
+
</fig-field>
|
|
697
|
+
<fig-field>
|
|
698
|
+
<label>With units (percentage)</label>
|
|
699
|
+
<fig-input-number value="100"
|
|
700
|
+
units="%"></fig-input-number>
|
|
701
|
+
</fig-field>
|
|
702
|
+
<fig-field>
|
|
703
|
+
<label>With units (degrees)</label>
|
|
704
|
+
<fig-input-number value="45"
|
|
705
|
+
units="°"
|
|
706
|
+
min="0"
|
|
707
|
+
max="360"
|
|
708
|
+
step="1"></fig-input-number>
|
|
709
|
+
</fig-field>
|
|
710
|
+
<fig-field>
|
|
711
|
+
<label>With prefix unit (currency)</label>
|
|
712
|
+
<fig-input-number value="50"
|
|
713
|
+
units="$"
|
|
714
|
+
unit-position="prefix"
|
|
715
|
+
min="0"
|
|
716
|
+
step="0.01"></fig-input-number>
|
|
717
|
+
</fig-field>
|
|
718
|
+
<fig-field>
|
|
719
|
+
<label>With transform (like slider)</label>
|
|
720
|
+
<fig-input-number value="0.5"
|
|
721
|
+
units="%"
|
|
722
|
+
transform="100"
|
|
723
|
+
min="0"
|
|
724
|
+
max="1"
|
|
725
|
+
step="0.01"></fig-input-number>
|
|
726
|
+
</fig-field>
|
|
727
|
+
<fig-field>
|
|
728
|
+
<label>With min/max constraints</label>
|
|
729
|
+
<fig-input-number value="25"
|
|
730
|
+
units="px"
|
|
731
|
+
min="0"
|
|
732
|
+
max="1000"
|
|
733
|
+
step="1"></fig-input-number>
|
|
734
|
+
</fig-field>
|
|
735
|
+
<fig-field>
|
|
736
|
+
<label>With placeholder</label>
|
|
737
|
+
<fig-input-number placeholder="Enter value"
|
|
738
|
+
units="%"
|
|
739
|
+
min="0"
|
|
740
|
+
max="100"></fig-input-number>
|
|
741
|
+
</fig-field>
|
|
742
|
+
<fig-field>
|
|
743
|
+
<label>Disabled</label>
|
|
744
|
+
<fig-input-number value="75"
|
|
745
|
+
units="%"
|
|
746
|
+
disabled></fig-input-number>
|
|
747
|
+
</fig-field>
|
|
748
|
+
<fig-field>
|
|
749
|
+
<label>With name attribute</label>
|
|
750
|
+
<fig-input-number value="42"
|
|
751
|
+
units="px"
|
|
752
|
+
name="width"></fig-input-number>
|
|
753
|
+
</fig-field>
|
|
754
|
+
<fig-field>
|
|
755
|
+
<label>Small step (decimals)</label>
|
|
756
|
+
<fig-input-number value="1.5"
|
|
757
|
+
units="rem"
|
|
758
|
+
min="0"
|
|
759
|
+
max="10"
|
|
760
|
+
step="0.1"></fig-input-number>
|
|
761
|
+
</fig-field>
|
|
762
|
+
<fig-field>
|
|
763
|
+
<label>Large range with transform</label>
|
|
764
|
+
<fig-input-number value="0.75"
|
|
765
|
+
units="%"
|
|
766
|
+
transform="100"
|
|
767
|
+
min="0"
|
|
768
|
+
max="1"
|
|
769
|
+
step="0.01"></fig-input-number>
|
|
770
|
+
</fig-field>
|
|
771
|
+
<fig-field>
|
|
772
|
+
<label>Negative values allowed</label>
|
|
773
|
+
<fig-input-number value="-10"
|
|
774
|
+
units="px"
|
|
775
|
+
min="-100"
|
|
776
|
+
max="100"
|
|
777
|
+
step="1"></fig-input-number>
|
|
778
|
+
</fig-field>
|
|
779
|
+
<fig-field>
|
|
780
|
+
<label>With append slot</label>
|
|
781
|
+
<fig-input-number value="90"
|
|
782
|
+
units="°"
|
|
783
|
+
min="0"
|
|
784
|
+
max="360">
|
|
785
|
+
<span slot="append">deg</span>
|
|
786
|
+
</fig-input-number>
|
|
787
|
+
</fig-field>
|
|
788
|
+
<fig-field>
|
|
789
|
+
<label>With prepend slot</label>
|
|
790
|
+
<fig-input-number value="50"
|
|
791
|
+
units="%">
|
|
792
|
+
<span slot="prepend">O</span>
|
|
793
|
+
</fig-input-number>
|
|
794
|
+
</fig-field>
|
|
668
795
|
<fig-header>
|
|
669
796
|
<h2>Color input</h2>
|
|
670
797
|
</fig-header>
|
|
@@ -981,6 +1108,13 @@
|
|
|
981
1108
|
</fig-field>
|
|
982
1109
|
</fig-content>
|
|
983
1110
|
|
|
1111
|
+
<script>
|
|
1112
|
+
document.addEventListener("input", (e) => {
|
|
1113
|
+
console.log("input", e.target, e.target.value);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
</script>
|
|
1117
|
+
|
|
984
1118
|
</body>
|
|
985
1119
|
|
|
986
1120
|
</html>
|
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;
|
|
@@ -170,11 +179,25 @@ class FigDropdown extends HTMLElement {
|
|
|
170
179
|
#handleSelectInput(e) {
|
|
171
180
|
this.value = e.target.value;
|
|
172
181
|
this.setAttribute("value", this.value);
|
|
182
|
+
this.dispatchEvent(
|
|
183
|
+
new CustomEvent("input", {
|
|
184
|
+
detail: this.value,
|
|
185
|
+
bubbles: true,
|
|
186
|
+
composed: true,
|
|
187
|
+
})
|
|
188
|
+
);
|
|
173
189
|
}
|
|
174
|
-
#handleSelectChange() {
|
|
190
|
+
#handleSelectChange(e) {
|
|
175
191
|
if (this.type === "dropdown") {
|
|
176
192
|
this.select.selectedIndex = -1;
|
|
177
193
|
}
|
|
194
|
+
this.dispatchEvent(
|
|
195
|
+
new CustomEvent("change", {
|
|
196
|
+
detail: this.value,
|
|
197
|
+
bubbles: true,
|
|
198
|
+
composed: true,
|
|
199
|
+
})
|
|
200
|
+
);
|
|
178
201
|
}
|
|
179
202
|
focus() {
|
|
180
203
|
this.select.focus();
|
|
@@ -223,6 +246,7 @@ customElements.define("fig-dropdown", FigDropdown);
|
|
|
223
246
|
class FigTooltip extends HTMLElement {
|
|
224
247
|
#boundHideOnChromeOpen;
|
|
225
248
|
#boundHideOnDragStart;
|
|
249
|
+
#boundHidePopupOutsideClick;
|
|
226
250
|
#touchTimeout;
|
|
227
251
|
#isTouching = false;
|
|
228
252
|
constructor() {
|
|
@@ -234,6 +258,7 @@ class FigTooltip extends HTMLElement {
|
|
|
234
258
|
// Bind methods that will be used as event listeners
|
|
235
259
|
this.#boundHideOnChromeOpen = this.#hideOnChromeOpen.bind(this);
|
|
236
260
|
this.#boundHideOnDragStart = this.hidePopup.bind(this);
|
|
261
|
+
this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
|
|
237
262
|
}
|
|
238
263
|
connectedCallback() {
|
|
239
264
|
this.setup();
|
|
@@ -251,6 +276,14 @@ class FigTooltip extends HTMLElement {
|
|
|
251
276
|
// Remove mousedown listener
|
|
252
277
|
this.removeEventListener("mousedown", this.#boundHideOnDragStart);
|
|
253
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
|
+
|
|
254
287
|
// Clean up touch-related timers and listeners
|
|
255
288
|
clearTimeout(this.#touchTimeout);
|
|
256
289
|
if (this.action === "hover") {
|
|
@@ -293,7 +326,13 @@ class FigTooltip extends HTMLElement {
|
|
|
293
326
|
if (this.popup) {
|
|
294
327
|
this.popup.remove();
|
|
295
328
|
}
|
|
296
|
-
|
|
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
|
+
}
|
|
297
336
|
}
|
|
298
337
|
isTouchDevice() {
|
|
299
338
|
return (
|
|
@@ -330,10 +369,7 @@ class FigTooltip extends HTMLElement {
|
|
|
330
369
|
});
|
|
331
370
|
} else if (this.action === "click") {
|
|
332
371
|
this.addEventListener("click", this.showDelayedPopup.bind(this));
|
|
333
|
-
document.body.addEventListener(
|
|
334
|
-
"click",
|
|
335
|
-
this.hidePopupOutsideClick.bind(this)
|
|
336
|
-
);
|
|
372
|
+
document.body.addEventListener("click", this.#boundHidePopupOutsideClick);
|
|
337
373
|
|
|
338
374
|
// Touch support for better mobile responsiveness
|
|
339
375
|
this.addEventListener("touchstart", this.showDelayedPopup.bind(this), {
|
|
@@ -651,10 +687,12 @@ class FigTab extends HTMLElement {
|
|
|
651
687
|
requestAnimationFrame(() => {
|
|
652
688
|
if (typeof this.getAttribute("content") === "string") {
|
|
653
689
|
this.content = document.querySelector(this.getAttribute("content"));
|
|
654
|
-
if (this
|
|
655
|
-
this
|
|
656
|
-
|
|
657
|
-
|
|
690
|
+
if (this.content) {
|
|
691
|
+
if (this.#selected) {
|
|
692
|
+
this.content.style.display = "block";
|
|
693
|
+
} else {
|
|
694
|
+
this.content.style.display = "none";
|
|
695
|
+
}
|
|
658
696
|
}
|
|
659
697
|
}
|
|
660
698
|
});
|
|
@@ -875,20 +913,15 @@ class FigSlider extends HTMLElement {
|
|
|
875
913
|
</div>`;
|
|
876
914
|
if (this.text) {
|
|
877
915
|
html = `${slider}
|
|
878
|
-
<fig-input-
|
|
916
|
+
<fig-input-number
|
|
879
917
|
placeholder="##"
|
|
880
|
-
type="number"
|
|
881
918
|
min="${this.min}"
|
|
882
919
|
max="${this.max}"
|
|
883
920
|
transform="${this.transform}"
|
|
884
921
|
step="${this.step}"
|
|
885
|
-
value="${this.value}"
|
|
886
|
-
${
|
|
887
|
-
|
|
888
|
-
? `<span slot="append">${this.units}</span>`
|
|
889
|
-
: ""
|
|
890
|
-
}
|
|
891
|
-
</fig-input-text>`;
|
|
922
|
+
value="${this.value}"
|
|
923
|
+
${this.units ? `units="${this.units}"` : ""}>
|
|
924
|
+
</fig-input-number>`;
|
|
892
925
|
} else {
|
|
893
926
|
html = slider;
|
|
894
927
|
}
|
|
@@ -909,7 +942,7 @@ class FigSlider extends HTMLElement {
|
|
|
909
942
|
}
|
|
910
943
|
|
|
911
944
|
this.datalist = this.querySelector("datalist");
|
|
912
|
-
this.
|
|
945
|
+
this.figInputNumber = this.querySelector("fig-input-number");
|
|
913
946
|
if (this.datalist) {
|
|
914
947
|
this.inputContainer.append(this.datalist);
|
|
915
948
|
this.datalist.setAttribute(
|
|
@@ -945,12 +978,15 @@ class FigSlider extends HTMLElement {
|
|
|
945
978
|
defaultOption.setAttribute("default", "true");
|
|
946
979
|
}
|
|
947
980
|
}
|
|
948
|
-
if (this.
|
|
949
|
-
this.
|
|
981
|
+
if (this.figInputNumber) {
|
|
982
|
+
this.figInputNumber.removeEventListener(
|
|
983
|
+
"input",
|
|
984
|
+
this.#boundHandleTextInput
|
|
985
|
+
);
|
|
986
|
+
this.figInputNumber.addEventListener(
|
|
950
987
|
"input",
|
|
951
988
|
this.#boundHandleTextInput
|
|
952
989
|
);
|
|
953
|
-
this.figInputText.addEventListener("input", this.#boundHandleTextInput);
|
|
954
990
|
}
|
|
955
991
|
|
|
956
992
|
this.#syncValue();
|
|
@@ -962,10 +998,12 @@ class FigSlider extends HTMLElement {
|
|
|
962
998
|
}
|
|
963
999
|
|
|
964
1000
|
#handleTextInput() {
|
|
965
|
-
if (this.
|
|
966
|
-
this.value = this.input.value = this.
|
|
1001
|
+
if (this.figInputNumber) {
|
|
1002
|
+
this.value = this.input.value = this.figInputNumber.value;
|
|
967
1003
|
this.#syncProperties();
|
|
968
|
-
this.dispatchEvent(
|
|
1004
|
+
this.dispatchEvent(
|
|
1005
|
+
new CustomEvent("input", { detail: this.value, bubbles: true })
|
|
1006
|
+
);
|
|
969
1007
|
}
|
|
970
1008
|
}
|
|
971
1009
|
#calculateNormal(value) {
|
|
@@ -984,14 +1022,16 @@ class FigSlider extends HTMLElement {
|
|
|
984
1022
|
let val = this.input.value;
|
|
985
1023
|
this.value = val;
|
|
986
1024
|
this.#syncProperties();
|
|
987
|
-
if (this.
|
|
988
|
-
this.
|
|
1025
|
+
if (this.figInputNumber) {
|
|
1026
|
+
this.figInputNumber.setAttribute("value", val);
|
|
989
1027
|
}
|
|
990
1028
|
}
|
|
991
1029
|
|
|
992
1030
|
#handleInput() {
|
|
993
1031
|
this.#syncValue();
|
|
994
|
-
this.dispatchEvent(
|
|
1032
|
+
this.dispatchEvent(
|
|
1033
|
+
new CustomEvent("input", { detail: this.value, bubbles: true })
|
|
1034
|
+
);
|
|
995
1035
|
}
|
|
996
1036
|
|
|
997
1037
|
static get observedAttributes() {
|
|
@@ -1021,23 +1061,22 @@ class FigSlider extends HTMLElement {
|
|
|
1021
1061
|
break;
|
|
1022
1062
|
case "disabled":
|
|
1023
1063
|
this.disabled = this.input.disabled =
|
|
1024
|
-
newValue
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this.
|
|
1028
|
-
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);
|
|
1029
1068
|
}
|
|
1030
1069
|
break;
|
|
1031
1070
|
case "value":
|
|
1032
1071
|
this.value = newValue;
|
|
1033
|
-
if (this.
|
|
1034
|
-
this.
|
|
1072
|
+
if (this.figInputNumber) {
|
|
1073
|
+
this.figInputNumber.setAttribute("value", newValue);
|
|
1035
1074
|
}
|
|
1036
1075
|
break;
|
|
1037
1076
|
case "transform":
|
|
1038
1077
|
this.transform = Number(newValue) || 1;
|
|
1039
|
-
if (this.
|
|
1040
|
-
this.
|
|
1078
|
+
if (this.figInputNumber) {
|
|
1079
|
+
this.figInputNumber.setAttribute("transform", this.transform);
|
|
1041
1080
|
}
|
|
1042
1081
|
break;
|
|
1043
1082
|
case "min":
|
|
@@ -1178,8 +1217,12 @@ class FigInputText extends HTMLElement {
|
|
|
1178
1217
|
}
|
|
1179
1218
|
this.value = value;
|
|
1180
1219
|
this.input.value = valueTransformed;
|
|
1181
|
-
this.dispatchEvent(
|
|
1182
|
-
|
|
1220
|
+
this.dispatchEvent(
|
|
1221
|
+
new CustomEvent("input", { detail: this.value, bubbles: true })
|
|
1222
|
+
);
|
|
1223
|
+
this.dispatchEvent(
|
|
1224
|
+
new CustomEvent("change", { detail: this.value, bubbles: true })
|
|
1225
|
+
);
|
|
1183
1226
|
}
|
|
1184
1227
|
#handleMouseMove(e) {
|
|
1185
1228
|
if (this.type !== "number") return;
|
|
@@ -1278,8 +1321,7 @@ class FigInputText extends HTMLElement {
|
|
|
1278
1321
|
switch (name) {
|
|
1279
1322
|
case "disabled":
|
|
1280
1323
|
this.disabled = this.input.disabled =
|
|
1281
|
-
newValue
|
|
1282
|
-
(newValue === undefined && newValue !== null);
|
|
1324
|
+
newValue !== null && newValue !== "false";
|
|
1283
1325
|
break;
|
|
1284
1326
|
case "transform":
|
|
1285
1327
|
if (this.type === "number") {
|
|
@@ -1319,6 +1361,362 @@ class FigInputText extends HTMLElement {
|
|
|
1319
1361
|
}
|
|
1320
1362
|
window.customElements.define("fig-input-text", FigInputText);
|
|
1321
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
|
+
|
|
1322
1720
|
/* Avatar */
|
|
1323
1721
|
class FigAvatar extends HTMLElement {
|
|
1324
1722
|
constructor() {
|