PageSourceSearch

https://sgdml.org/js/bootstrap-input-spinner/bootstrap-input-spinner.js

js sgdml.org collected 2026-10-03 21:31:34 UTC 15,047 bytes, 361 lines download raw bytes

1/**
2 * Author and copyright: Stefan Haack (https://shaack.com)
3 * Repository: https://github.com/shaack/bootstrap-input-spinner
4 * License: MIT, see file 'LICENSE'
5 */
6
7;(function ($) {
8    "use strict"
9
10    // the default editor for parsing and rendering
11    var I18nEditor = function (props, element) {
12        var locale = props.locale || "en-US"
13
14        this.parse = function (customFormat) {
15            var numberFormat = new Intl.NumberFormat(locale)
16            var thousandSeparator = numberFormat.format(11111).replace(/1/g, '') || '.'
17            var decimalSeparator = numberFormat.format(1.1).replace(/1/g, '')
18            return parseFloat(customFormat
19                .replace(new RegExp(' ', 'g'), '')
20                .replace(new RegExp('\\' + thousandSeparator, 'g'), '')
21                .replace(new RegExp('\\' + decimalSeparator), '.')
22            )
23        }
24
25        this.render = function (number) {
26            var decimals = parseInt(element.getAttribute("data-decimals")) || 0
27            var digitGrouping = !(element.getAttribute("data-digit-grouping") === "false")
28            var numberFormat = new Intl.NumberFormat(locale, {
29                minimumFractionDigits: decimals,
30                maximumFractionDigits: decimals,
31                useGrouping: digitGrouping
32            })
33            return numberFormat.format(number)
34        }
35    }
36
37    var triggerKeyPressed = false
38    var originalVal = $.fn.val
39    $.fn.val = function (value) {
40        if (arguments.length >= 1) {
41            for (var i = 0; i < this.length; i++) {
42                if (this[i]["bootstrap-input-spinner"] && this[i].setValue) {
43                    this[i].setValue(value)
44                }
45            }
46        }
47        return originalVal.apply(this, arguments)
48    }
49
50    $.fn.inputSpinner = function (methodOrProps) {
51
52        if (methodOrProps === "destroy") {
53            this.each(function () {
54                if (this["bootstrap-input-spinner"]) {
55                    this.destroyInputSpinner()
56                } else {
57                    console.warn("element", this, "is no bootstrap-input-spinner")
58                }
59            })
60            return this
61        }
62
63        var props = {
64            decrementButton: "<strong>&minus;</strong>", // button text
65            incrementButton: "<strong>&plus;</strong>", // ..
66            groupClass: "", // css class of the resulting input-group
67            buttonsClass: "btn-outline-secondary",
68            buttonsWidth: "2.5rem",
69            textAlign: "center", // alignment of the entered number
70            autoDelay: 500, // ms threshold before auto value change
71            autoInterval: 50, // speed of auto value change
72            buttonsOnly: false, // set this `true` to disable the possibility to enter or paste the number via keyboard
73            keyboardStepping: true, // set this to `false` to disallow the use of the up and down arrow keys to step
74            locale: navigator.language, // the locale, per default detected automatically from the browser
75            editor: I18nEditor, // the editor (parsing and rendering of the input)
76            template: // the template of the input
77                '<div class="input-group ${groupClass}">' +
78                '<div class="input-group-prepend"><button style="min-width: ${buttonsWidth}" class="btn btn-decrement ${buttonsClass} btn-minus" type="button">${decrementButton}</button></div>' +
79                '<input type="text" inputmode="decimal" style="text-align: ${textAlign}" class="form-control form-control-text-input"/>' +
80                '<div class="input-group-append"><button style="min-width: ${buttonsWidth}" class="btn btn-increment ${buttonsClass} btn-plus" type="button">${incrementButton}</button></div>' +
81                '</div>'
82        }
83
84        for (var option in methodOrProps) {
85            // noinspection JSUnfilteredForInLoop
86            props[option] = methodOrProps[option]
87        }
88
89        var html = props.template
90            .replace(/\${groupClass}/g, props.groupClass)
91            .replace(/\${buttonsWidth}/g, props.buttonsWidth)
92            .replace(/\${buttonsClass}/g, props.buttonsClass)
93            .replace(/\${decrementButton}/g, props.decrementButton)
94            .replace(/\${incrementButton}/g, props.incrementButton)
95            .replace(/\${textAlign}/g, props.textAlign)
96
97        this.each(function () {
98
99            if (this["bootstrap-input-spinner"]) {
100                console.warn("element", this, "is already a bootstrap-input-spinner")
101            } else {
102
103                var $original = $(this)
104                $original[0]["bootstrap-input-spinner"] = true
105                $original.hide()
106                $original[0].inputSpinnerEditor = new props.editor(props, this)
107
108                var autoDelayHandler = null
109                var autoIntervalHandler = null
110
111                var $inputGroup = $(html)
112                var $buttonDecrement = $inputGroup.find(".btn-decrement")
113                var $buttonIncrement = $inputGroup.find(".btn-increment")
114                var $input = $inputGroup.find("input")
115                var $label = $("label[for='" + $original.attr("id") + "']")
116                if (!$label[0]) {
117                    $label = $original.closest("label")
118                }
119
120                var min = null
121                var max = null
122                var step = null
123
124                updateAttributes()
125
126                var value = parseFloat($original[0].value)
127
128                var prefix = $original.attr("data-prefix") || ""
129                var suffix = $original.attr("data-suffix") || ""
130
131                if (prefix) {
132                    var prefixElement = $('<span class="input-group-text">' + prefix + '</span>')
133                    $inputGroup.find(".input-group-prepend").append(prefixElement)
134                }
135                if (suffix) {
136                    var suffixElement = $('<span class="input-group-text">' + suffix + '</span>')
137                    $inputGroup.find(".input-group-append").prepend(suffixElement)
138                }
139
140                $original[0].setValue = function (newValue) {
141                    setValue(newValue)
142                }
143                $original[0].destroyInputSpinner = function () {
144                    destroy()
145                }
146
147                var observer = new MutationObserver(function () {
148                    updateAttributes()
149                    setValue(value, true)
150                })
151                observer.observe($original[0], {attributes: true})
152
153                $original.after($inputGroup)
154
155                setValue(value)
156
157                $input.on("paste input change focusout", function (event) {
158                    var newValue = $input[0].value
159                    var focusOut = event.type === "focusout"
160                    newValue = $original[0].inputSpinnerEditor.parse(newValue)
161                    setValue(newValue, focusOut)
162                    dispatchEvent($original, event.type)
163                }).on("keydown", function (event) {
164                    if (props.keyboardStepping) {
165                        if (event.which === 38) { // up arrow pressed
166                            event.preventDefault()
167                            if (!$buttonDecrement.prop("disabled")) {
168                                stepHandling(step)
169                            }
170                        } else if (event.which === 40) { // down arrow pressed
171                            event.preventDefault()
172                            if (!$buttonIncrement.prop("disabled")) {
173                                stepHandling(-step)
174                            }
175                        }
176                    }
177                }).on("keyup", function (event) {
178                    // up/down arrow released
179                    if (props.keyboardStepping && (event.which === 38 || event.which === 40)) {
180                        event.preventDefault()
181                        resetTimer()
182                    }
183                })
184
185                onPointerDown($buttonDecrement[0], function () {
186                    if (!$buttonDecrement.prop("disabled")) {
187                        stepHandling(-step)
188                    }
189                })
190                onPointerDown($buttonIncrement[0], function () {
191                    if (!$buttonIncrement.prop("disabled")) {
192                        stepHandling(step)
193                    }
194                })
195                onPointerUp(document.body, function () {
196                    resetTimer()
197                    dispatchEvent($original, "change")
198                })
199            }
200
201            function setValue(newValue, updateInput) {
202                if (updateInput === undefined) {
203                    updateInput = true
204                }
205                if (isNaN(newValue) || newValue === "") {
206                    $original[0].value = ""
207                    if (updateInput) {
208                        $input[0].value = ""
209                    }
210                    value = NaN
211                } else {
212                    newValue = parseFloat(newValue)
213                    newValue = Math.min(Math.max(newValue, min), max)
214                    // newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals)
215                    $original[0].value = newValue
216                    if (updateInput) {
217                        // $input[0].value = numberFormat.format(newValue)
218                        $input[0].value = $original[0].inputSpinnerEditor.render(newValue)
219                    }
220                    value = newValue
221                }
222            }
223
224            function destroy() {
225                $original.prop("required", $input.prop("required"))
226                observer.disconnect()
227                resetTimer()
228                $input.off("paste input change focusout")
229                $inputGroup.remove()
230                $original.show()
231                $original[0]["bootstrap-input-spinner"] = undefined
232                if ($label[0]) {
233                    $label.attr("for", $original.attr("id"))
234                }
235            }
236
237            function dispatchEvent($element, type) {
238                if (type) {
239                    setTimeout(function () {
240                        var event
241                        if (typeof (Event) === 'function') {
242                            event = new Event(type, {bubbles: true})
243                        } else { // IE
244                            event = document.createEvent('Event')
245                            event.initEvent(type, true, true)
246                        }
247                        $element[0].dispatchEvent(event)
248                    })
249                }
250            }
251
252            function stepHandling(step) {
253                calcStep(step)
254                resetTimer()
255                autoDelayHandler = setTimeout(function () {
256                    autoIntervalHandler = setInterval(function () {
257                        calcStep(step)
258                    }, props.autoInterval)
259                }, props.autoDelay)
260            }
261
262            function calcStep(step) {
263                if (isNaN(value)) {
264                    value = 0
265                }
266                setValue(Math.round(value / step) * step + step)
267                dispatchEvent($original, "input")
268            }
269
270            function resetTimer() {
271                clearTimeout(autoDelayHandler)
272                clearTimeout(autoIntervalHandler)
273            }
274
275            function updateAttributes() {
276                // copy properties from original to the new input
277                if ($original.prop("required")) {
278                    $input.prop("required", $original.prop("required"))
279                    $original.removeAttr('required')
280                }
281                $input.prop("placeholder", $original.prop("placeholder"))
282                $input.attr("inputmode", $original.attr("inputmode") || "decimal")
283                var disabled = $original.prop("disabled")
284                var readonly = $original.prop("readonly")
285                $input.prop("disabled", disabled)
286                $input.prop("readonly", readonly || props.buttonsOnly)
287                $buttonIncrement.prop("disabled", disabled || readonly)
288                $buttonDecrement.prop("disabled", disabled || readonly)
289                if (disabled || readonly) {
290                    resetTimer()
291                }
292                var originalClass = $original.prop("class")
293                var groupClass = ""
294                // sizing
295                if (/form-control-sm/g.test(originalClass)) {
296                    groupClass = "input-group-sm"
297                } else if (/form-control-lg/g.test(originalClass)) {
298                    groupClass = "input-group-lg"
299                }
300                var inputClass = originalClass.replace(/form-control(-(sm|lg))?/g, "")
301                $inputGroup.prop("class", "input-group " + groupClass + " " + props.groupClass)
302                $input.prop("class", "form-control " + inputClass)
303
304                // update the main attributes
305                min = isNaN($original.prop("min")) || $original.prop("min") === "" ? -Infinity : parseFloat($original.prop("min"))
306                max = isNaN($original.prop("max")) || $original.prop("max") === "" ? Infinity : parseFloat($original.prop("max"))
307                step = parseFloat($original.prop("step")) || 1
308                if ($original.attr("hidden")) {
309                    $inputGroup.attr("hidden", $original.attr("hidden"))
310                } else {
311                    $inputGroup.removeAttr("hidden")
312                }
313                if ($original.attr("id")) {
314                    $input.attr("id", $original.attr("id") + "_MP_cBdLN29i2")
315                    if ($label[0]) {
316                        $label.attr("for", $input.attr("id"))
317                    }
318                }
319            }
320        })
321
322        return this
323    }
324
325    function onPointerUp(element, callback) {
326        element.addEventListener("mouseup", function (e) {
327            callback(e)
328        })
329        element.addEventListener("touchend", function (e) {
330            callback(e)
331        })
332        element.addEventListener("keyup", function (e) {
333            if ((e.keyCode === 32 || e.keyCode === 13)) {
334                triggerKeyPressed = false
335                callback(e)
336            }
337        })
338    }
339
340    function onPointerDown(element, callback) {
341        element.addEventListener("mousedown", function (e) {
342            if (e.button === 0) {
343                e.preventDefault()
344                callback(e)
345            }
346        })
347        element.addEventListener("touchstart", function (e) {
348            if (e.cancelable) {
349                e.preventDefault()
350            }
351            callback(e)
352        })
353        element.addEventListener("keydown", function (e) {
354            if ((e.keyCode === 32 || e.keyCode === 13) && !triggerKeyPressed) {
355                triggerKeyPressed = true
356                callback(e)
357            }
358        })
359    }
360
361}(jQuery))

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.