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 +7 -0
- package/index.html +17 -0
- package/package.json +1 -1
- package/src/InputSpinner.js +57 -12
- package/test/TestInputSpinner.js +77 -0
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><input type="number"></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
package/src/InputSpinner.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
17
|
-
.replace(
|
|
18
|
-
.replace(
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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) {
|
package/test/TestInputSpinner.js
CHANGED
|
@@ -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"})
|