bootstrap-input-spinner 5.0.7 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/InputSpinner.js +17 -1
- package/test/TestInputSpinner.js +31 -0
- package/test/compare-native.html +136 -0
package/package.json
CHANGED
package/src/InputSpinner.js
CHANGED
|
@@ -336,7 +336,23 @@ export class InputSpinner {
|
|
|
336
336
|
if (isNaN(self.value)) {
|
|
337
337
|
self.value = 0
|
|
338
338
|
}
|
|
339
|
-
|
|
339
|
+
// Step like the native <input type="number">: the stepping base is
|
|
340
|
+
// `min` if set, otherwise the `value` content attribute, otherwise 0.
|
|
341
|
+
// Valid values are `base + n * step`. On-grid values move exactly one
|
|
342
|
+
// step; off-grid values (e.g. typed manually) snap to the next valid
|
|
343
|
+
// value in the button's direction.
|
|
344
|
+
const direction = step < 0 ? -1 : 1
|
|
345
|
+
const stepSize = Math.abs(step)
|
|
346
|
+
const base = isFinite(self.min) ? self.min : parseNumberAttr(self.original, "value", 0)
|
|
347
|
+
const offset = (self.value - base) / stepSize
|
|
348
|
+
const rounded = Math.round(offset)
|
|
349
|
+
let next
|
|
350
|
+
if (Math.abs(offset - rounded) < 1e-9) {
|
|
351
|
+
next = base + (rounded + direction) * stepSize
|
|
352
|
+
} else {
|
|
353
|
+
next = base + (direction > 0 ? Math.ceil(offset) : Math.floor(offset)) * stepSize
|
|
354
|
+
}
|
|
355
|
+
setValue(next)
|
|
340
356
|
dispatchEvent(self.original, "input")
|
|
341
357
|
}
|
|
342
358
|
|
package/test/TestInputSpinner.js
CHANGED
|
@@ -212,6 +212,37 @@ describe("InputSpinner stepping", () => {
|
|
|
212
212
|
assert.equal(el.value, "4")
|
|
213
213
|
clear()
|
|
214
214
|
})
|
|
215
|
+
it("uses the value attribute as the stepping base when no min is set (regression for #56)", () => {
|
|
216
|
+
// No min → base is the value attribute (5); with step 2 the grid is
|
|
217
|
+
// ..., 1, 3, 5, 7, ... Native stepDown(5) === 3, not value - step rounded to 4.
|
|
218
|
+
const {el, group} = spin({value: "5", step: "2"})
|
|
219
|
+
pressButton(group.querySelector(".btn-decrement"))
|
|
220
|
+
assert.equal(el.value, "3")
|
|
221
|
+
clear()
|
|
222
|
+
})
|
|
223
|
+
it("steps up from the value-attribute base when no min is set (regression for #56)", () => {
|
|
224
|
+
const {el, group} = spin({value: "5", step: "2"})
|
|
225
|
+
pressButton(group.querySelector(".btn-increment"))
|
|
226
|
+
assert.equal(el.value, "7")
|
|
227
|
+
clear()
|
|
228
|
+
})
|
|
229
|
+
it("uses min as the stepping base and snaps off-grid values (regression for #56)", () => {
|
|
230
|
+
// base -99, step 20 → grid ..., -19, 1, 21, ...; the off-grid 12 snaps
|
|
231
|
+
// down to 1 and up to 21, matching native stepDown/stepUp
|
|
232
|
+
const {el, group} = spin({value: "12", min: "-99", step: "20"})
|
|
233
|
+
pressButton(group.querySelector(".btn-decrement"))
|
|
234
|
+
assert.equal(el.value, "1")
|
|
235
|
+
pressButton(group.querySelector(".btn-increment"))
|
|
236
|
+
assert.equal(el.value, "21")
|
|
237
|
+
clear()
|
|
238
|
+
})
|
|
239
|
+
it("snaps an off-grid value to the min-based grid (regression for #56)", () => {
|
|
240
|
+
// base 1, step 3 → grid 1, 4, 7, 10, ...; the off-grid 5 snaps down to 4
|
|
241
|
+
const {el, group} = spin({value: "5", min: "1", step: "3"})
|
|
242
|
+
pressButton(group.querySelector(".btn-decrement"))
|
|
243
|
+
assert.equal(el.value, "4")
|
|
244
|
+
clear()
|
|
245
|
+
})
|
|
215
246
|
})
|
|
216
247
|
|
|
217
248
|
describe("InputSpinner dynamic step while holding", () => {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>bootstrap-input-spinner vs. native input[type=number]</title>
|
|
7
|
+
<link rel="stylesheet"
|
|
8
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
|
9
|
+
crossorigin="anonymous">
|
|
10
|
+
<style>
|
|
11
|
+
body { padding: 2rem; max-width: 880px; }
|
|
12
|
+
.case { border: 1px solid var(--bs-border-color); border-radius: .5rem; padding: 1rem 1.25rem; margin-bottom: 1rem; }
|
|
13
|
+
.case h2 { font-size: 1rem; margin: 0 0 .75rem; }
|
|
14
|
+
.case code { color: var(--bs-pink); }
|
|
15
|
+
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: start; }
|
|
16
|
+
.col-label { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--bs-secondary-color); margin-bottom: .35rem; }
|
|
17
|
+
.native input { max-width: 12rem; }
|
|
18
|
+
.grid-hint { font-size: .8rem; color: var(--bs-secondary-color); margin-top: .4rem; }
|
|
19
|
+
.note { background: var(--bs-light); border-radius: .5rem; padding: .75rem 1rem; font-size: .9rem; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<h1 class="h4 mb-3">Spinner vs. native <code>input[type=number]</code></h1>
|
|
24
|
+
<p class="text-secondary">
|
|
25
|
+
Click the up/down controls (native on the left, the spinner on the right) and compare.
|
|
26
|
+
Both fields carry the same attributes. The stepping base is <code>min</code>, otherwise the
|
|
27
|
+
<code>value</code> attribute, otherwise 0; valid values are <code>base + n·step</code>.
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<div id="cases"></div>
|
|
31
|
+
|
|
32
|
+
<div class="note">
|
|
33
|
+
<strong>About manual typing:</strong> native Chrome accepts an off-grid value that is typed
|
|
34
|
+
in and only flags it as invalid on form validation. The spinner likewise leaves a typed value
|
|
35
|
+
untouched, but a button click snaps it, like the native arrows, to the nearest valid grid value
|
|
36
|
+
in the click direction.
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script type="module">
|
|
40
|
+
import {InputSpinner} from "../src/InputSpinner.js"
|
|
41
|
+
|
|
42
|
+
// Each case renders the same attributes once as a native control and once as a spinner.
|
|
43
|
+
const cases = [
|
|
44
|
+
{
|
|
45
|
+
title: "No min: the value attribute is the base (core of #56)",
|
|
46
|
+
desc: "value 5, step 2 → base 5 → grid …,1,3,5,7,…",
|
|
47
|
+
attrs: {value: 5, step: 2},
|
|
48
|
+
grid: "Expected native: down 3, up 7"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: "min as the stepping base",
|
|
52
|
+
desc: "value 12, step 20, min -99 → grid …,-19,1,21,41,…",
|
|
53
|
+
attrs: {value: 12, step: 20, min: -99},
|
|
54
|
+
grid: "Expected native: down 1, up 21"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: "On-grid default case",
|
|
58
|
+
desc: "value 5, step 1, min 0, max 10",
|
|
59
|
+
attrs: {value: 5, step: 1, min: 0, max: 10},
|
|
60
|
+
grid: "Expected: down 4, up 6"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "On-grid with min",
|
|
64
|
+
desc: "value 7, step 3, min 1 → grid 1,4,7,10,…",
|
|
65
|
+
attrs: {value: 7, step: 3, min: 1},
|
|
66
|
+
grid: "7 is on the grid → down 4, up 10"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: "Off-grid with min",
|
|
70
|
+
desc: "value 5, step 3, min 1 → grid 1,4,7,10,…",
|
|
71
|
+
attrs: {value: 5, step: 3, min: 1},
|
|
72
|
+
grid: "5 is off the grid → native down 4, up 7"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Decimal values",
|
|
76
|
+
desc: "value 0.5, step 0.3, min 0, data-decimals 1 → grid 0,0.3,0.6,…",
|
|
77
|
+
attrs: {value: 0.5, step: 0.3, min: 0, "data-decimals": 1},
|
|
78
|
+
grid: "0.5 is off the grid → native down 0.3, up 0.6"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
function makeInput(attrs) {
|
|
83
|
+
const input = document.createElement("input")
|
|
84
|
+
input.type = "number"
|
|
85
|
+
input.className = "form-control"
|
|
86
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
87
|
+
input.setAttribute(k, String(v))
|
|
88
|
+
}
|
|
89
|
+
return input
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const container = document.getElementById("cases")
|
|
93
|
+
for (const c of cases) {
|
|
94
|
+
const card = document.createElement("div")
|
|
95
|
+
card.className = "case"
|
|
96
|
+
|
|
97
|
+
const h = document.createElement("h2")
|
|
98
|
+
h.textContent = c.title
|
|
99
|
+
card.appendChild(h)
|
|
100
|
+
|
|
101
|
+
const d = document.createElement("div")
|
|
102
|
+
d.className = "text-secondary mb-3"
|
|
103
|
+
d.style.fontSize = ".9rem"
|
|
104
|
+
d.textContent = c.desc
|
|
105
|
+
card.appendChild(d)
|
|
106
|
+
|
|
107
|
+
const cols = document.createElement("div")
|
|
108
|
+
cols.className = "cols"
|
|
109
|
+
|
|
110
|
+
const nativeCol = document.createElement("div")
|
|
111
|
+
nativeCol.className = "native"
|
|
112
|
+
nativeCol.innerHTML = '<div class="col-label">Native</div>'
|
|
113
|
+
nativeCol.appendChild(makeInput(c.attrs))
|
|
114
|
+
|
|
115
|
+
const spinnerCol = document.createElement("div")
|
|
116
|
+
spinnerCol.innerHTML = '<div class="col-label">Spinner</div>'
|
|
117
|
+
const spinnerInput = makeInput(c.attrs)
|
|
118
|
+
spinnerCol.appendChild(spinnerInput)
|
|
119
|
+
|
|
120
|
+
cols.appendChild(nativeCol)
|
|
121
|
+
cols.appendChild(spinnerCol)
|
|
122
|
+
card.appendChild(cols)
|
|
123
|
+
|
|
124
|
+
const hint = document.createElement("div")
|
|
125
|
+
hint.className = "grid-hint"
|
|
126
|
+
hint.textContent = c.grid
|
|
127
|
+
card.appendChild(hint)
|
|
128
|
+
|
|
129
|
+
container.appendChild(card)
|
|
130
|
+
|
|
131
|
+
// Initialise the spinner after the input is in the DOM.
|
|
132
|
+
new InputSpinner(spinnerInput)
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|