bootstrap-input-spinner 5.0.6 → 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 +21 -5
- package/test/TestInputSpinner.js +53 -0
- package/test/compare-native.html +136 -0
package/package.json
CHANGED
package/src/InputSpinner.js
CHANGED
|
@@ -46,8 +46,6 @@ const I18nEditor = function (props, element) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
let triggerKeyPressed = false
|
|
50
|
-
|
|
51
49
|
function parseTemplate(html) {
|
|
52
50
|
const tpl = document.createElement("template")
|
|
53
51
|
tpl.innerHTML = html.trim()
|
|
@@ -159,6 +157,8 @@ export class InputSpinner {
|
|
|
159
157
|
destroy()
|
|
160
158
|
}
|
|
161
159
|
|
|
160
|
+
let triggerKeyPressed = false
|
|
161
|
+
|
|
162
162
|
this.observer = new MutationObserver(function () {
|
|
163
163
|
updateAttributes()
|
|
164
164
|
setValue(self.value, true)
|
|
@@ -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
|
|
|
@@ -364,9 +380,9 @@ export class InputSpinner {
|
|
|
364
380
|
}
|
|
365
381
|
const originalClass = self.original.className || ""
|
|
366
382
|
let groupClass = ""
|
|
367
|
-
if (/form-control-sm
|
|
383
|
+
if (/form-control-sm/.test(originalClass)) {
|
|
368
384
|
groupClass = "input-group-sm"
|
|
369
|
-
} else if (/form-control-lg
|
|
385
|
+
} else if (/form-control-lg/.test(originalClass)) {
|
|
370
386
|
groupClass = "input-group-lg"
|
|
371
387
|
}
|
|
372
388
|
const inputClass = originalClass.replace(/form-control(-(sm|lg))?/g, "")
|
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", () => {
|
|
@@ -341,6 +372,28 @@ describe("InputSpinner instance isolation", () => {
|
|
|
341
372
|
assert.equal(b, 0)
|
|
342
373
|
clear()
|
|
343
374
|
})
|
|
375
|
+
it("two spinners both respond to keyboard activation while the other is held", () => {
|
|
376
|
+
const elA = createInput({value: "0", min: "0", max: "10", step: "1"})
|
|
377
|
+
const elB = createInput({value: "0", min: "0", max: "10", step: "1"})
|
|
378
|
+
new InputSpinner(elA, {autoInterval: undefined})
|
|
379
|
+
new InputSpinner(elB, {autoInterval: undefined})
|
|
380
|
+
const btnA = elA.nextElementSibling.querySelector(".btn-increment")
|
|
381
|
+
const btnB = elB.nextElementSibling.querySelector(".btn-increment")
|
|
382
|
+
const make = (type) => {
|
|
383
|
+
const e = new KeyboardEvent(type, {key: " ", bubbles: true})
|
|
384
|
+
Object.defineProperty(e, "keyCode", {value: 32})
|
|
385
|
+
return e
|
|
386
|
+
}
|
|
387
|
+
const down = () => make("keydown")
|
|
388
|
+
const up = () => make("keyup")
|
|
389
|
+
btnA.dispatchEvent(down())
|
|
390
|
+
assert.equal(elA.value, "1")
|
|
391
|
+
// B's keydown must still register even though A's keyup hasn't fired.
|
|
392
|
+
btnB.dispatchEvent(down())
|
|
393
|
+
assert.equal(elB.value, "1")
|
|
394
|
+
document.body.dispatchEvent(up())
|
|
395
|
+
clear()
|
|
396
|
+
})
|
|
344
397
|
})
|
|
345
398
|
|
|
346
399
|
describe("InputSpinner attribute observation", () => {
|
|
@@ -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>
|