vanilla-wheel-number-picker 1.1.0 → 1.1.2
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/README.md +64 -12
- package/demo/index.html +4 -2
- package/dist/wheel-number-picker.css +39 -2
- package/dist/wheel-number-picker.js +86 -13
- package/dist/wheel-number-picker.min.js +86 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vanilla JS Wheel Number Picker
|
|
2
2
|
|
|
3
3
|
A small plain JavaScript wheel-style number picker. No framework required.
|
|
4
4
|
|
|
@@ -21,6 +21,8 @@ A small plain JavaScript wheel-style number picker. No framework required.
|
|
|
21
21
|
muted-color="#9ca3af"
|
|
22
22
|
active-color="#2563eb"
|
|
23
23
|
background="#f8fafc"
|
|
24
|
+
arrows
|
|
25
|
+
arrows-hidden-max-width="480"
|
|
24
26
|
>
|
|
25
27
|
</wheel-number-picker>
|
|
26
28
|
|
|
@@ -58,6 +60,8 @@ A small plain JavaScript wheel-style number picker. No framework required.
|
|
|
58
60
|
mutedColor: "#9ca3af",
|
|
59
61
|
activeColor: "#2563eb",
|
|
60
62
|
background: "#f8fafc",
|
|
63
|
+
arrows: true,
|
|
64
|
+
arrowsHiddenMaxWidth: 480,
|
|
61
65
|
onChange(value) {
|
|
62
66
|
console.log(value);
|
|
63
67
|
},
|
|
@@ -67,17 +71,19 @@ A small plain JavaScript wheel-style number picker. No framework required.
|
|
|
67
71
|
|
|
68
72
|
## Options / attributes
|
|
69
73
|
|
|
70
|
-
| Option
|
|
71
|
-
|
|
|
72
|
-
| `min`
|
|
73
|
-
| `max`
|
|
74
|
-
| `value`
|
|
75
|
-
| `itemHeight`
|
|
76
|
-
| `visibleItems`
|
|
77
|
-
| `color`
|
|
78
|
-
| `mutedColor`
|
|
79
|
-
| `activeColor`
|
|
80
|
-
| `background`
|
|
74
|
+
| Option | Attribute | Default | Description |
|
|
75
|
+
| ---------------------- | ------------------------- | -------: | ------------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `min` | `min` | `0` | Minimum value |
|
|
77
|
+
| `max` | `max` | `100` | Maximum value |
|
|
78
|
+
| `value` | `value` | `min` | Initial/current value |
|
|
79
|
+
| `itemHeight` | `item-height` | `44` | Row height in pixels. This now controls row height, highlight height, snap distance, and total picker height. |
|
|
80
|
+
| `visibleItems` | `visible-items` | `5` | Number of visible rows. Even numbers are rounded up to the next odd number so one row can stay centered. |
|
|
81
|
+
| `color` | `color` | `#111` | Base/active text color fallback |
|
|
82
|
+
| `mutedColor` | `muted-color` | `#aaa` | Non-selected item text color |
|
|
83
|
+
| `activeColor` | `active-color` | `color` | Selected item text color |
|
|
84
|
+
| `background` | `background` | gradient | Picker window background. Accepts any valid CSS background value. |
|
|
85
|
+
| `arrows` | `arrows` | `false` | Shows clickable top/bottom arrow buttons that decrement/increment the value. |
|
|
86
|
+
| `arrowsHiddenMaxWidth` | `arrows-hidden-max-width` | empty | Optional responsive breakpoint in pixels. At screen widths up to this value, the arrow buttons are hidden. |
|
|
81
87
|
|
|
82
88
|
## Dynamic updates
|
|
83
89
|
|
|
@@ -90,6 +96,8 @@ picker.setAttribute("item-height", "64");
|
|
|
90
96
|
picker.setAttribute("visible-items", "3");
|
|
91
97
|
picker.setAttribute("active-color", "crimson");
|
|
92
98
|
picker.setAttribute("background", "#fff7ed");
|
|
99
|
+
picker.setAttribute("arrows", "true");
|
|
100
|
+
picker.setAttribute("arrows-hidden-max-width", "480");
|
|
93
101
|
```
|
|
94
102
|
|
|
95
103
|
The component recalculates its render dimensions and snapping math automatically.
|
|
@@ -115,3 +123,47 @@ For the most predictable behavior, prefer the `item-height` attribute or `itemHe
|
|
|
115
123
|
## Local demo
|
|
116
124
|
|
|
117
125
|
Open `demo/index.html` in a browser.
|
|
126
|
+
|
|
127
|
+
## 1.1.1 item-height alignment fix
|
|
128
|
+
|
|
129
|
+
`item-height` now drives the offset math from the center line of the picker, not from an implicit zero-offset assumption. Font sizes also scale down with small item heights unless you explicitly override `--wnp-item-font-size` or `--wnp-active-font-size`.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
|
|
133
|
+
```html
|
|
134
|
+
<wheel-number-picker item-height="28" visible-items="3"></wheel-number-picker>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 1.1.2 responsive arrows
|
|
138
|
+
|
|
139
|
+
Enable clickable arrows with the `arrows` attribute. Each click steps the picker by one value.
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<wheel-number-picker
|
|
143
|
+
min="0"
|
|
144
|
+
max="10"
|
|
145
|
+
value="5"
|
|
146
|
+
arrows
|
|
147
|
+
arrows-hidden-max-width="480"
|
|
148
|
+
>
|
|
149
|
+
</wheel-number-picker>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`arrows-hidden-max-width="480"` injects a component-scoped media query equivalent to:
|
|
153
|
+
|
|
154
|
+
```css
|
|
155
|
+
@media (max-width: 480px) {
|
|
156
|
+
/* this component's arrows are hidden */
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
You can style the buttons with CSS variables:
|
|
161
|
+
|
|
162
|
+
```css
|
|
163
|
+
wheel-number-picker {
|
|
164
|
+
--wnp-arrow-height: 36px;
|
|
165
|
+
--wnp-arrow-gap: 6px;
|
|
166
|
+
--wnp-arrow-background: #f8fafc;
|
|
167
|
+
--wnp-arrow-color: #2563eb;
|
|
168
|
+
}
|
|
169
|
+
```
|
package/demo/index.html
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<body>
|
|
49
49
|
<main>
|
|
50
50
|
<h1>Wheel Number Picker</h1>
|
|
51
|
-
<p>Drag, scroll,
|
|
51
|
+
<p>Drag, scroll, use keyboard arrows, or click the arrow buttons. Resize below 480px to hide arrows.</p>
|
|
52
52
|
|
|
53
53
|
<wheel-number-picker
|
|
54
54
|
min="0"
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
color="#111827"
|
|
60
60
|
muted-color="#9ca3af"
|
|
61
61
|
active-color="#2563eb"
|
|
62
|
-
background="#f8fafc"
|
|
62
|
+
background="#f8fafc"
|
|
63
|
+
arrows
|
|
64
|
+
arrows-hidden-max-width="480">
|
|
63
65
|
</wheel-number-picker>
|
|
64
66
|
|
|
65
67
|
<div class="selected">Selected: <strong id="selectedValue">25</strong></div>
|
|
@@ -26,17 +26,21 @@ wheel-number-picker,
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
.wheel-number-picker__item {
|
|
29
|
+
box-sizing: border-box;
|
|
29
30
|
height: var(--wnp-item-height, 44px);
|
|
31
|
+
min-height: var(--wnp-item-height, 44px);
|
|
30
32
|
display: grid;
|
|
31
33
|
place-items: center;
|
|
32
|
-
|
|
34
|
+
line-height: 1;
|
|
35
|
+
font-size: var(--wnp-item-font-size, clamp(12px, calc(var(--wnp-item-height, 44px) * 0.5), 22px));
|
|
33
36
|
color: var(--wnp-muted-color, #aaa);
|
|
34
37
|
transition: color 120ms ease, font-size 120ms ease, opacity 120ms ease;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
.wheel-number-picker__item--active {
|
|
38
41
|
color: var(--wnp-active-color, var(--wnp-color, #111));
|
|
39
|
-
|
|
42
|
+
line-height: 1;
|
|
43
|
+
font-size: var(--wnp-active-font-size, clamp(16px, calc(var(--wnp-item-height, 44px) * 0.72), 34px));
|
|
40
44
|
font-weight: var(--wnp-active-font-weight, 750);
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -58,3 +62,36 @@ wheel-number-picker,
|
|
|
58
62
|
pointer-events: none;
|
|
59
63
|
background: var(--wnp-fade, linear-gradient(to bottom, white 0%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, white 100%));
|
|
60
64
|
}
|
|
65
|
+
|
|
66
|
+
.wheel-number-picker__arrow {
|
|
67
|
+
width: 100%;
|
|
68
|
+
height: var(--wnp-arrow-height, 32px);
|
|
69
|
+
display: var(--wnp-arrows-display, none);
|
|
70
|
+
place-items: center;
|
|
71
|
+
border: var(--wnp-arrow-border, 1px solid #ddd);
|
|
72
|
+
border-radius: var(--wnp-arrow-radius, 12px);
|
|
73
|
+
background: var(--wnp-arrow-background, var(--wnp-background, #fff));
|
|
74
|
+
color: var(--wnp-arrow-color, var(--wnp-active-color, var(--wnp-color, #111)));
|
|
75
|
+
font: inherit;
|
|
76
|
+
font-size: var(--wnp-arrow-font-size, 14px);
|
|
77
|
+
line-height: 1;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
user-select: none;
|
|
80
|
+
touch-action: manipulation;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.wheel-number-picker__arrow:hover {
|
|
84
|
+
background: var(--wnp-arrow-hover-background, #f5f5f5);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.wheel-number-picker__arrow:active {
|
|
88
|
+
transform: translateY(1px);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.wheel-number-picker__arrow--up {
|
|
92
|
+
margin-bottom: var(--wnp-arrow-gap, 8px);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.wheel-number-picker__arrow--down {
|
|
96
|
+
margin-top: var(--wnp-arrow-gap, 8px);
|
|
97
|
+
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
color: '',
|
|
11
11
|
activeColor: '',
|
|
12
12
|
mutedColor: '',
|
|
13
|
-
background: ''
|
|
13
|
+
background: '',
|
|
14
|
+
arrows: false,
|
|
15
|
+
arrowsHiddenMaxWidth: ''
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
function readOption(root, options, optionName, attributeName, fallback) {
|
|
@@ -36,6 +38,9 @@
|
|
|
36
38
|
background: readOption(this.root, options, 'background', 'background', DEFAULTS.background)
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
this.arrows = this.normalizeBoolean(readOption(this.root, options, 'arrows', 'arrows', DEFAULTS.arrows));
|
|
42
|
+
this.arrowsHiddenMaxWidth = readOption(this.root, options, 'arrowsHiddenMaxWidth', 'arrows-hidden-max-width', DEFAULTS.arrowsHiddenMaxWidth);
|
|
43
|
+
|
|
39
44
|
this.isDragging = false;
|
|
40
45
|
this.startY = 0;
|
|
41
46
|
this.startOffset = 0;
|
|
@@ -45,6 +50,7 @@
|
|
|
45
50
|
this.velocity = 0;
|
|
46
51
|
this.animationFrame = null;
|
|
47
52
|
|
|
53
|
+
this.normalizeConfiguration();
|
|
48
54
|
this.render();
|
|
49
55
|
this.bindEvents();
|
|
50
56
|
this.snapToValue(this.value, false);
|
|
@@ -57,26 +63,30 @@
|
|
|
57
63
|
if (!this.root.getAttribute('aria-label')) this.root.setAttribute('aria-label', 'Number picker');
|
|
58
64
|
|
|
59
65
|
this.root.innerHTML = `
|
|
66
|
+
<button class="wheel-number-picker__arrow wheel-number-picker__arrow--up" type="button" aria-label="Decrease value">▲</button>
|
|
60
67
|
<div class="wheel-number-picker__window">
|
|
61
68
|
<div class="wheel-number-picker__list"></div>
|
|
62
69
|
<div class="wheel-number-picker__highlight"></div>
|
|
63
70
|
<div class="wheel-number-picker__fade"></div>
|
|
64
71
|
</div>
|
|
72
|
+
<button class="wheel-number-picker__arrow wheel-number-picker__arrow--down" type="button" aria-label="Increase value">▼</button>
|
|
65
73
|
`;
|
|
66
74
|
|
|
67
75
|
this.list = this.root.querySelector('.wheel-number-picker__list');
|
|
68
|
-
this.
|
|
76
|
+
this.upArrow = this.root.querySelector('.wheel-number-picker__arrow--up');
|
|
77
|
+
this.downArrow = this.root.querySelector('.wheel-number-picker__arrow--down');
|
|
69
78
|
this.applyConfiguration();
|
|
79
|
+
this.rebuildItems();
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
rebuildItems() {
|
|
73
83
|
if (!this.list) return;
|
|
74
84
|
this.list.innerHTML = '';
|
|
75
|
-
|
|
85
|
+
this.spacerCount = Math.floor(this.visibleItems / 2);
|
|
76
86
|
|
|
77
|
-
for (let i = 0; i < spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
87
|
+
for (let i = 0; i < this.spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
78
88
|
for (let value = this.min; value <= this.max; value++) this.list.appendChild(this.createItem(value));
|
|
79
|
-
for (let i = 0; i < spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
89
|
+
for (let i = 0; i < this.spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
createItem(value) {
|
|
@@ -87,12 +97,17 @@
|
|
|
87
97
|
return item;
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
normalizeConfiguration() {
|
|
91
101
|
if (!Number.isFinite(this.itemHeight) || this.itemHeight <= 0) this.itemHeight = DEFAULTS.itemHeight;
|
|
92
102
|
if (!Number.isFinite(this.visibleItems) || this.visibleItems < 1) this.visibleItems = DEFAULTS.visibleItems;
|
|
93
103
|
|
|
94
104
|
this.visibleItems = Math.round(this.visibleItems);
|
|
95
105
|
if (this.visibleItems % 2 === 0) this.visibleItems += 1;
|
|
106
|
+
this.spacerCount = Math.floor(this.visibleItems / 2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
applyConfiguration() {
|
|
110
|
+
this.normalizeConfiguration();
|
|
96
111
|
|
|
97
112
|
this.root.style.setProperty('--wnp-item-height', `${this.itemHeight}px`);
|
|
98
113
|
this.root.style.setProperty('--wnp-visible-items', String(this.visibleItems));
|
|
@@ -105,6 +120,9 @@
|
|
|
105
120
|
|
|
106
121
|
this.root.setAttribute('item-height', String(this.itemHeight));
|
|
107
122
|
this.root.setAttribute('visible-items', String(this.visibleItems));
|
|
123
|
+
this.root.classList.toggle('wheel-number-picker--arrows', this.arrows);
|
|
124
|
+
this.root.style.setProperty('--wnp-arrows-display', this.arrows ? 'grid' : 'none');
|
|
125
|
+
this.applyArrowMediaQuery();
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
setCssVariableFromValue(name, value) {
|
|
@@ -112,18 +130,55 @@
|
|
|
112
130
|
this.root.style.setProperty(name, String(value));
|
|
113
131
|
}
|
|
114
132
|
|
|
133
|
+
normalizeBoolean(value) {
|
|
134
|
+
if (value === true || value === '') return true;
|
|
135
|
+
if (value === false || value === null || value === undefined) return false;
|
|
136
|
+
return !['false', '0', 'no', 'off'].includes(String(value).toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
applyArrowMediaQuery() {
|
|
140
|
+
if (this.arrowStyleElement) this.arrowStyleElement.remove();
|
|
141
|
+
if (!this.arrows || this.arrowsHiddenMaxWidth === '' || this.arrowsHiddenMaxWidth === null || this.arrowsHiddenMaxWidth === undefined) return;
|
|
142
|
+
|
|
143
|
+
const maxWidth = Number(this.arrowsHiddenMaxWidth);
|
|
144
|
+
if (!Number.isFinite(maxWidth) || maxWidth < 0) return;
|
|
145
|
+
|
|
146
|
+
if (!this.root.id) this.root.id = `wheel-number-picker-${Math.random().toString(36).slice(2)}`;
|
|
147
|
+
this.arrowStyleElement = document.createElement('style');
|
|
148
|
+
this.arrowStyleElement.textContent = `@media (max-width: ${maxWidth}px) { #${CSS.escape(this.root.id)} .wheel-number-picker__arrow { display: none; } }`;
|
|
149
|
+
document.head.appendChild(this.arrowStyleElement);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setArrows(arrows) {
|
|
153
|
+
this.arrows = this.normalizeBoolean(arrows);
|
|
154
|
+
this.applyConfiguration();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setArrowsHiddenMaxWidth(value) {
|
|
158
|
+
this.arrowsHiddenMaxWidth = value ?? '';
|
|
159
|
+
this.applyConfiguration();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
115
163
|
bindEvents() {
|
|
116
164
|
this.onPointerDownBound = this.onPointerDown.bind(this);
|
|
117
165
|
this.onPointerMoveBound = this.onPointerMove.bind(this);
|
|
118
166
|
this.onPointerUpBound = this.onPointerUp.bind(this);
|
|
119
167
|
this.onWheelBound = this.onWheel.bind(this);
|
|
120
168
|
this.onKeyDownBound = this.onKeyDown.bind(this);
|
|
169
|
+
this.onArrowUpClickBound = () => this.setValue(this.value - 1);
|
|
170
|
+
this.onArrowDownClickBound = () => this.setValue(this.value + 1);
|
|
171
|
+
this.onArrowPointerDownBound = event => event.stopPropagation();
|
|
121
172
|
|
|
122
173
|
this.root.addEventListener('pointerdown', this.onPointerDownBound);
|
|
123
174
|
window.addEventListener('pointermove', this.onPointerMoveBound);
|
|
124
175
|
window.addEventListener('pointerup', this.onPointerUpBound);
|
|
125
176
|
this.root.addEventListener('wheel', this.onWheelBound, { passive: false });
|
|
126
177
|
this.root.addEventListener('keydown', this.onKeyDownBound);
|
|
178
|
+
this.upArrow.addEventListener('click', this.onArrowUpClickBound);
|
|
179
|
+
this.downArrow.addEventListener('click', this.onArrowDownClickBound);
|
|
180
|
+
this.upArrow.addEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
181
|
+
this.downArrow.addEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
destroy() {
|
|
@@ -133,6 +188,12 @@
|
|
|
133
188
|
window.removeEventListener('pointerup', this.onPointerUpBound);
|
|
134
189
|
this.root.removeEventListener('wheel', this.onWheelBound);
|
|
135
190
|
this.root.removeEventListener('keydown', this.onKeyDownBound);
|
|
191
|
+
this.upArrow?.removeEventListener('click', this.onArrowUpClickBound);
|
|
192
|
+
this.downArrow?.removeEventListener('click', this.onArrowDownClickBound);
|
|
193
|
+
this.upArrow?.removeEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
194
|
+
this.downArrow?.removeEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
195
|
+
this.arrowStyleElement?.remove();
|
|
196
|
+
this.arrowStyleElement = null;
|
|
136
197
|
this.root.innerHTML = '';
|
|
137
198
|
}
|
|
138
199
|
|
|
@@ -215,7 +276,7 @@
|
|
|
215
276
|
}
|
|
216
277
|
|
|
217
278
|
snapToNearest() {
|
|
218
|
-
const rawIndex =
|
|
279
|
+
const rawIndex = this.offsetToIndex(this.offset);
|
|
219
280
|
const value = this.clamp(this.min + Math.round(rawIndex));
|
|
220
281
|
this.setValue(value);
|
|
221
282
|
}
|
|
@@ -256,9 +317,8 @@
|
|
|
256
317
|
const next = Number(visibleItems);
|
|
257
318
|
if (!Number.isFinite(next) || next < 1) return;
|
|
258
319
|
this.visibleItems = Math.round(next);
|
|
259
|
-
if (this.visibleItems % 2 === 0) this.visibleItems += 1;
|
|
260
|
-
this.rebuildItems();
|
|
261
320
|
this.applyConfiguration();
|
|
321
|
+
this.rebuildItems();
|
|
262
322
|
this.snapToValue(this.value, false);
|
|
263
323
|
}
|
|
264
324
|
|
|
@@ -272,7 +332,16 @@
|
|
|
272
332
|
}
|
|
273
333
|
|
|
274
334
|
valueToOffset(value) {
|
|
275
|
-
|
|
335
|
+
const index = Number(value) - this.min;
|
|
336
|
+
const windowCenter = (this.itemHeight * this.visibleItems) / 2;
|
|
337
|
+
const itemCenter = (this.spacerCount + index) * this.itemHeight + this.itemHeight / 2;
|
|
338
|
+
return windowCenter - itemCenter;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
offsetToIndex(offset) {
|
|
342
|
+
const windowCenter = (this.itemHeight * this.visibleItems) / 2;
|
|
343
|
+
const centeredItemPosition = windowCenter - offset - this.itemHeight / 2;
|
|
344
|
+
return centeredItemPosition / this.itemHeight - this.spacerCount;
|
|
276
345
|
}
|
|
277
346
|
|
|
278
347
|
clamp(value) {
|
|
@@ -290,7 +359,7 @@
|
|
|
290
359
|
}
|
|
291
360
|
|
|
292
361
|
updateActiveFromOffset() {
|
|
293
|
-
const rawIndex =
|
|
362
|
+
const rawIndex = this.offsetToIndex(this.offset);
|
|
294
363
|
const value = this.clamp(this.min + Math.round(rawIndex));
|
|
295
364
|
this.updateActive(value);
|
|
296
365
|
}
|
|
@@ -317,7 +386,7 @@
|
|
|
317
386
|
|
|
318
387
|
class WheelNumberPickerElement extends HTMLElement {
|
|
319
388
|
static get observedAttributes() {
|
|
320
|
-
return ['value', 'item-height', 'visible-items', 'color', 'active-color', 'muted-color', 'background'];
|
|
389
|
+
return ['value', 'item-height', 'visible-items', 'color', 'active-color', 'muted-color', 'background', 'arrows', 'arrows-hidden-max-width'];
|
|
321
390
|
}
|
|
322
391
|
|
|
323
392
|
connectedCallback() {
|
|
@@ -331,7 +400,9 @@
|
|
|
331
400
|
color: this.getAttribute('color') ?? DEFAULTS.color,
|
|
332
401
|
activeColor: this.getAttribute('active-color') ?? DEFAULTS.activeColor,
|
|
333
402
|
mutedColor: this.getAttribute('muted-color') ?? DEFAULTS.mutedColor,
|
|
334
|
-
background: this.getAttribute('background') ?? DEFAULTS.background
|
|
403
|
+
background: this.getAttribute('background') ?? DEFAULTS.background,
|
|
404
|
+
arrows: this.hasAttribute('arrows') ? this.getAttribute('arrows') : DEFAULTS.arrows,
|
|
405
|
+
arrowsHiddenMaxWidth: this.getAttribute('arrows-hidden-max-width') ?? DEFAULTS.arrowsHiddenMaxWidth
|
|
335
406
|
});
|
|
336
407
|
}
|
|
337
408
|
|
|
@@ -350,6 +421,8 @@
|
|
|
350
421
|
if (name === 'active-color') this.picker.setColors({ activeColor: newValue });
|
|
351
422
|
if (name === 'muted-color') this.picker.setColors({ mutedColor: newValue });
|
|
352
423
|
if (name === 'background') this.picker.setColors({ background: newValue });
|
|
424
|
+
if (name === 'arrows') this.picker.setArrows(newValue !== null && this.picker.normalizeBoolean(newValue));
|
|
425
|
+
if (name === 'arrows-hidden-max-width') this.picker.setArrowsHiddenMaxWidth(newValue);
|
|
353
426
|
}
|
|
354
427
|
|
|
355
428
|
get value() {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
color: '',
|
|
11
11
|
activeColor: '',
|
|
12
12
|
mutedColor: '',
|
|
13
|
-
background: ''
|
|
13
|
+
background: '',
|
|
14
|
+
arrows: false,
|
|
15
|
+
arrowsHiddenMaxWidth: ''
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
function readOption(root, options, optionName, attributeName, fallback) {
|
|
@@ -36,6 +38,9 @@
|
|
|
36
38
|
background: readOption(this.root, options, 'background', 'background', DEFAULTS.background)
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
this.arrows = this.normalizeBoolean(readOption(this.root, options, 'arrows', 'arrows', DEFAULTS.arrows));
|
|
42
|
+
this.arrowsHiddenMaxWidth = readOption(this.root, options, 'arrowsHiddenMaxWidth', 'arrows-hidden-max-width', DEFAULTS.arrowsHiddenMaxWidth);
|
|
43
|
+
|
|
39
44
|
this.isDragging = false;
|
|
40
45
|
this.startY = 0;
|
|
41
46
|
this.startOffset = 0;
|
|
@@ -45,6 +50,7 @@
|
|
|
45
50
|
this.velocity = 0;
|
|
46
51
|
this.animationFrame = null;
|
|
47
52
|
|
|
53
|
+
this.normalizeConfiguration();
|
|
48
54
|
this.render();
|
|
49
55
|
this.bindEvents();
|
|
50
56
|
this.snapToValue(this.value, false);
|
|
@@ -57,26 +63,30 @@
|
|
|
57
63
|
if (!this.root.getAttribute('aria-label')) this.root.setAttribute('aria-label', 'Number picker');
|
|
58
64
|
|
|
59
65
|
this.root.innerHTML = `
|
|
66
|
+
<button class="wheel-number-picker__arrow wheel-number-picker__arrow--up" type="button" aria-label="Decrease value">▲</button>
|
|
60
67
|
<div class="wheel-number-picker__window">
|
|
61
68
|
<div class="wheel-number-picker__list"></div>
|
|
62
69
|
<div class="wheel-number-picker__highlight"></div>
|
|
63
70
|
<div class="wheel-number-picker__fade"></div>
|
|
64
71
|
</div>
|
|
72
|
+
<button class="wheel-number-picker__arrow wheel-number-picker__arrow--down" type="button" aria-label="Increase value">▼</button>
|
|
65
73
|
`;
|
|
66
74
|
|
|
67
75
|
this.list = this.root.querySelector('.wheel-number-picker__list');
|
|
68
|
-
this.
|
|
76
|
+
this.upArrow = this.root.querySelector('.wheel-number-picker__arrow--up');
|
|
77
|
+
this.downArrow = this.root.querySelector('.wheel-number-picker__arrow--down');
|
|
69
78
|
this.applyConfiguration();
|
|
79
|
+
this.rebuildItems();
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
rebuildItems() {
|
|
73
83
|
if (!this.list) return;
|
|
74
84
|
this.list.innerHTML = '';
|
|
75
|
-
|
|
85
|
+
this.spacerCount = Math.floor(this.visibleItems / 2);
|
|
76
86
|
|
|
77
|
-
for (let i = 0; i < spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
87
|
+
for (let i = 0; i < this.spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
78
88
|
for (let value = this.min; value <= this.max; value++) this.list.appendChild(this.createItem(value));
|
|
79
|
-
for (let i = 0; i < spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
89
|
+
for (let i = 0; i < this.spacerCount; i++) this.list.appendChild(this.createItem(''));
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
createItem(value) {
|
|
@@ -87,12 +97,17 @@
|
|
|
87
97
|
return item;
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
normalizeConfiguration() {
|
|
91
101
|
if (!Number.isFinite(this.itemHeight) || this.itemHeight <= 0) this.itemHeight = DEFAULTS.itemHeight;
|
|
92
102
|
if (!Number.isFinite(this.visibleItems) || this.visibleItems < 1) this.visibleItems = DEFAULTS.visibleItems;
|
|
93
103
|
|
|
94
104
|
this.visibleItems = Math.round(this.visibleItems);
|
|
95
105
|
if (this.visibleItems % 2 === 0) this.visibleItems += 1;
|
|
106
|
+
this.spacerCount = Math.floor(this.visibleItems / 2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
applyConfiguration() {
|
|
110
|
+
this.normalizeConfiguration();
|
|
96
111
|
|
|
97
112
|
this.root.style.setProperty('--wnp-item-height', `${this.itemHeight}px`);
|
|
98
113
|
this.root.style.setProperty('--wnp-visible-items', String(this.visibleItems));
|
|
@@ -105,6 +120,9 @@
|
|
|
105
120
|
|
|
106
121
|
this.root.setAttribute('item-height', String(this.itemHeight));
|
|
107
122
|
this.root.setAttribute('visible-items', String(this.visibleItems));
|
|
123
|
+
this.root.classList.toggle('wheel-number-picker--arrows', this.arrows);
|
|
124
|
+
this.root.style.setProperty('--wnp-arrows-display', this.arrows ? 'grid' : 'none');
|
|
125
|
+
this.applyArrowMediaQuery();
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
setCssVariableFromValue(name, value) {
|
|
@@ -112,18 +130,55 @@
|
|
|
112
130
|
this.root.style.setProperty(name, String(value));
|
|
113
131
|
}
|
|
114
132
|
|
|
133
|
+
normalizeBoolean(value) {
|
|
134
|
+
if (value === true || value === '') return true;
|
|
135
|
+
if (value === false || value === null || value === undefined) return false;
|
|
136
|
+
return !['false', '0', 'no', 'off'].includes(String(value).toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
applyArrowMediaQuery() {
|
|
140
|
+
if (this.arrowStyleElement) this.arrowStyleElement.remove();
|
|
141
|
+
if (!this.arrows || this.arrowsHiddenMaxWidth === '' || this.arrowsHiddenMaxWidth === null || this.arrowsHiddenMaxWidth === undefined) return;
|
|
142
|
+
|
|
143
|
+
const maxWidth = Number(this.arrowsHiddenMaxWidth);
|
|
144
|
+
if (!Number.isFinite(maxWidth) || maxWidth < 0) return;
|
|
145
|
+
|
|
146
|
+
if (!this.root.id) this.root.id = `wheel-number-picker-${Math.random().toString(36).slice(2)}`;
|
|
147
|
+
this.arrowStyleElement = document.createElement('style');
|
|
148
|
+
this.arrowStyleElement.textContent = `@media (max-width: ${maxWidth}px) { #${CSS.escape(this.root.id)} .wheel-number-picker__arrow { display: none; } }`;
|
|
149
|
+
document.head.appendChild(this.arrowStyleElement);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setArrows(arrows) {
|
|
153
|
+
this.arrows = this.normalizeBoolean(arrows);
|
|
154
|
+
this.applyConfiguration();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setArrowsHiddenMaxWidth(value) {
|
|
158
|
+
this.arrowsHiddenMaxWidth = value ?? '';
|
|
159
|
+
this.applyConfiguration();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
115
163
|
bindEvents() {
|
|
116
164
|
this.onPointerDownBound = this.onPointerDown.bind(this);
|
|
117
165
|
this.onPointerMoveBound = this.onPointerMove.bind(this);
|
|
118
166
|
this.onPointerUpBound = this.onPointerUp.bind(this);
|
|
119
167
|
this.onWheelBound = this.onWheel.bind(this);
|
|
120
168
|
this.onKeyDownBound = this.onKeyDown.bind(this);
|
|
169
|
+
this.onArrowUpClickBound = () => this.setValue(this.value - 1);
|
|
170
|
+
this.onArrowDownClickBound = () => this.setValue(this.value + 1);
|
|
171
|
+
this.onArrowPointerDownBound = event => event.stopPropagation();
|
|
121
172
|
|
|
122
173
|
this.root.addEventListener('pointerdown', this.onPointerDownBound);
|
|
123
174
|
window.addEventListener('pointermove', this.onPointerMoveBound);
|
|
124
175
|
window.addEventListener('pointerup', this.onPointerUpBound);
|
|
125
176
|
this.root.addEventListener('wheel', this.onWheelBound, { passive: false });
|
|
126
177
|
this.root.addEventListener('keydown', this.onKeyDownBound);
|
|
178
|
+
this.upArrow.addEventListener('click', this.onArrowUpClickBound);
|
|
179
|
+
this.downArrow.addEventListener('click', this.onArrowDownClickBound);
|
|
180
|
+
this.upArrow.addEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
181
|
+
this.downArrow.addEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
destroy() {
|
|
@@ -133,6 +188,12 @@
|
|
|
133
188
|
window.removeEventListener('pointerup', this.onPointerUpBound);
|
|
134
189
|
this.root.removeEventListener('wheel', this.onWheelBound);
|
|
135
190
|
this.root.removeEventListener('keydown', this.onKeyDownBound);
|
|
191
|
+
this.upArrow?.removeEventListener('click', this.onArrowUpClickBound);
|
|
192
|
+
this.downArrow?.removeEventListener('click', this.onArrowDownClickBound);
|
|
193
|
+
this.upArrow?.removeEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
194
|
+
this.downArrow?.removeEventListener('pointerdown', this.onArrowPointerDownBound);
|
|
195
|
+
this.arrowStyleElement?.remove();
|
|
196
|
+
this.arrowStyleElement = null;
|
|
136
197
|
this.root.innerHTML = '';
|
|
137
198
|
}
|
|
138
199
|
|
|
@@ -215,7 +276,7 @@
|
|
|
215
276
|
}
|
|
216
277
|
|
|
217
278
|
snapToNearest() {
|
|
218
|
-
const rawIndex =
|
|
279
|
+
const rawIndex = this.offsetToIndex(this.offset);
|
|
219
280
|
const value = this.clamp(this.min + Math.round(rawIndex));
|
|
220
281
|
this.setValue(value);
|
|
221
282
|
}
|
|
@@ -256,9 +317,8 @@
|
|
|
256
317
|
const next = Number(visibleItems);
|
|
257
318
|
if (!Number.isFinite(next) || next < 1) return;
|
|
258
319
|
this.visibleItems = Math.round(next);
|
|
259
|
-
if (this.visibleItems % 2 === 0) this.visibleItems += 1;
|
|
260
|
-
this.rebuildItems();
|
|
261
320
|
this.applyConfiguration();
|
|
321
|
+
this.rebuildItems();
|
|
262
322
|
this.snapToValue(this.value, false);
|
|
263
323
|
}
|
|
264
324
|
|
|
@@ -272,7 +332,16 @@
|
|
|
272
332
|
}
|
|
273
333
|
|
|
274
334
|
valueToOffset(value) {
|
|
275
|
-
|
|
335
|
+
const index = Number(value) - this.min;
|
|
336
|
+
const windowCenter = (this.itemHeight * this.visibleItems) / 2;
|
|
337
|
+
const itemCenter = (this.spacerCount + index) * this.itemHeight + this.itemHeight / 2;
|
|
338
|
+
return windowCenter - itemCenter;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
offsetToIndex(offset) {
|
|
342
|
+
const windowCenter = (this.itemHeight * this.visibleItems) / 2;
|
|
343
|
+
const centeredItemPosition = windowCenter - offset - this.itemHeight / 2;
|
|
344
|
+
return centeredItemPosition / this.itemHeight - this.spacerCount;
|
|
276
345
|
}
|
|
277
346
|
|
|
278
347
|
clamp(value) {
|
|
@@ -290,7 +359,7 @@
|
|
|
290
359
|
}
|
|
291
360
|
|
|
292
361
|
updateActiveFromOffset() {
|
|
293
|
-
const rawIndex =
|
|
362
|
+
const rawIndex = this.offsetToIndex(this.offset);
|
|
294
363
|
const value = this.clamp(this.min + Math.round(rawIndex));
|
|
295
364
|
this.updateActive(value);
|
|
296
365
|
}
|
|
@@ -317,7 +386,7 @@
|
|
|
317
386
|
|
|
318
387
|
class WheelNumberPickerElement extends HTMLElement {
|
|
319
388
|
static get observedAttributes() {
|
|
320
|
-
return ['value', 'item-height', 'visible-items', 'color', 'active-color', 'muted-color', 'background'];
|
|
389
|
+
return ['value', 'item-height', 'visible-items', 'color', 'active-color', 'muted-color', 'background', 'arrows', 'arrows-hidden-max-width'];
|
|
321
390
|
}
|
|
322
391
|
|
|
323
392
|
connectedCallback() {
|
|
@@ -331,7 +400,9 @@
|
|
|
331
400
|
color: this.getAttribute('color') ?? DEFAULTS.color,
|
|
332
401
|
activeColor: this.getAttribute('active-color') ?? DEFAULTS.activeColor,
|
|
333
402
|
mutedColor: this.getAttribute('muted-color') ?? DEFAULTS.mutedColor,
|
|
334
|
-
background: this.getAttribute('background') ?? DEFAULTS.background
|
|
403
|
+
background: this.getAttribute('background') ?? DEFAULTS.background,
|
|
404
|
+
arrows: this.hasAttribute('arrows') ? this.getAttribute('arrows') : DEFAULTS.arrows,
|
|
405
|
+
arrowsHiddenMaxWidth: this.getAttribute('arrows-hidden-max-width') ?? DEFAULTS.arrowsHiddenMaxWidth
|
|
335
406
|
});
|
|
336
407
|
}
|
|
337
408
|
|
|
@@ -350,6 +421,8 @@
|
|
|
350
421
|
if (name === 'active-color') this.picker.setColors({ activeColor: newValue });
|
|
351
422
|
if (name === 'muted-color') this.picker.setColors({ mutedColor: newValue });
|
|
352
423
|
if (name === 'background') this.picker.setColors({ background: newValue });
|
|
424
|
+
if (name === 'arrows') this.picker.setArrows(newValue !== null && this.picker.normalizeBoolean(newValue));
|
|
425
|
+
if (name === 'arrows-hidden-max-width') this.picker.setArrowsHiddenMaxWidth(newValue);
|
|
353
426
|
}
|
|
354
427
|
|
|
355
428
|
get value() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-wheel-number-picker",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "A plain JavaScript wheel-style number picker with drag, inertia, snapping, keyboard support, colors, dynamic item height, and a custom element API.",
|
|
5
5
|
"main": "dist/wheel-number-picker.js",
|
|
6
6
|
"unpkg": "dist/wheel-number-picker.min.js",
|