bootstrap-input-spinner.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. ;(function ($) {
  7. "use strict"
  8. var triggerKeyPressed = false
  9. var originalVal = $.fn.val
  10. $.fn.val = function (value) {
  11. if (arguments.length >= 1) {
  12. if (this[0] && this[0]["bootstrap-input-spinner"] && this[0].setValue) {
  13. var element = this[0]
  14. setTimeout(function () {
  15. element.setValue(value)
  16. })
  17. }
  18. }
  19. return originalVal.apply(this, arguments)
  20. }
  21. $.fn.inputSpinner = function (methodOrOptions) {
  22. if (methodOrOptions === "destroy") {
  23. this.each(function () {
  24. this.destroyInputSpinner()
  25. })
  26. return this
  27. }
  28. var config = {
  29. decrementButton: "<strong>&minus;</strong>", // button text
  30. incrementButton: "<strong>&plus;</strong>", // ..
  31. groupClass: "", // css class of the resulting input-group
  32. buttonsClass: "btn-outline-secondary",
  33. buttonsWidth: "2.5rem",
  34. textAlign: "center",
  35. autoDelay: 500, // ms holding before auto value change
  36. autoInterval: 100, // speed of auto value change
  37. boostThreshold: 10, // boost after these steps
  38. boostMultiplier: "auto" // you can also set a constant number as multiplier
  39. }
  40. for (var option in methodOrOptions) {
  41. // noinspection JSUnfilteredForInLoop
  42. config[option] = methodOrOptions[option]
  43. }
  44. var html = '<div class="input-group ' + config.groupClass + '">' +
  45. '<div class="input-group-prepend">' +
  46. '<button style="min-width: ' + config.buttonsWidth + '" class="btn btn-decrement ' + config.buttonsClass + '" type="button">' + config.decrementButton + '</button>' +
  47. '</div>' +
  48. '<input type="text" inputmode="decimal" style="text-align: ' + config.textAlign + '" class="form-control"/>' +
  49. '<div class="input-group-append">' +
  50. '<button style="min-width: ' + config.buttonsWidth + '" class="btn btn-increment ' + config.buttonsClass + '" type="button">' + config.incrementButton + '</button>' +
  51. '</div>' +
  52. '</div>'
  53. var locale = navigator.language || "en-US"
  54. this.each(function () {
  55. var $original = $(this)
  56. $original[0]["bootstrap-input-spinner"] = true
  57. $original.hide()
  58. var autoDelayHandler = null
  59. var autoIntervalHandler = null
  60. var autoMultiplier = config.boostMultiplier === "auto"
  61. var boostMultiplier = autoMultiplier ? 1 : config.boostMultiplier
  62. var $inputGroup = $(html)
  63. var $buttonDecrement = $inputGroup.find(".btn-decrement")
  64. var $buttonIncrement = $inputGroup.find(".btn-increment")
  65. var $input = $inputGroup.find("input")
  66. var min = null
  67. var max = null
  68. var step = null
  69. var stepMax = null
  70. var decimals = null
  71. var digitGrouping = null
  72. var numberFormat = null
  73. updateAttributes()
  74. var value = parseFloat($original[0].value)
  75. var boostStepsCount = 0
  76. var prefix = $original.attr("data-prefix") || ""
  77. var suffix = $original.attr("data-suffix") || ""
  78. if (prefix) {
  79. var prefixElement = $('<span class="input-group-text">' + prefix + '</span>')
  80. $inputGroup.find(".input-group-prepend").append(prefixElement)
  81. }
  82. if (suffix) {
  83. var suffixElement = $('<span class="input-group-text">' + suffix + '</span>')
  84. $inputGroup.find(".input-group-append").prepend(suffixElement)
  85. }
  86. $original[0].setValue = function (newValue) {
  87. setValue(newValue)
  88. }
  89. $original[0].destroyInputSpinner = function () {
  90. destroy()
  91. }
  92. var observer = new MutationObserver(function () {
  93. updateAttributes()
  94. setValue(value, true)
  95. })
  96. observer.observe($original[0], {attributes: true})
  97. $original.after($inputGroup)
  98. setValue(value)
  99. $input.on("paste input change focusout", function (event) {
  100. var newValue = $input[0].value
  101. var focusOut = event.type === "focusout"
  102. newValue = parseLocaleNumber(newValue)
  103. setValue(newValue, focusOut)
  104. dispatchEvent($original, event.type)
  105. })
  106. onPointerDown($buttonDecrement[0], function () {
  107. stepHandling(-step)
  108. })
  109. onPointerDown($buttonIncrement[0], function () {
  110. stepHandling(step)
  111. })
  112. onPointerUp(document.body, function () {
  113. resetTimer()
  114. })
  115. function setValue(newValue, updateInput) {
  116. if (updateInput === undefined) {
  117. updateInput = true
  118. }
  119. if (isNaN(newValue) || newValue === "") {
  120. $original[0].value = ""
  121. if (updateInput) {
  122. $input[0].value = ""
  123. }
  124. value = NaN
  125. } else {
  126. newValue = parseFloat(newValue)
  127. newValue = Math.min(Math.max(newValue, min), max)
  128. newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals)
  129. $original[0].value = newValue
  130. if (updateInput) {
  131. $input[0].value = numberFormat.format(newValue)
  132. }
  133. value = newValue
  134. }
  135. }
  136. function destroy() {
  137. $original.prop("required", $input.prop("required"))
  138. observer.disconnect()
  139. resetTimer()
  140. $input.off("paste input change focusout")
  141. $inputGroup.remove()
  142. $original.show()
  143. }
  144. function dispatchEvent($element, type) {
  145. if (type) {
  146. setTimeout(function () {
  147. var event
  148. if (typeof (Event) === 'function') {
  149. event = new Event(type, {bubbles: true})
  150. } else { // IE
  151. event = document.createEvent('Event')
  152. event.initEvent(type, true, true)
  153. }
  154. $element[0].dispatchEvent(event)
  155. })
  156. }
  157. }
  158. function stepHandling(step) {
  159. if (!$input[0].disabled && !$input[0].readOnly) {
  160. calcStep(step)
  161. resetTimer()
  162. autoDelayHandler = setTimeout(function () {
  163. autoIntervalHandler = setInterval(function () {
  164. if (boostStepsCount > config.boostThreshold) {
  165. if (autoMultiplier) {
  166. calcStep(step * parseInt(boostMultiplier, 10))
  167. if (boostMultiplier < 100000000) {
  168. boostMultiplier = boostMultiplier * 1.1
  169. }
  170. if (stepMax) {
  171. boostMultiplier = Math.min(stepMax, boostMultiplier)
  172. }
  173. } else {
  174. calcStep(step * boostMultiplier)
  175. }
  176. } else {
  177. calcStep(step)
  178. }
  179. boostStepsCount++
  180. }, config.autoInterval)
  181. }, config.autoDelay)
  182. }
  183. }
  184. function calcStep(step) {
  185. if (isNaN(value)) {
  186. value = 0
  187. }
  188. setValue(Math.round(value / step) * step + step)
  189. dispatchEvent($original, "input")
  190. dispatchEvent($original, "change")
  191. }
  192. function resetTimer() {
  193. boostStepsCount = 0
  194. boostMultiplier = boostMultiplier = autoMultiplier ? 1 : config.boostMultiplier
  195. clearTimeout(autoDelayHandler)
  196. clearTimeout(autoIntervalHandler)
  197. }
  198. function updateAttributes() {
  199. // copy properties from original to the new input
  200. if($original.prop("required")) {
  201. $input.prop("required", $original.prop("required"))
  202. $original.removeAttr('required')
  203. }
  204. $input.prop("placeholder", $original.prop("placeholder"))
  205. $input.attr("inputmode", $original.attr("inputmode") || "decimal")
  206. var disabled = $original.prop("disabled")
  207. var readonly = $original.prop("readonly")
  208. $input.prop("disabled", disabled)
  209. $input.prop("readonly", readonly)
  210. $buttonIncrement.prop("disabled", disabled || readonly)
  211. $buttonDecrement.prop("disabled", disabled || readonly)
  212. if (disabled || readonly) {
  213. resetTimer()
  214. }
  215. var originalClass = $original.prop("class")
  216. var groupClass = ""
  217. // sizing
  218. if (/form-control-sm/g.test(originalClass)) {
  219. groupClass = "input-group-sm"
  220. } else if (/form-control-lg/g.test(originalClass)) {
  221. groupClass = "input-group-lg"
  222. }
  223. var inputClass = originalClass.replace(/form-control(-(sm|lg))?/g, "")
  224. $inputGroup.prop("class", "input-group " + groupClass + " " + config.groupClass)
  225. $input.prop("class", "form-control " + inputClass)
  226. // update the main attributes
  227. min = parseFloat($original.prop("min")) || 0
  228. max = isNaN($original.prop("max")) || $original.prop("max") === "" ? Infinity : parseFloat($original.prop("max"))
  229. step = parseFloat($original.prop("step")) || 1
  230. stepMax = parseInt($original.attr("data-step-max")) || 0
  231. var newDecimals = parseInt($original.attr("data-decimals")) || 0
  232. var newDigitGrouping = !($original.attr("data-digit-grouping") === "false")
  233. if (decimals !== newDecimals || digitGrouping !== newDigitGrouping) {
  234. decimals = newDecimals
  235. digitGrouping = newDigitGrouping
  236. numberFormat = new Intl.NumberFormat(locale, {
  237. minimumFractionDigits: decimals,
  238. maximumFractionDigits: decimals,
  239. useGrouping: digitGrouping
  240. })
  241. }
  242. }
  243. function parseLocaleNumber(stringNumber) {
  244. var numberFormat = new Intl.NumberFormat(locale)
  245. var thousandSeparator = numberFormat.format(11111).replace(/1/g, '') || '.'
  246. var decimalSeparator = numberFormat.format(1.1).replace(/1/g, '')
  247. return parseFloat(stringNumber
  248. .replace(new RegExp(' ', 'g'), '')
  249. .replace(new RegExp('\\' + thousandSeparator, 'g'), '')
  250. .replace(new RegExp('\\' + decimalSeparator), '.')
  251. )
  252. }
  253. })
  254. return this
  255. }
  256. function onPointerUp(element, callback) {
  257. element.addEventListener("mouseup", function (e) {
  258. callback(e)
  259. })
  260. element.addEventListener("touchend", function (e) {
  261. callback(e)
  262. })
  263. element.addEventListener("keyup", function (e) {
  264. if ((e.keyCode === 32 || e.keyCode === 13)) {
  265. triggerKeyPressed = false
  266. callback(e)
  267. }
  268. })
  269. }
  270. function onPointerDown(element, callback) {
  271. element.addEventListener("mousedown", function (e) {
  272. e.preventDefault()
  273. callback(e)
  274. })
  275. element.addEventListener("touchstart", function (e) {
  276. if (e.cancelable) {
  277. e.preventDefault()
  278. }
  279. callback(e)
  280. })
  281. element.addEventListener("keydown", function (e) {
  282. if ((e.keyCode === 32 || e.keyCode === 13) && !triggerKeyPressed) {
  283. triggerKeyPressed = true
  284. callback(e)
  285. }
  286. })
  287. }
  288. }(jQuery))