bootstrap-input-spinner 4.1.1 → 5.0.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/README.md +41 -37
- package/index.html +106 -152
- package/package.json +8 -3
- package/src/InputSpinner.js +215 -208
- package/test/TestInputSpinner.js +10 -7
- package/test/index.html +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# bootstrap-input-spinner
|
|
2
2
|
|
|
3
|
-
A Bootstrap extension to create input spinner elements for number input.
|
|
3
|
+
A Bootstrap 5 extension to create input spinner elements for number input. Zero dependencies other than Bootstrap 5 — **no jQuery required** since v5.0.0.
|
|
4
4
|
|
|
5
|
-
> Note: bootstrap-input-spinner is now
|
|
5
|
+
> Note: bootstrap-input-spinner is now an ES6 module. The legacy ES5 version has been removed; if you still need it, pin to npm `3.x`. If you still need jQuery integration, pin to `4.x`.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
*Examples with floating-point and german localization*
|
|
@@ -72,7 +72,7 @@ Create the element in HTML. The attributes are compatible to the native `input[t
|
|
|
72
72
|
</script>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
That's it. **No extra css needed**, just Bootstrap 5
|
|
75
|
+
That's it. **No extra css needed**, just Bootstrap 5.
|
|
76
76
|
|
|
77
77
|
## API Reference
|
|
78
78
|
|
|
@@ -98,11 +98,13 @@ The InputSpinner also handles the standard input attributes `required`, `disable
|
|
|
98
98
|
|
|
99
99
|
### Create an instance in JavaScript
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
Instantiate the `InputSpinner` class on any `<input type="number">` element. You may provide additional configuration
|
|
102
|
+
in an object as a second parameter.
|
|
103
103
|
|
|
104
104
|
```js
|
|
105
|
-
|
|
105
|
+
import {InputSpinner} from "bootstrap-input-spinner/src/InputSpinner.js"
|
|
106
|
+
|
|
107
|
+
new InputSpinner(element, config)
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
#### Configuration (props)
|
|
@@ -177,12 +179,20 @@ step.
|
|
|
177
179
|
Used to format the number in the UI. Detected automatically from the user's browser, can be set to "de", "en",… or "
|
|
178
180
|
de_DE", "en_GB",….
|
|
179
181
|
|
|
180
|
-
##### editor
|
|
182
|
+
##### editor
|
|
183
|
+
|
|
184
|
+
An Editor defines how the input is parsed and rendered. The default editor is the internal `I18nEditor`, which
|
|
185
|
+
parses and renders an internationalized number. Additional editors live in `src/custom-editors.js` and are available
|
|
186
|
+
as named ES exports:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import {RawEditor, TimeEditor} from "bootstrap-input-spinner/src/custom-editors.js"
|
|
190
|
+
|
|
191
|
+
new InputSpinner(element, {editor: TimeEditor})
|
|
192
|
+
```
|
|
181
193
|
|
|
182
|
-
An Editor
|
|
183
|
-
|
|
184
|
-
must implement the two functions `parse(customValue)`, to parse the input to a number and `render(number)` to render the
|
|
185
|
-
number to the spinner input.
|
|
194
|
+
An Editor must implement two functions: `parse(customFormat)` to turn the input string into a number, and
|
|
195
|
+
`render(number)` to format the number back for display.
|
|
186
196
|
|
|
187
197
|
The simplest custom Editor is the `RawEditor`, it renders just the value und parses just the value, without any changes,
|
|
188
198
|
like a native number input. It looks like this:
|
|
@@ -215,14 +225,14 @@ To modify the look completely, you can use the template parameter. There is an e
|
|
|
215
225
|
|
|
216
226
|
### Programmatic change and read of value
|
|
217
227
|
|
|
218
|
-
|
|
228
|
+
Read via `element.value`, write via `element.setValue(newValue)`:
|
|
219
229
|
|
|
220
230
|
```javascript
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
const currentValue = element.value // read
|
|
232
|
+
element.setValue(newValue) // write
|
|
223
233
|
```
|
|
224
234
|
|
|
225
|
-
>
|
|
235
|
+
> Writing directly with `element.value = 5` bypasses the editor rendering, so always use `setValue` to update the spinner value programmatically.
|
|
226
236
|
|
|
227
237
|
### Handling attributes
|
|
228
238
|
|
|
@@ -237,34 +247,22 @@ is dynamically set to `input-group-sm` or `input-group-lg`.
|
|
|
237
247
|
|
|
238
248
|
### Events
|
|
239
249
|
|
|
240
|
-
The InputSpinner
|
|
241
|
-
|
|
242
|
-
#### Event handling with vanilla JavaScript
|
|
250
|
+
The InputSpinner dispatches native `input` and `change` events on the original element, just like a native number input.
|
|
243
251
|
|
|
244
252
|
```javascript
|
|
245
253
|
element.addEventListener("change", function (event) {
|
|
246
|
-
newValue =
|
|
247
|
-
})
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
#### Event handling with jQuery syntax
|
|
251
|
-
|
|
252
|
-
```javascript
|
|
253
|
-
$(element).on("change", function (event) {
|
|
254
|
-
newValue = $(this).val()
|
|
254
|
+
const newValue = event.target.value
|
|
255
255
|
})
|
|
256
256
|
```
|
|
257
257
|
|
|
258
258
|
### Methods
|
|
259
259
|
|
|
260
|
-
Methods are passed as string values instead of the options object.
|
|
261
|
-
|
|
262
260
|
#### destroy
|
|
263
261
|
|
|
264
262
|
Removes the InputSpinner and shows the original input element.
|
|
265
263
|
|
|
266
264
|
```javascript
|
|
267
|
-
|
|
265
|
+
element.destroyInputSpinner()
|
|
268
266
|
```
|
|
269
267
|
|
|
270
268
|
## Minified version
|
|
@@ -280,21 +278,27 @@ Just install uglify
|
|
|
280
278
|
npm install uglify-js -g
|
|
281
279
|
```
|
|
282
280
|
|
|
283
|
-
and then in the src
|
|
281
|
+
and then in the `src` folder
|
|
284
282
|
|
|
285
283
|
```bash
|
|
286
|
-
uglifyjs
|
|
284
|
+
uglifyjs InputSpinner.js --compress --mangle > InputSpinner.min.js
|
|
287
285
|
```
|
|
288
286
|
|
|
289
287
|
Violà! :)
|
|
290
288
|
|
|
291
|
-
##
|
|
289
|
+
## Testing
|
|
290
|
+
|
|
291
|
+
There is a [Teevi](https://github.com/shaack/teevi) based browser test suite under `test/`. Serve the repo with any
|
|
292
|
+
static server and open `test/index.html` in a browser to run it:
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
```bash
|
|
295
|
+
npx http-server -p 8080
|
|
296
|
+
# then open http://localhost:8080/test/
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Browser support
|
|
294
300
|
|
|
295
|
-
|
|
296
|
-
**"Intl is not defined"** (See [issue #34](https://github.com/shaack/bootstrap-input-spinner/issues/34)), just use a
|
|
297
|
-
shim or polyfill like [Intl.js](https://github.com/andyearnshaw/Intl.js), and it works.
|
|
301
|
+
The spinner works in all modern browsers that support ES modules.
|
|
298
302
|
|
|
299
303
|
---
|
|
300
304
|
|
package/index.html
CHANGED
|
@@ -22,23 +22,19 @@
|
|
|
22
22
|
max-width: 250px;
|
|
23
23
|
}
|
|
24
24
|
</style>
|
|
25
|
-
<!-- TODO remove jQuery -->
|
|
26
|
-
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
|
|
27
|
-
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
|
28
25
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
|
29
|
-
<script src="src/custom-editors.js"></script>
|
|
30
26
|
<script src="./node_modules/prismjs/prism.js"></script>
|
|
31
27
|
</head>
|
|
32
28
|
<body>
|
|
33
29
|
<section class="container py-5">
|
|
34
30
|
<h1>bootstrap-input-spinner</h1>
|
|
35
31
|
<p>
|
|
36
|
-
A Bootstrap
|
|
37
|
-
<a href="https://shaack.com/en">shaack.com</a> engineering.
|
|
32
|
+
A Bootstrap 5 plugin to create input spinner elements for number input, by
|
|
33
|
+
<a href="https://shaack.com/en">shaack.com</a> engineering. Zero dependencies other than Bootstrap 5.
|
|
38
34
|
</p>
|
|
39
|
-
<p>This version is compatible with Bootstrap 5
|
|
40
|
-
<a href="https://github.com/shaack/bootstrap-input-spinner/tree/bootstrap4-compatible">bootstrap4-compatible</a
|
|
41
|
-
npm versions 3.x are Bootstrap 5 compatible, versions 2.x Bootstrap 4 compatible.</p>
|
|
35
|
+
<p>This version is compatible with Bootstrap 5. For Bootstrap 4 use the
|
|
36
|
+
<a href="https://github.com/shaack/bootstrap-input-spinner/tree/bootstrap4-compatible">bootstrap4-compatible</a>
|
|
37
|
+
branch. npm versions 3.x are Bootstrap 5 compatible, versions 2.x Bootstrap 4 compatible.</p>
|
|
42
38
|
<p>
|
|
43
39
|
License: <a href="https://github.com/shaack/bootstrap-input-spinner/blob/master/LICENSE">MIT</a>
|
|
44
40
|
</p>
|
|
@@ -58,10 +54,7 @@
|
|
|
58
54
|
allows setting a <b>prefix</b> or <b>suffix</b> text in the input,
|
|
59
55
|
</li>
|
|
60
56
|
<li>
|
|
61
|
-
handles <
|
|
62
|
-
</li>
|
|
63
|
-
<li>
|
|
64
|
-
<b>dynamically handles</b> changing <b>attribute values</b> like <code>disabled</code> oder
|
|
57
|
+
<b>dynamically handles</b> changing <b>attribute values</b> like <code>disabled</code> or
|
|
65
58
|
<code>class</code>,
|
|
66
59
|
</li>
|
|
67
60
|
<li>
|
|
@@ -77,9 +70,11 @@
|
|
|
77
70
|
This script enables the InputSpinner for all inputs with <code>type='number'</code>.
|
|
78
71
|
<b>No extra css needed</b>, just Bootstrap 5.
|
|
79
72
|
</p>
|
|
80
|
-
<pre><code class="language-html"><script
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
<pre><code class="language-html"><script type="module">
|
|
74
|
+
import {InputSpinner} from "./src/InputSpinner.js"
|
|
75
|
+
for (const el of document.querySelectorAll("input[type='number']")) {
|
|
76
|
+
new InputSpinner(el)
|
|
77
|
+
}
|
|
83
78
|
</script></code></pre>
|
|
84
79
|
<h2>Repository, documentation and npm package</h2>
|
|
85
80
|
<p>Find the source code, more documentation and the npm package at</p>
|
|
@@ -109,8 +104,7 @@
|
|
|
109
104
|
class="language-html"><input type="number" value="4.5" data-decimals="2" min="0" max="9" step="0.1"/></code></pre>
|
|
110
105
|
|
|
111
106
|
<h3>Handle <code>change</code> and <code>input</code> events and
|
|
112
|
-
read the value from JavaScript
|
|
113
|
-
with <code>val()</code></h3>
|
|
107
|
+
read the value from JavaScript</h3>
|
|
114
108
|
<p>
|
|
115
109
|
Type in a number to see the difference between <code>change</code> and <code>input</code> events.
|
|
116
110
|
</p>
|
|
@@ -122,32 +116,21 @@
|
|
|
122
116
|
Value on change: <span id="valueOnChange"></span>
|
|
123
117
|
</p>
|
|
124
118
|
<script>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
var $valueOnChange = $("#valueOnChange")
|
|
128
|
-
$changedInput.on("input", function (event) {
|
|
129
|
-
console.log("on input", event)
|
|
130
|
-
$valueOnInput.html($(event.target).val())
|
|
131
|
-
// or $valueOnInput.html(event.target.value) // in vanilla js
|
|
132
|
-
// or $valueOnInput.html($changedInput.val())
|
|
119
|
+
document.getElementById("changedInput").addEventListener("input", function (event) {
|
|
120
|
+
document.getElementById("valueOnInput").textContent = event.target.value
|
|
133
121
|
})
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
$valueOnChange.html($(event.target).val())
|
|
122
|
+
document.getElementById("changedInput").addEventListener("change", function (event) {
|
|
123
|
+
document.getElementById("valueOnChange").textContent = event.target.value
|
|
137
124
|
})
|
|
138
125
|
</script>
|
|
139
|
-
<pre><code class="language-javascript">
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
$changedInput.on("input", function (event) {
|
|
143
|
-
$valueOnInput.html($(event.target).val())
|
|
144
|
-
// or $valueOnInput.html(event.target.value) // in vanilla js
|
|
145
|
-
// or $valueOnInput.html($changedInput.val())
|
|
126
|
+
<pre><code class="language-javascript">const changedInput = document.getElementById("changedInput")
|
|
127
|
+
changedInput.addEventListener("input", function (event) {
|
|
128
|
+
document.getElementById("valueOnInput").textContent = event.target.value
|
|
146
129
|
})
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
changedInput.addEventListener("change", function (event) {
|
|
131
|
+
document.getElementById("valueOnChange").textContent = event.target.value
|
|
149
132
|
})</code></pre>
|
|
150
|
-
<h3>Programmatic changing the value
|
|
133
|
+
<h3>Programmatic changing the value</h3>
|
|
151
134
|
<p>
|
|
152
135
|
<label for="inputNet">Net</label>
|
|
153
136
|
<input type="number" id="inputNet" value="100" min="0" max="10000" step="0.01" data-decimals="2"/>
|
|
@@ -157,24 +140,23 @@ $changedInput.on("change", function (event) {
|
|
|
157
140
|
<input type="number" id="inputGross" value="100" min="0" max="11900" step="0.01" data-decimals="2"/>
|
|
158
141
|
</p>
|
|
159
142
|
<script>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
143
|
+
window.addEventListener("load", function () {
|
|
144
|
+
const inputNet = document.getElementById("inputNet")
|
|
145
|
+
const inputGross = document.getElementById("inputGross")
|
|
146
|
+
inputNet.addEventListener("input", function (event) {
|
|
147
|
+
inputGross.setValue(event.target.value * 1.19)
|
|
148
|
+
})
|
|
149
|
+
inputGross.addEventListener("input", function (event) {
|
|
150
|
+
inputNet.setValue(event.target.value / 1.19)
|
|
151
|
+
})
|
|
152
|
+
inputGross.setValue(inputNet.value * 1.19)
|
|
167
153
|
})
|
|
168
|
-
$inputGross.val($inputNet.val() * 1.19)
|
|
169
154
|
</script>
|
|
170
|
-
<pre><code class="language-javascript"
|
|
171
|
-
|
|
172
|
-
// or $inputGross[0].setValue(event.target.value * 1.19) // in vanilla js
|
|
173
|
-
// or $inputGross.val($inputNet.val() * 1.19)
|
|
174
|
-
// do all the same
|
|
155
|
+
<pre><code class="language-javascript">inputNet.addEventListener("input", function (event) {
|
|
156
|
+
inputGross.setValue(event.target.value * 1.19)
|
|
175
157
|
})
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
inputGross.addEventListener("input", function (event) {
|
|
159
|
+
inputNet.setValue(event.target.value / 1.19)
|
|
178
160
|
})</code></pre>
|
|
179
161
|
<h3>Attributes <code>placeholder</code> and <code>required</code></h3>
|
|
180
162
|
<form>
|
|
@@ -197,10 +179,8 @@ $inputGross.on("input", function (event) {
|
|
|
197
179
|
<label class="form-check-label" for="disabledSwitch">Disabled</label>
|
|
198
180
|
</div>
|
|
199
181
|
<script>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
$disabledSwitch.on("change", function () {
|
|
203
|
-
$inputDisabled.prop("disabled", $(this).prop("checked"))
|
|
182
|
+
document.getElementById("disabledSwitch").addEventListener("change", function (event) {
|
|
183
|
+
document.getElementById("inputDisabled").disabled = event.target.checked
|
|
204
184
|
})
|
|
205
185
|
</script>
|
|
206
186
|
<pre><code class="language-html"><input id="inputDisabled" disabled type="number" value="50"/>
|
|
@@ -209,10 +189,8 @@ $inputGross.on("input", function (event) {
|
|
|
209
189
|
<label class="form-check-label" for="disabledSwitch">Disabled</label>
|
|
210
190
|
</div>
|
|
211
191
|
<script>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
$disabledSwitch.on("change", function () {
|
|
215
|
-
$inputDisabled.prop("disabled", $(this).prop("checked"))
|
|
192
|
+
document.getElementById("disabledSwitch").addEventListener("change", function (event) {
|
|
193
|
+
document.getElementById("inputDisabled").disabled = event.target.checked
|
|
216
194
|
})
|
|
217
195
|
</script></code></pre>
|
|
218
196
|
</form>
|
|
@@ -229,11 +207,10 @@ $inputGross.on("input", function (event) {
|
|
|
229
207
|
</p>
|
|
230
208
|
<script type="module">
|
|
231
209
|
import {InputSpinner} from "./src/InputSpinner.js"
|
|
232
|
-
|
|
233
|
-
new InputSpinner(element,
|
|
210
|
+
new InputSpinner(document.getElementById("buttons-only"),
|
|
234
211
|
{buttonsOnly: true, autoInterval: undefined})
|
|
235
212
|
</script>
|
|
236
|
-
<pre><code class="language-js"
|
|
213
|
+
<pre><code class="language-js">new InputSpinner(element, {buttonsOnly: true, autoInterval: undefined})</code></pre>
|
|
237
214
|
|
|
238
215
|
<h3>Dynamically handling of the <code>class</code> attribute</h3>
|
|
239
216
|
<p>
|
|
@@ -245,20 +222,16 @@ $inputGross.on("input", function (event) {
|
|
|
245
222
|
Try to change the class to "is-invalid" or "text-info".
|
|
246
223
|
</p>
|
|
247
224
|
<script>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
$classInput.on("input", function () {
|
|
251
|
-
$inputChangeClass.prop("class", this.value)
|
|
225
|
+
document.getElementById("classInput").addEventListener("input", function (event) {
|
|
226
|
+
document.getElementById("inputChangeClass").className = event.target.value
|
|
252
227
|
})
|
|
253
228
|
</script>
|
|
254
229
|
<pre><code class="language-html"><input id="inputChangeClass" class="is-valid" type="number" value="50"/>
|
|
255
230
|
<label for="classInput">CSS Class</label>
|
|
256
231
|
<input id="classInput" type="text" class="form-control" value="is-valid"/>
|
|
257
232
|
<script>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
$classInput.on("input", function() {
|
|
261
|
-
$inputChangeClass.prop("class", this.value);
|
|
233
|
+
document.getElementById("classInput").addEventListener("input", function (event) {
|
|
234
|
+
document.getElementById("inputChangeClass").className = event.target.value
|
|
262
235
|
})
|
|
263
236
|
</script></code></pre>
|
|
264
237
|
|
|
@@ -313,45 +286,24 @@ $inputGross.on("input", function (event) {
|
|
|
313
286
|
<input id="minMaxTester" type="number" value="50" min="0" max="100" step="0.05" data-decimals="2"/>
|
|
314
287
|
</p>
|
|
315
288
|
<script>
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
})
|
|
329
|
-
$stepInput.on("change", function (event) {
|
|
330
|
-
console.log("on change", event)
|
|
331
|
-
$minMaxTester.attr("step", $stepInput.val())
|
|
332
|
-
})
|
|
333
|
-
$dataDecimalsInput.on("change", function (event) {
|
|
334
|
-
console.log("on change", event)
|
|
335
|
-
$minMaxTester.attr("data-decimals", $dataDecimalsInput.val())
|
|
336
|
-
})
|
|
289
|
+
(function () {
|
|
290
|
+
const tester = document.getElementById("minMaxTester")
|
|
291
|
+
const bindAttr = function (inputId, attrName) {
|
|
292
|
+
document.getElementById(inputId).addEventListener("change", function (event) {
|
|
293
|
+
tester.setAttribute(attrName, event.target.value)
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
bindAttr("minInput", "min")
|
|
297
|
+
bindAttr("maxInput", "max")
|
|
298
|
+
bindAttr("stepInput", "step")
|
|
299
|
+
bindAttr("dataDecimalsInput", "data-decimals")
|
|
300
|
+
})()
|
|
337
301
|
</script>
|
|
338
|
-
<pre><code class="language-javascript">
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
var $dataDecimalsInput = $("#dataDecimalsInput")
|
|
342
|
-
var $minMaxTester = $("#minMaxTester")
|
|
343
|
-
$minInput.on("change", function (event) {
|
|
344
|
-
$minMaxTester.attr("min", $minInput.val())
|
|
345
|
-
})
|
|
346
|
-
$maxInput.on("change", function (event) {
|
|
347
|
-
$minMaxTester.attr("max", $maxInput.val())
|
|
348
|
-
})
|
|
349
|
-
$stepInput.on("change", function (event) {
|
|
350
|
-
$minMaxTester.attr("step", $stepInput.val())
|
|
351
|
-
})
|
|
352
|
-
$dataDecimalsInput.on("change", function (event) {
|
|
353
|
-
$minMaxTester.attr("data-decimals", $dataDecimalsInput.val())
|
|
302
|
+
<pre><code class="language-javascript">const tester = document.getElementById("minMaxTester")
|
|
303
|
+
document.getElementById("minInput").addEventListener("change", function (event) {
|
|
304
|
+
tester.setAttribute("min", event.target.value)
|
|
354
305
|
})
|
|
306
|
+
// ...same for max, step, data-decimals
|
|
355
307
|
</code></pre>
|
|
356
308
|
|
|
357
309
|
<h3>Prefix and Suffix</h3>
|
|
@@ -375,30 +327,32 @@ $dataDecimalsInput.on("change", function (event) {
|
|
|
375
327
|
<input type="number" id="inputLoop" value="0" data-decimals="0" min="-10" max="360" step="10"/>
|
|
376
328
|
</p>
|
|
377
329
|
<script>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
330
|
+
window.addEventListener("load", function () {
|
|
331
|
+
const inputLoop = document.getElementById("inputLoop")
|
|
332
|
+
inputLoop.addEventListener("input", function () {
|
|
333
|
+
let value = parseInt(inputLoop.value, 10)
|
|
334
|
+
value = (value < 0) ? 360 + value : value % 360
|
|
335
|
+
inputLoop.setValue(value)
|
|
336
|
+
})
|
|
383
337
|
})
|
|
384
338
|
</script>
|
|
385
339
|
<pre><code class="language-html"><input step="10" type="number" id="inputLoop" value="0" data-decimals="0" min="-10" max="360"/></code></pre>
|
|
386
|
-
<p>"Loop" the value between 0 and 360 with the <code>
|
|
387
|
-
<pre><code class="language-javascript">
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
value = (value < 0) ? 360 +
|
|
391
|
-
|
|
340
|
+
<p>"Loop" the value between 0 and 360 with the <code>input</code> event in JavaScript.</p>
|
|
341
|
+
<pre><code class="language-javascript">const inputLoop = document.getElementById("inputLoop")
|
|
342
|
+
inputLoop.addEventListener("input", function () {
|
|
343
|
+
let value = parseInt(inputLoop.value, 10)
|
|
344
|
+
value = (value < 0) ? 360 + value : value % 360
|
|
345
|
+
inputLoop.setValue(value)
|
|
392
346
|
})</code></pre>
|
|
393
347
|
|
|
394
348
|
<h3>Custom Editors</h3>
|
|
395
349
|
|
|
396
|
-
<p>An Editor defines
|
|
397
|
-
<code>/src/custom-editors.js</code
|
|
350
|
+
<p>An Editor defines how the input is parsed and rendered. The inputSpinner ships some custom Editors in
|
|
351
|
+
<code>/src/custom-editors.js</code>, available as named ES exports.</p>
|
|
398
352
|
|
|
399
353
|
<h4>RawEditor</h4>
|
|
400
354
|
|
|
401
|
-
<p>The simplest custom Editor is the <code>RawEditor</code>, it renders just the value
|
|
355
|
+
<p>The simplest custom Editor is the <code>RawEditor</code>, it renders just the value and parses just the value,
|
|
402
356
|
without any
|
|
403
357
|
changes, like a native number input. No internationalization, no digit grouping.</p>
|
|
404
358
|
<p>
|
|
@@ -406,12 +360,12 @@ $inputLoop.on("input", function(event) {
|
|
|
406
360
|
</p>
|
|
407
361
|
<script type="module">
|
|
408
362
|
import {InputSpinner} from "./src/InputSpinner.js"
|
|
409
|
-
|
|
410
|
-
new InputSpinner(document.getElementById("rawEditor"),
|
|
411
|
-
{editor: customEditors.RawEditor})
|
|
363
|
+
import {RawEditor} from "./src/custom-editors.js"
|
|
364
|
+
new InputSpinner(document.getElementById("rawEditor"), {editor: RawEditor})
|
|
412
365
|
</script>
|
|
413
|
-
<pre><code
|
|
414
|
-
|
|
366
|
+
<pre><code class="language-js">import {InputSpinner} from "./src/InputSpinner.js"
|
|
367
|
+
import {RawEditor} from "./src/custom-editors.js"
|
|
368
|
+
new InputSpinner(document.getElementById("rawEditor"), {editor: RawEditor})</code></pre>
|
|
415
369
|
|
|
416
370
|
<h4>TimeEditor</h4>
|
|
417
371
|
|
|
@@ -420,19 +374,21 @@ $inputLoop.on("input", function(event) {
|
|
|
420
374
|
<div class="mt-1">value: <span id="timeValue"></span></div>
|
|
421
375
|
<script type="module">
|
|
422
376
|
import {InputSpinner} from "./src/InputSpinner.js"
|
|
377
|
+
import {TimeEditor} from "./src/custom-editors.js"
|
|
423
378
|
|
|
424
379
|
const element = document.getElementById("timeEditor")
|
|
425
|
-
new InputSpinner(element, {editor:
|
|
380
|
+
new InputSpinner(element, {editor: TimeEditor})
|
|
426
381
|
element.addEventListener("input", () => {
|
|
427
382
|
document.getElementById("timeValue").textContent = element.value
|
|
428
383
|
})
|
|
429
384
|
document.getElementById("timeValue").textContent = element.value
|
|
430
385
|
</script>
|
|
431
|
-
<pre><code
|
|
432
|
-
|
|
386
|
+
<pre><code class="language-js">import {InputSpinner} from "./src/InputSpinner.js"
|
|
387
|
+
import {TimeEditor} from "./src/custom-editors.js"
|
|
388
|
+
new InputSpinner(document.getElementById("timeEditor"), {editor: TimeEditor})</code></pre>
|
|
433
389
|
|
|
434
|
-
<h3>Styling with templates
|
|
435
|
-
<p>With the
|
|
390
|
+
<h3>Styling with templates</h3>
|
|
391
|
+
<p>With the templating feature, you can almost do <b>anything, when it comes to layout</b>.</p>
|
|
436
392
|
<h5>How about... buttons right</h5>
|
|
437
393
|
<p>
|
|
438
394
|
<input data-prefix="¥" id="templateButtonsRight" value="1000"/>
|
|
@@ -468,11 +424,11 @@ $inputLoop.on("input", function(event) {
|
|
|
468
424
|
<li>${incrementButton}</li>
|
|
469
425
|
</ul>
|
|
470
426
|
<p>Provide the template as configuration parameter:</p>
|
|
471
|
-
<pre><code class="language-js"
|
|
427
|
+
<pre><code class="language-js">new InputSpinner(element, {template: '<div class...'})</code></pre>
|
|
472
428
|
|
|
473
429
|
<h3>Destroying the spinner</h3>
|
|
474
|
-
<p>To
|
|
475
|
-
<pre><code class="language-javascript"
|
|
430
|
+
<p>To remove the InputSpinner and show the original input element, use</p>
|
|
431
|
+
<pre><code class="language-javascript">element.destroyInputSpinner()</code></pre>
|
|
476
432
|
<div class="mb-3">
|
|
477
433
|
<label for="inputDestroyCreate">Label `for` switches dynamically:</label>
|
|
478
434
|
<input type="number" id="inputDestroyCreate" value="50"/>
|
|
@@ -481,18 +437,18 @@ $inputLoop.on("input", function(event) {
|
|
|
481
437
|
<button id="buttonCreate" disabled="disabled" class="btn btn-primary">re-create</button>
|
|
482
438
|
<script type="module">
|
|
483
439
|
import {InputSpinner} from "./src/InputSpinner.js"
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
440
|
+
const buttonDestroy = document.getElementById("buttonDestroy")
|
|
441
|
+
const buttonCreate = document.getElementById("buttonCreate")
|
|
442
|
+
const inputDestroyCreate = document.getElementById("inputDestroyCreate")
|
|
443
|
+
buttonDestroy.addEventListener("click", function () {
|
|
444
|
+
inputDestroyCreate.destroyInputSpinner()
|
|
445
|
+
buttonDestroy.disabled = true
|
|
446
|
+
buttonCreate.disabled = false
|
|
491
447
|
})
|
|
492
|
-
|
|
493
|
-
new InputSpinner(
|
|
494
|
-
|
|
495
|
-
|
|
448
|
+
buttonCreate.addEventListener("click", function () {
|
|
449
|
+
new InputSpinner(inputDestroyCreate)
|
|
450
|
+
buttonDestroy.disabled = false
|
|
451
|
+
buttonCreate.disabled = true
|
|
496
452
|
})
|
|
497
453
|
</script>
|
|
498
454
|
|
|
@@ -510,13 +466,11 @@ $inputLoop.on("input", function(event) {
|
|
|
510
466
|
<a href="https://github.com/shaack/bootstrap-input-spinner/issues">issue</a>.</p>
|
|
511
467
|
<br/><br/>
|
|
512
468
|
</section>
|
|
513
|
-
<!-- bootstrap needs jQuery -->
|
|
514
469
|
<script type="module">
|
|
515
470
|
import {InputSpinner} from "./src/InputSpinner.js"
|
|
516
471
|
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
new InputSpinner(inputSpinnerElement)
|
|
472
|
+
for (const el of document.querySelectorAll("input[type='number']")) {
|
|
473
|
+
new InputSpinner(el)
|
|
520
474
|
}
|
|
521
475
|
</script>
|
|
522
476
|
</body>
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bootstrap-input-spinner",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A Bootstrap 5
|
|
5
|
-
"browser": "./src/
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "A Bootstrap 5 plugin to create input spinner elements for number input.",
|
|
5
|
+
"browser": "./src/InputSpinner.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/InputSpinner.js",
|
|
9
|
+
"./custom-editors": "./src/custom-editors.js"
|
|
10
|
+
},
|
|
6
11
|
"scripts": {
|
|
7
12
|
"test": "tput setaf 4;echo open test/index.html in your browser for testing."
|
|
8
13
|
},
|
package/src/InputSpinner.js
CHANGED
|
@@ -6,45 +6,45 @@
|
|
|
6
6
|
|
|
7
7
|
// the default editor for parsing and rendering
|
|
8
8
|
const I18nEditor = function (props, element) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
const locale = props.locale || "en-US"
|
|
10
|
+
|
|
11
|
+
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, '')
|
|
15
|
+
return parseFloat(customFormat
|
|
16
|
+
.replace(new RegExp(' ', 'g'), '')
|
|
17
|
+
.replace(new RegExp('\\' + thousandSeparator, 'g'), '')
|
|
18
|
+
.replace(new RegExp('\\' + decimalSeparator), '.')
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
22
|
+
this.render = function (number) {
|
|
23
|
+
const decimals = parseInt(element.getAttribute("data-decimals")) || 0
|
|
24
|
+
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)
|
|
32
31
|
}
|
|
32
|
+
}
|
|
33
33
|
|
|
34
34
|
let triggerKeyPressed = false
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
|
|
36
|
+
function parseTemplate(html) {
|
|
37
|
+
const tpl = document.createElement("template")
|
|
38
|
+
tpl.innerHTML = html.trim()
|
|
39
|
+
return tpl.content.firstElementChild
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseNumberAttr(el, name, fallback) {
|
|
43
|
+
const raw = el.getAttribute(name)
|
|
44
|
+
if (raw === null || raw === "" || isNaN(parseFloat(raw))) {
|
|
45
|
+
return fallback
|
|
46
46
|
}
|
|
47
|
-
return
|
|
47
|
+
return parseFloat(raw)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export class InputSpinner {
|
|
@@ -53,18 +53,6 @@ export class InputSpinner {
|
|
|
53
53
|
|
|
54
54
|
const self = this
|
|
55
55
|
this.element = element
|
|
56
|
-
/*
|
|
57
|
-
if (props === "destroy") { // todo replace with method
|
|
58
|
-
this.each(function () {
|
|
59
|
-
if (this["bootstrap-input-spinner"]) {
|
|
60
|
-
this.destroyInputSpinner()
|
|
61
|
-
} else {
|
|
62
|
-
console.warn("element", this, "is no bootstrap-input-spinner")
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
return this
|
|
66
|
-
}
|
|
67
|
-
*/
|
|
68
56
|
|
|
69
57
|
this.props = {
|
|
70
58
|
decrementButton: "<strong>−</strong>", // button text
|
|
@@ -97,168 +85,190 @@ export class InputSpinner {
|
|
|
97
85
|
.replace(/\${incrementButton}/g, this.props.incrementButton)
|
|
98
86
|
.replace(/\${textAlign}/g, this.props.textAlign)
|
|
99
87
|
|
|
100
|
-
if (
|
|
101
|
-
console.warn("element",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.$original = $(this.element)
|
|
105
|
-
this.$original[0]["bootstrap-input-spinner"] = true
|
|
106
|
-
this.$original.hide()
|
|
107
|
-
this.$original[0].inputSpinnerEditor = new this.props.editor(this.props, this.element)
|
|
108
|
-
|
|
109
|
-
this.autoDelayHandler = null
|
|
110
|
-
this.autoIntervalHandler = null
|
|
111
|
-
|
|
112
|
-
this.$inputGroup = $(html)
|
|
113
|
-
this.$buttonDecrement = this.$inputGroup.find(".btn-decrement")
|
|
114
|
-
this.$buttonIncrement = this.$inputGroup.find(".btn-increment")
|
|
115
|
-
this.$input = this.$inputGroup.find("input")
|
|
116
|
-
this.$label = $("label[for='" + this.$original.attr("id") + "']")
|
|
117
|
-
if (!this.$label[0]) {
|
|
118
|
-
this.$label = this.$original.closest("label")
|
|
119
|
-
}
|
|
88
|
+
if (element["bootstrap-input-spinner"]) {
|
|
89
|
+
console.warn("element", element, "is already a bootstrap-input-spinner")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
120
92
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
93
|
+
this.original = element
|
|
94
|
+
this.original["bootstrap-input-spinner"] = true
|
|
95
|
+
this.original.style.display = "none"
|
|
96
|
+
this.original.inputSpinnerEditor = new this.props.editor(this.props, element)
|
|
124
97
|
|
|
125
|
-
|
|
98
|
+
this.autoDelayHandler = null
|
|
99
|
+
this.autoIntervalHandler = null
|
|
126
100
|
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
this.inputGroup = parseTemplate(html)
|
|
102
|
+
this.buttonDecrement = this.inputGroup.querySelector(".btn-decrement")
|
|
103
|
+
this.buttonIncrement = this.inputGroup.querySelector(".btn-increment")
|
|
104
|
+
this.input = this.inputGroup.querySelector("input")
|
|
129
105
|
|
|
130
|
-
|
|
131
|
-
|
|
106
|
+
this.label = null
|
|
107
|
+
if (this.original.id) {
|
|
108
|
+
this.label = document.querySelector("label[for='" + this.original.id + "']")
|
|
109
|
+
}
|
|
110
|
+
if (!this.label) {
|
|
111
|
+
this.label = this.original.closest("label")
|
|
112
|
+
}
|
|
132
113
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
if (suffix) {
|
|
138
|
-
const suffixElement = $('<span class="input-group-text">' + suffix + '</span>')
|
|
139
|
-
this.$inputGroup.find("input").after(suffixElement)
|
|
140
|
-
}
|
|
114
|
+
this.min = null
|
|
115
|
+
this.max = null
|
|
116
|
+
this.step = null
|
|
141
117
|
|
|
142
|
-
|
|
143
|
-
setValue(newValue)
|
|
144
|
-
}
|
|
145
|
-
this.$original[0].destroyInputSpinner = function () {
|
|
146
|
-
destroy()
|
|
147
|
-
}
|
|
118
|
+
updateAttributes()
|
|
148
119
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
setValue(self.value, true)
|
|
152
|
-
})
|
|
153
|
-
this.observer.observe(this.$original[0], {attributes: true})
|
|
120
|
+
this.value = parseFloat(this.original.value)
|
|
121
|
+
let pointerState = false
|
|
154
122
|
|
|
155
|
-
|
|
123
|
+
const prefix = this.original.getAttribute("data-prefix") || ""
|
|
124
|
+
const suffix = this.original.getAttribute("data-suffix") || ""
|
|
156
125
|
|
|
157
|
-
|
|
126
|
+
if (prefix) {
|
|
127
|
+
const prefixElement = document.createElement("span")
|
|
128
|
+
prefixElement.className = "input-group-text"
|
|
129
|
+
prefixElement.textContent = prefix
|
|
130
|
+
this.input.parentNode.insertBefore(prefixElement, this.input)
|
|
131
|
+
}
|
|
132
|
+
if (suffix) {
|
|
133
|
+
const suffixElement = document.createElement("span")
|
|
134
|
+
suffixElement.className = "input-group-text"
|
|
135
|
+
suffixElement.textContent = suffix
|
|
136
|
+
this.input.parentNode.insertBefore(suffixElement, this.input.nextSibling)
|
|
137
|
+
}
|
|
158
138
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
dispatchEvent(self.$original, event.type)
|
|
166
|
-
}
|
|
167
|
-
if (self.props.keyboardStepping && focusOut) { // stop stepping
|
|
168
|
-
resetTimer()
|
|
169
|
-
}
|
|
170
|
-
}).on("keydown", function (event) {
|
|
171
|
-
if (self.props.keyboardStepping) {
|
|
172
|
-
if (event.which === 38) { // up arrow pressed
|
|
173
|
-
event.preventDefault()
|
|
174
|
-
if (!self.$buttonDecrement.prop("disabled")) {
|
|
175
|
-
stepHandling(self.step)
|
|
176
|
-
}
|
|
177
|
-
} else if (event.which === 40) { // down arrow pressed
|
|
178
|
-
event.preventDefault()
|
|
179
|
-
if (!self.$buttonIncrement.prop("disabled")) {
|
|
180
|
-
stepHandling(-self.step)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}).on("keyup", function (event) {
|
|
185
|
-
// up/down arrow released
|
|
186
|
-
if (self.props.keyboardStepping && (event.which === 38 || event.which === 40)) {
|
|
187
|
-
event.preventDefault()
|
|
188
|
-
resetTimer()
|
|
189
|
-
}
|
|
190
|
-
})
|
|
139
|
+
this.original.setValue = function (newValue) {
|
|
140
|
+
setValue(newValue)
|
|
141
|
+
}
|
|
142
|
+
this.original.destroyInputSpinner = function () {
|
|
143
|
+
destroy()
|
|
144
|
+
}
|
|
191
145
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
146
|
+
this.observer = new MutationObserver(function () {
|
|
147
|
+
updateAttributes()
|
|
148
|
+
setValue(self.value, true)
|
|
149
|
+
})
|
|
150
|
+
this.observer.observe(this.original, {attributes: true})
|
|
151
|
+
|
|
152
|
+
this.original.parentNode.insertBefore(this.inputGroup, this.original.nextSibling)
|
|
153
|
+
|
|
154
|
+
setValue(this.value)
|
|
155
|
+
|
|
156
|
+
// Track listeners so destroy() can detach them cleanly.
|
|
157
|
+
this._teardown = []
|
|
158
|
+
const bind = function (target, type, handler, options) {
|
|
159
|
+
target.addEventListener(type, handler, options)
|
|
160
|
+
self._teardown.push(function () {
|
|
161
|
+
target.removeEventListener(type, handler, options)
|
|
198
162
|
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const onInputEvent = function (event) {
|
|
166
|
+
let newValue = self.input.value
|
|
167
|
+
const focusOut = event.type === "focusout"
|
|
168
|
+
if (!self.props.buttonsOnly) {
|
|
169
|
+
newValue = self.original.inputSpinnerEditor.parse(newValue)
|
|
170
|
+
setValue(newValue, focusOut)
|
|
171
|
+
dispatchEvent(self.original, event.type)
|
|
172
|
+
}
|
|
173
|
+
if (self.props.keyboardStepping && focusOut) { // stop stepping
|
|
174
|
+
resetTimer()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
bind(this.input, "paste", onInputEvent)
|
|
178
|
+
bind(this.input, "input", onInputEvent)
|
|
179
|
+
bind(this.input, "change", onInputEvent)
|
|
180
|
+
bind(this.input, "focusout", onInputEvent)
|
|
181
|
+
|
|
182
|
+
bind(this.input, "keydown", function (event) {
|
|
183
|
+
if (!self.props.keyboardStepping) return
|
|
184
|
+
if (event.key === "ArrowUp" || event.keyCode === 38) {
|
|
185
|
+
event.preventDefault()
|
|
186
|
+
if (!self.buttonIncrement.disabled) {
|
|
203
187
|
stepHandling(self.step)
|
|
204
188
|
}
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
dispatchEvent(self.$original, "change")
|
|
210
|
-
pointerState = false
|
|
189
|
+
} else if (event.key === "ArrowDown" || event.keyCode === 40) {
|
|
190
|
+
event.preventDefault()
|
|
191
|
+
if (!self.buttonDecrement.disabled) {
|
|
192
|
+
stepHandling(-self.step)
|
|
211
193
|
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
bind(this.input, "keyup", function (event) {
|
|
197
|
+
if (self.props.keyboardStepping &&
|
|
198
|
+
(event.key === "ArrowUp" || event.key === "ArrowDown" ||
|
|
199
|
+
event.keyCode === 38 || event.keyCode === 40)) {
|
|
200
|
+
event.preventDefault()
|
|
201
|
+
resetTimer()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// decrement button
|
|
206
|
+
onPointerDown(self.buttonDecrement, function () {
|
|
207
|
+
if (!self.buttonDecrement.disabled) {
|
|
208
|
+
pointerState = true
|
|
209
|
+
stepHandling(-self.step)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
// increment button
|
|
213
|
+
onPointerDown(self.buttonIncrement, function () {
|
|
214
|
+
if (!self.buttonIncrement.disabled) {
|
|
215
|
+
pointerState = true
|
|
216
|
+
stepHandling(self.step)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
onPointerUp(document.body, function () {
|
|
220
|
+
if (pointerState === true) {
|
|
221
|
+
resetTimer()
|
|
222
|
+
dispatchEvent(self.original, "change")
|
|
223
|
+
pointerState = false
|
|
224
|
+
}
|
|
225
|
+
})
|
|
214
226
|
|
|
215
227
|
function setValue(newValue, updateInput) {
|
|
216
228
|
if (updateInput === undefined) {
|
|
217
229
|
updateInput = true
|
|
218
230
|
}
|
|
219
231
|
if (isNaN(newValue) || newValue === "") {
|
|
220
|
-
self
|
|
232
|
+
self.original.value = ""
|
|
221
233
|
if (updateInput) {
|
|
222
|
-
self
|
|
234
|
+
self.input.value = ""
|
|
223
235
|
}
|
|
224
236
|
self.value = NaN
|
|
225
237
|
} else {
|
|
226
238
|
newValue = parseFloat(newValue)
|
|
227
239
|
newValue = Math.min(Math.max(newValue, self.min), self.max)
|
|
228
|
-
self
|
|
240
|
+
self.original.value = newValue
|
|
229
241
|
if (updateInput) {
|
|
230
|
-
self
|
|
242
|
+
self.input.value = self.original.inputSpinnerEditor.render(newValue)
|
|
231
243
|
}
|
|
232
244
|
self.value = newValue
|
|
233
245
|
}
|
|
234
246
|
}
|
|
235
247
|
|
|
236
248
|
function destroy() {
|
|
237
|
-
self
|
|
249
|
+
if (self.input.required) {
|
|
250
|
+
self.original.required = true
|
|
251
|
+
}
|
|
238
252
|
self.observer.disconnect()
|
|
239
253
|
resetTimer()
|
|
240
|
-
|
|
241
|
-
self
|
|
242
|
-
self
|
|
243
|
-
self
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
for (const off of self._teardown) off()
|
|
255
|
+
self._teardown = []
|
|
256
|
+
self.inputGroup.remove()
|
|
257
|
+
self.original.style.display = ""
|
|
258
|
+
self.original["bootstrap-input-spinner"] = undefined
|
|
259
|
+
delete self.original.setValue
|
|
260
|
+
delete self.original.destroyInputSpinner
|
|
261
|
+
delete self.original.inputSpinnerEditor
|
|
262
|
+
if (self.label) {
|
|
263
|
+
self.label.setAttribute("for", self.original.id || "")
|
|
246
264
|
}
|
|
247
265
|
}
|
|
248
266
|
|
|
249
|
-
function dispatchEvent(
|
|
250
|
-
if (type)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
event = new Event(type, {bubbles: true})
|
|
255
|
-
} else { // IE todo remove
|
|
256
|
-
event = document.createEvent('Event')
|
|
257
|
-
event.initEvent(type, true, true)
|
|
258
|
-
}
|
|
259
|
-
$element[0].dispatchEvent(event)
|
|
260
|
-
})
|
|
261
|
-
}
|
|
267
|
+
function dispatchEvent(target, type) {
|
|
268
|
+
if (!type) return
|
|
269
|
+
setTimeout(function () {
|
|
270
|
+
target.dispatchEvent(new Event(type, {bubbles: true}))
|
|
271
|
+
})
|
|
262
272
|
}
|
|
263
273
|
|
|
264
274
|
function stepHandling(step) {
|
|
@@ -278,68 +288,67 @@ export class InputSpinner {
|
|
|
278
288
|
self.value = 0
|
|
279
289
|
}
|
|
280
290
|
setValue(Math.round(self.value / step) * step + step)
|
|
281
|
-
dispatchEvent(self
|
|
291
|
+
dispatchEvent(self.original, "input")
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
function resetTimer() {
|
|
285
295
|
clearTimeout(self.autoDelayHandler)
|
|
286
|
-
|
|
296
|
+
clearInterval(self.autoIntervalHandler)
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
function updateAttributes() {
|
|
290
300
|
// copy properties from original to the new input
|
|
291
|
-
if (self
|
|
292
|
-
self
|
|
293
|
-
self
|
|
301
|
+
if (self.original.required) {
|
|
302
|
+
self.input.required = true
|
|
303
|
+
self.original.removeAttribute("required")
|
|
294
304
|
}
|
|
295
|
-
self
|
|
296
|
-
self
|
|
297
|
-
const disabled = self
|
|
298
|
-
const readonly = self
|
|
299
|
-
self
|
|
300
|
-
self
|
|
301
|
-
self
|
|
302
|
-
self
|
|
305
|
+
self.input.placeholder = self.original.placeholder
|
|
306
|
+
self.input.setAttribute("inputmode", self.original.getAttribute("inputmode") || "decimal")
|
|
307
|
+
const disabled = self.original.disabled
|
|
308
|
+
const readonly = self.original.readOnly
|
|
309
|
+
self.input.disabled = disabled
|
|
310
|
+
self.input.readOnly = readonly || self.props.buttonsOnly
|
|
311
|
+
self.buttonIncrement.disabled = disabled || readonly
|
|
312
|
+
self.buttonDecrement.disabled = disabled || readonly
|
|
303
313
|
if (disabled || readonly) {
|
|
304
314
|
resetTimer()
|
|
305
315
|
}
|
|
306
|
-
const originalClass = self
|
|
316
|
+
const originalClass = self.original.className || ""
|
|
307
317
|
let groupClass = ""
|
|
308
|
-
// sizing
|
|
309
318
|
if (/form-control-sm/g.test(originalClass)) {
|
|
310
319
|
groupClass = "input-group-sm"
|
|
311
320
|
} else if (/form-control-lg/g.test(originalClass)) {
|
|
312
321
|
groupClass = "input-group-lg"
|
|
313
322
|
}
|
|
314
323
|
const inputClass = originalClass.replace(/form-control(-(sm|lg))?/g, "")
|
|
315
|
-
self
|
|
316
|
-
self
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
self.
|
|
320
|
-
self.
|
|
321
|
-
|
|
322
|
-
if (self
|
|
323
|
-
self
|
|
324
|
+
self.inputGroup.className = "input-group " + groupClass + " " + self.props.groupClass
|
|
325
|
+
self.input.className = "form-control " + inputClass
|
|
326
|
+
|
|
327
|
+
self.min = parseNumberAttr(self.original, "min", -Infinity)
|
|
328
|
+
self.max = parseNumberAttr(self.original, "max", Infinity)
|
|
329
|
+
self.step = parseNumberAttr(self.original, "step", 1) || 1
|
|
330
|
+
|
|
331
|
+
if (self.original.hasAttribute("hidden")) {
|
|
332
|
+
self.inputGroup.setAttribute("hidden", self.original.getAttribute("hidden") || "")
|
|
324
333
|
} else {
|
|
325
|
-
self
|
|
334
|
+
self.inputGroup.removeAttribute("hidden")
|
|
326
335
|
}
|
|
327
|
-
if (self
|
|
328
|
-
self
|
|
329
|
-
if (self
|
|
330
|
-
self
|
|
336
|
+
if (self.original.id) {
|
|
337
|
+
self.input.id = self.original.id + ":input_spinner" // give the spinner a unique id...
|
|
338
|
+
if (self.label) {
|
|
339
|
+
self.label.setAttribute("for", self.input.id) // ...to rewire the label
|
|
331
340
|
}
|
|
332
341
|
}
|
|
333
342
|
}
|
|
334
343
|
|
|
335
|
-
function onPointerUp(
|
|
336
|
-
|
|
344
|
+
function onPointerUp(el, callback) {
|
|
345
|
+
bind(el, "mouseup", function (e) {
|
|
337
346
|
callback(e)
|
|
338
347
|
})
|
|
339
|
-
|
|
348
|
+
bind(el, "touchend", function (e) {
|
|
340
349
|
callback(e)
|
|
341
350
|
})
|
|
342
|
-
|
|
351
|
+
bind(el, "keyup", function (e) {
|
|
343
352
|
if ((e.keyCode === 32 || e.keyCode === 13)) {
|
|
344
353
|
triggerKeyPressed = false
|
|
345
354
|
callback(e)
|
|
@@ -347,20 +356,20 @@ export class InputSpinner {
|
|
|
347
356
|
})
|
|
348
357
|
}
|
|
349
358
|
|
|
350
|
-
function onPointerDown(
|
|
351
|
-
|
|
359
|
+
function onPointerDown(el, callback) {
|
|
360
|
+
bind(el, "mousedown", function (e) {
|
|
352
361
|
if (e.button === 0) {
|
|
353
362
|
e.preventDefault()
|
|
354
363
|
callback(e)
|
|
355
364
|
}
|
|
356
365
|
})
|
|
357
|
-
|
|
366
|
+
bind(el, "touchstart", function (e) {
|
|
358
367
|
if (e.cancelable) {
|
|
359
368
|
e.preventDefault()
|
|
360
369
|
}
|
|
361
370
|
callback(e)
|
|
362
371
|
}, {passive: false})
|
|
363
|
-
|
|
372
|
+
bind(el, "keydown", function (e) {
|
|
364
373
|
if ((e.keyCode === 32 || e.keyCode === 13) && !triggerKeyPressed) {
|
|
365
374
|
triggerKeyPressed = true
|
|
366
375
|
callback(e)
|
|
@@ -368,6 +377,4 @@ export class InputSpinner {
|
|
|
368
377
|
})
|
|
369
378
|
}
|
|
370
379
|
}
|
|
371
|
-
|
|
372
380
|
}
|
|
373
|
-
|
package/test/TestInputSpinner.js
CHANGED
|
@@ -125,10 +125,9 @@ describe("InputSpinner setValue", () => {
|
|
|
125
125
|
assert.equal(group.querySelector("input").value, "")
|
|
126
126
|
clear()
|
|
127
127
|
})
|
|
128
|
-
it("
|
|
128
|
+
it("element.setValue updates both the original and the visible input", () => {
|
|
129
129
|
const {el, group} = spin({value: "1", min: "0", max: "100"})
|
|
130
|
-
|
|
131
|
-
await wait()
|
|
130
|
+
el.setValue(42)
|
|
132
131
|
assert.equal(el.value, "42")
|
|
133
132
|
assert.equal(group.querySelector("input").value, "42")
|
|
134
133
|
clear()
|
|
@@ -170,15 +169,19 @@ describe("InputSpinner stepping", () => {
|
|
|
170
169
|
assert.equal(el.value, "0")
|
|
171
170
|
clear()
|
|
172
171
|
})
|
|
173
|
-
it("arrow-up key steps the value",
|
|
172
|
+
it("arrow-up key steps the value", () => {
|
|
174
173
|
const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"})
|
|
175
|
-
|
|
174
|
+
group.querySelector("input").dispatchEvent(
|
|
175
|
+
new KeyboardEvent("keydown", {key: "ArrowUp", bubbles: true, cancelable: true})
|
|
176
|
+
)
|
|
176
177
|
assert.equal(el.value, "6")
|
|
177
178
|
clear()
|
|
178
179
|
})
|
|
179
|
-
it("arrow-down key steps the value",
|
|
180
|
+
it("arrow-down key steps the value", () => {
|
|
180
181
|
const {el, group} = spin({value: "5", min: "0", max: "10", step: "1"})
|
|
181
|
-
|
|
182
|
+
group.querySelector("input").dispatchEvent(
|
|
183
|
+
new KeyboardEvent("keydown", {key: "ArrowDown", bubbles: true, cancelable: true})
|
|
184
|
+
)
|
|
182
185
|
assert.equal(el.value, "4")
|
|
183
186
|
clear()
|
|
184
187
|
})
|
package/test/index.html
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<link rel="stylesheet"
|
|
7
7
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
|
8
8
|
crossorigin="anonymous">
|
|
9
|
-
<script src="https://code.jquery.com/jquery-3.6.0.min.js" crossorigin="anonymous"></script>
|
|
10
9
|
<style>
|
|
11
10
|
body { font-family: sans-serif; padding: 1rem; }
|
|
12
11
|
#fixture { position: absolute; left: -10000px; top: -10000px; }
|
|
12
|
+
h2 { margin-top: 1rem; }
|
|
13
13
|
</style>
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|