InputNumber

自定义数值输入框元素

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/432807/1160998/InputNumber.js

  1. /**
  2. * @file 自定义数值输入框元素 InputNumber
  3. * @version 1.1.1.20230313
  4. * @author Laster2800
  5. * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/CustomElements/InputNumber InputNumber}
  6. */
  7.  
  8. if (!customElements.get('laster2800-input-number')) {
  9. customElements.define('laster2800-input-number', class InputNumber extends HTMLInputElement {
  10. #arrowTid = false
  11.  
  12. static get observedAttributes() {
  13. return ['type', 'step']
  14. }
  15.  
  16. constructor() {
  17. super()
  18. this.#update()
  19. this.addEventListener('input', this.#onInput)
  20. this.addEventListener('blur', this.#onInputDone)
  21. this.addEventListener('keydown', this.#onArrowDown)
  22. this.addEventListener('keyup', this.#onArrowUp)
  23. }
  24.  
  25. #onInput() {
  26. let val = this.value
  27. const regex = this.min < 0 ? /-?\d+(\.(\d+)?)?|-/ : /\d+(\.(\d+)?)?/
  28. val = regex.exec(val)?.[0] ?? ''
  29. if (val) {
  30. if (val !== '-') {
  31. // input listener 只应处理外极值
  32. if (val.startsWith('-')) {
  33. if (val < this.min) {
  34. val = String(this.min)
  35. }
  36. } else {
  37. if (val > this.max) {
  38. val = String(this.max)
  39. }
  40. }
  41. if (this.digits !== Number.POSITIVE_INFINITY) { // Number#toFixed() 不便于动态精度
  42. if (this.digits > 0) {
  43. const m = new RegExp(String.raw`(.+\.\d{${this.digits}}).+`).exec(val)
  44. if (m) {
  45. val = m[1]
  46. }
  47. } else {
  48. const idx = val.indexOf('.')
  49. if (idx >= 0) {
  50. val = val.slice(0, idx)
  51. }
  52. }
  53. }
  54. }
  55. this.value = val
  56. } else {
  57. this.value = ''
  58. }
  59. }
  60.  
  61. #onInputDone() {
  62. let val = this.value
  63. if (val === '') {
  64. val = this.defaultValue
  65. }
  66. val = Number.parseFloat(val)
  67. if (!Number.isNaN(val)) {
  68. if (val === 0) {
  69. if (this.allowZero === 'true') {
  70. this.value = '0'
  71. return
  72. } else if (this.allowZero === 'false') {
  73. val = Number.parseFloat(this.defaultValue)
  74. if (val === 0) {
  75. this.value = '0'
  76. } else {
  77. this.value = ''
  78. this.#onInputDone()
  79. }
  80. return
  81. }
  82. }
  83. if (val > this.max) {
  84. val = this.max
  85. } else if (val < this.min) {
  86. val = this.min
  87. }
  88. if (this.digits !== Number.POSITIVE_INFINITY) {
  89. val = String(val)
  90. if (this.digits > 0) {
  91. const m = new RegExp(String.raw`(.+\.\d{${this.digits}}).+`).exec(val)
  92. if (m) {
  93. val = m[1]
  94. }
  95. } else {
  96. const idx = val.indexOf('.')
  97. if (idx >= 0) {
  98. val = val.slice(0, idx)
  99. }
  100. }
  101. }
  102. this.value = val
  103. }
  104. }
  105.  
  106. /** @param {KeyboardEvent} e */
  107. #onArrowDown(e) {
  108. let move = ({ ArrowUp: 1, ArrowDown: -1 })[e.key]
  109. if (move) {
  110. e.preventDefault()
  111. if (this.#arrowTid) return
  112. this.#arrowTid = setTimeout(() => { this.#arrowTid = null }, 100)
  113.  
  114. if (e.altKey) {
  115. move *= 0.1
  116. } else if (e.shiftKey) {
  117. move *= 10
  118. } else if (e.ctrlKey) {
  119. move *= 100
  120. }
  121.  
  122. let val = this.value
  123. if (val === '') {
  124. val = this.defaultValue
  125. }
  126. val = Number.parseFloat(val)
  127. if (Number.isNaN(val)) return
  128. val += move
  129. if (val > this.max) {
  130. val = this.max
  131. } else if (val < this.min) {
  132. val = this.min
  133. }
  134. this.value = this.digits === Number.POSITIVE_INFINITY ? val : val.toFixed(this.digits)
  135.  
  136. this.dispatchEvent(new Event('change'))
  137. }
  138. }
  139.  
  140. /** @param {KeyboardEvent} e */
  141. #onArrowUp(e) {
  142. if (['ArrowUp', 'ArrowDown'].includes(e.key) && this.#arrowTid) {
  143. clearTimeout(this.#arrowTid)
  144. this.#arrowTid = null
  145. }
  146. }
  147.  
  148. set digits(val) {
  149. this.setAttribute('digits', val)
  150. }
  151.  
  152. get digits() {
  153. return this.#getNumberAttr('digits', 0, 0)
  154. }
  155.  
  156. set max(val) {
  157. this.setAttribute('max', val)
  158. }
  159.  
  160. get max() {
  161. return this.#getNumberAttr('max', Number.POSITIVE_INFINITY)
  162. }
  163.  
  164. set min(val) {
  165. this.setAttribute('min', val)
  166. }
  167.  
  168. get min() {
  169. return this.#getNumberAttr('min', 0)
  170. }
  171.  
  172. set allowZero(val) {
  173. this.setAttribute('allow-zero', val)
  174. }
  175.  
  176. get allowZero() {
  177. return this.getAttribute('allow-zero')
  178. }
  179.  
  180. attributeChangedCallback(name, oldValue, newValue) {
  181. newValue && this.removeAttribute(name)
  182. }
  183.  
  184. /**
  185. * 内部更新
  186. */
  187. #update() {
  188. if (this.getAttribute('type')) {
  189. this.removeAttribute('type')
  190. }
  191. }
  192.  
  193. /**
  194. * 获取数值属性值
  195. * @param {string} name 属性名
  196. * @param {number | string} [defaultVal=0] 默认值
  197. * @param {number} [digits=Number.POSITIVE_INFINITY] 保留到小数点后几位
  198. * @returns {number} 数值属性值
  199. */
  200. #getNumberAttr(name, defaultVal = 0, digits = Number.POSITIVE_INFINITY) {
  201. let val = this.getAttribute(name)
  202. if (val == null) return defaultVal
  203. val = Number.parseFloat(val)
  204. if (Number.isNaN(val)) return defaultVal
  205. if (digits === 0) {
  206. val = Math.round(val)
  207. } else if (digits !== Number.POSITIVE_INFINITY) {
  208. val = Number.parseFloat(val.toFixed(digits))
  209. }
  210. return val
  211. }
  212. }, { extends: 'input' })
  213. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址