bootstrap-input-spinner 5.0.4 → 5.0.6

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 CHANGED
@@ -124,6 +124,7 @@ const props = {
124
124
  autoInterval: 50, // speed of auto value change, set to `undefined` to disable auto-change
125
125
  buttonsOnly: false, // set this `true` to disable the possibility to enter or paste the number via keyboard
126
126
  keyboardStepping: true, // set this to `false` to disallow the use of the up and down arrow keys to step
127
+ mouseWheel: false, // set to `true` to step the value on mouse wheel when the input is focused
127
128
  locale: navigator.language, // the locale, per default detected automatically from the browser
128
129
  editor: I18nEditor, // the editor (parsing and rendering of the input)
129
130
  template: // the template of the input
@@ -175,6 +176,12 @@ the plus and minus buttons still allow to change the value.
175
176
  In `keyboardStepping` mode (set `true`) allows the use of the up/down arrow keys to increase/decrease the number by the
176
177
  step.
177
178
 
179
+ ##### mouseWheel
180
+
181
+ Off by default, matching modern browsers which no longer wheel-step native `<input type="number">` elements. Set to
182
+ `true` to enable mouse-wheel stepping. The wheel listener is only attached while the input has focus, so an unfocused
183
+ spinner never hijacks page scroll. Scroll up increases the value, scroll down decreases it.
184
+
178
185
  ##### locale
179
186
 
180
187
  Used to format the number in the UI. Detected automatically from the user's browser, can be set to "de", "en",… or "
package/index.html CHANGED
@@ -212,6 +212,22 @@ inputGross.addEventListener("input", function (event) {
212
212
  </script>
213
213
  <pre><code class="language-js">new InputSpinner(element, {buttonsOnly: true, autoInterval: undefined})</code></pre>
214
214
 
215
+ <h3>Mouse wheel stepping</h3>
216
+ <p>
217
+ Off by default, matching what modern browsers do with the native <code>&lt;input type="number"&gt;</code>.
218
+ Pass <code>mouseWheel: true</code> to enable it. The listener is attached only while the input has focus,
219
+ so an unfocused spinner never hijacks page scroll.
220
+ </p>
221
+ <p>
222
+ <label for="inputMouseWheel">Focus and scroll to step</label>
223
+ <input id="inputMouseWheel" type="number" value="50" min="0" max="100" step="1"/>
224
+ </p>
225
+ <script type="module">
226
+ import {InputSpinner} from "./src/InputSpinner.js"
227
+ new InputSpinner(document.getElementById("inputMouseWheel"), {mouseWheel: true})
228
+ </script>
229
+ <pre><code class="language-js">new InputSpinner(element, {mouseWheel: true})</code></pre>
230
+
215
231
  <h3>Dynamically handling of the <code>class</code> attribute</h3>
216
232
  <p>
217
233
  <input id="inputChangeClass" class="is-valid" type="number" value="50"/>
@@ -471,6 +487,7 @@ new InputSpinner(document.getElementById("timeEditor"), {editor: TimeEditor})</c
471
487
  import {InputSpinner} from "./src/InputSpinner.js"
472
488
 
473
489
  for (const el of document.querySelectorAll("input[type='number']")) {
490
+ if (el["bootstrap-input-spinner"]) continue
474
491
  new InputSpinner(el)
475
492
  }
476
493
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bootstrap-input-spinner",
3
- "version": "5.0.4",
3
+ "version": "5.0.6",
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",
@@ -8,26 +8,41 @@
8
8
  const I18nEditor = function (props, element) {
9
9
  const locale = props.locale || "en-US"
10
10
 
11
+ let parseRegexes = null
11
12
  this.parse = function (customFormat) {
12
- const numberFormat = new Intl.NumberFormat(locale)
13
- const thousandSeparator = numberFormat.format(11111).replace(/1/g, '') || '.'
14
- const decimalSeparator = numberFormat.format(1.1).replace(/1/g, '')
13
+ if (!parseRegexes) {
14
+ const fmt = new Intl.NumberFormat(locale)
15
+ const thousandSeparator = fmt.format(11111).replace(/1/g, '') || '.'
16
+ const decimalSeparator = fmt.format(1.1).replace(/1/g, '')
17
+ parseRegexes = {
18
+ space: / /g,
19
+ thousand: new RegExp('\\' + thousandSeparator, 'g'),
20
+ decimal: new RegExp('\\' + decimalSeparator)
21
+ }
22
+ }
15
23
  return parseFloat(customFormat
16
- .replace(new RegExp(' ', 'g'), '')
17
- .replace(new RegExp('\\' + thousandSeparator, 'g'), '')
18
- .replace(new RegExp('\\' + decimalSeparator), '.')
24
+ .replace(parseRegexes.space, '')
25
+ .replace(parseRegexes.thousand, '')
26
+ .replace(parseRegexes.decimal, '.')
19
27
  )
20
28
  }
21
29
 
30
+ let renderFmt = null
31
+ let renderDecimals = -1
32
+ let renderGrouping = null
22
33
  this.render = function (number) {
23
34
  const decimals = parseInt(element.getAttribute("data-decimals")) || 0
24
35
  const digitGrouping = !(element.getAttribute("data-digit-grouping") === "false")
25
- const numberFormat = new Intl.NumberFormat(locale, {
26
- minimumFractionDigits: decimals,
27
- maximumFractionDigits: decimals,
28
- useGrouping: digitGrouping
29
- })
30
- return numberFormat.format(number)
36
+ if (!renderFmt || decimals !== renderDecimals || digitGrouping !== renderGrouping) {
37
+ renderFmt = new Intl.NumberFormat(locale, {
38
+ minimumFractionDigits: decimals,
39
+ maximumFractionDigits: decimals,
40
+ useGrouping: digitGrouping
41
+ })
42
+ renderDecimals = decimals
43
+ renderGrouping = digitGrouping
44
+ }
45
+ return renderFmt.format(number)
31
46
  }
32
47
  }
33
48
 
@@ -65,6 +80,7 @@ export class InputSpinner {
65
80
  autoInterval: 50, // speed of auto value change, set to `undefined` to disable auto-change
66
81
  buttonsOnly: false, // set this `true` to disable the possibility to enter or paste the number via keyboard
67
82
  keyboardStepping: true, // set this to `false` to disallow the use of the up and down arrow keys to step
83
+ mouseWheel: false, // set `true` to step the value on wheel when the input is focused. Off by default — modern browsers no longer wheel-step native <input type="number">.
68
84
  locale: navigator.language, // the locale, per default detected automatically from the browser
69
85
  editor: I18nEditor, // the editor (parsing and rendering of the input)
70
86
  template: // the template of the input
@@ -202,6 +218,35 @@ export class InputSpinner {
202
218
  }
203
219
  })
204
220
 
221
+ // Focus-gated mouse-wheel stepping: matches native <input type="number">.
222
+ // The listener is attached on focus and detached on blur, so an
223
+ // unfocused spinner never hijacks page scroll and no passive-listener
224
+ // warnings are produced while the input is idle.
225
+ const onWheel = function (event) {
226
+ if (self.input.disabled || self.input.readOnly) return
227
+ if (event.deltaY === 0) return
228
+ event.preventDefault()
229
+ // Scroll up → increment (macOS natural-scroll convention:
230
+ // pushing the wheel/trackpad up yields deltaY > 0).
231
+ const direction = event.deltaY > 0 ? 1 : -1
232
+ calcStep(direction * self.step)
233
+ dispatchEvent(self.original, "change")
234
+ }
235
+ let wheelBound = false
236
+ const attachWheel = function () {
237
+ if (wheelBound || !self.props.mouseWheel) return
238
+ self.input.addEventListener("wheel", onWheel, {passive: false})
239
+ wheelBound = true
240
+ }
241
+ const detachWheel = function () {
242
+ if (!wheelBound) return
243
+ self.input.removeEventListener("wheel", onWheel, {passive: false})
244
+ wheelBound = false
245
+ }
246
+ bind(this.input, "focus", attachWheel)
247
+ bind(this.input, "blur", detachWheel)
248
+ self._teardown.push(detachWheel)
249
+
205
250
  // decrement button
206
251
  onPointerDown(self.buttonDecrement, function () {
207
252
  if (!self.buttonDecrement.disabled) {
@@ -95,6 +95,33 @@ describe("InputSpinner I18n rendering (default editor)", () => {
95
95
  assert.equal(group.querySelector("input").value, "1.234,5")
96
96
  clear()
97
97
  })
98
+ it("re-renders when data-decimals changes (formatter cache invalidates)", async () => {
99
+ const {el, group} = spin({value: "4.5", "data-decimals": "0"}, {locale: "en-US"})
100
+ assert.equal(group.querySelector("input").value, "5")
101
+ el.setAttribute("data-decimals", "2")
102
+ await wait()
103
+ assert.equal(group.querySelector("input").value, "4.50")
104
+ clear()
105
+ })
106
+ it("re-renders when data-digit-grouping changes (formatter cache invalidates)", async () => {
107
+ const {el, group} = spin({value: "12345"}, {locale: "en-US"})
108
+ assert.equal(group.querySelector("input").value, "12,345")
109
+ el.setAttribute("data-digit-grouping", "false")
110
+ await wait()
111
+ assert.equal(group.querySelector("input").value, "12345")
112
+ clear()
113
+ })
114
+ it("parses i18n input round-trips after multiple calls (separator cache stable)", () => {
115
+ const {el, group} = spin({value: "0", "data-decimals": "2"}, {locale: "de-DE"})
116
+ const input = group.querySelector("input")
117
+ input.value = "1.234,56"
118
+ input.dispatchEvent(new Event("input", {bubbles: true}))
119
+ assert.equal(parseFloat(el.value), 1234.56)
120
+ input.value = "9.876,54"
121
+ input.dispatchEvent(new Event("input", {bubbles: true}))
122
+ assert.equal(parseFloat(el.value), 9876.54)
123
+ clear()
124
+ })
98
125
  })
99
126
 
100
127
  describe("InputSpinner setValue", () => {
@@ -214,6 +241,56 @@ describe("InputSpinner dynamic step while holding", () => {
214
241
  })
215
242
  })
216
243
 
244
+ describe("InputSpinner mouse wheel", () => {
245
+ function wheel(input, deltaY) {
246
+ input.dispatchEvent(new WheelEvent("wheel", {deltaY, cancelable: true, bubbles: true}))
247
+ }
248
+ it("is disabled by default (matches modern native behavior)", () => {
249
+ const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"})
250
+ const visible = group.querySelector("input")
251
+ visible.focus()
252
+ wheel(visible, 100)
253
+ wheel(visible, -100)
254
+ assert.equal(el.value, "5")
255
+ clear()
256
+ })
257
+ it("scroll up (positive deltaY) increments when enabled and focused (#132)", () => {
258
+ const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"}, {mouseWheel: true})
259
+ const visible = group.querySelector("input")
260
+ visible.focus()
261
+ wheel(visible, 100)
262
+ assert.equal(el.value, "6")
263
+ clear()
264
+ })
265
+ it("scroll down (negative deltaY) decrements when enabled and focused", () => {
266
+ const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"}, {mouseWheel: true})
267
+ const visible = group.querySelector("input")
268
+ visible.focus()
269
+ wheel(visible, -100)
270
+ assert.equal(el.value, "4")
271
+ clear()
272
+ })
273
+ it("does not step when the input is not focused (#115)", () => {
274
+ const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"}, {mouseWheel: true})
275
+ const visible = group.querySelector("input")
276
+ // never focus
277
+ wheel(visible, 100)
278
+ assert.equal(el.value, "5")
279
+ clear()
280
+ })
281
+ it("stops stepping after the input blurs", () => {
282
+ const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"}, {mouseWheel: true})
283
+ const visible = group.querySelector("input")
284
+ visible.focus()
285
+ wheel(visible, 100)
286
+ assert.equal(el.value, "6")
287
+ visible.blur()
288
+ wheel(visible, 100)
289
+ assert.equal(el.value, "6")
290
+ clear()
291
+ })
292
+ })
293
+
217
294
  describe("InputSpinner events", () => {
218
295
  it("dispatches 'input' when stepping", async () => {
219
296
  const {el, group} = spin({value: "5", min: "0", max: "10"})