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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bootstrap-input-spinner",
3
- "version": "5.0.7",
3
+ "version": "5.1.0",
4
4
  "description": "A Bootstrap 5 plugin to create input spinner elements for number input.",
5
5
  "browser": "./src/InputSpinner.js",
6
6
  "type": "module",
@@ -336,7 +336,23 @@ export class InputSpinner {
336
336
  if (isNaN(self.value)) {
337
337
  self.value = 0
338
338
  }
339
- setValue(Math.round(self.value / step) * step + step)
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
 
@@ -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>