number_box.base.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /**
  2. * DevExtreme (ui/number_box/number_box.base.js)
  3. * Version: 19.1.16
  4. * Build date: Tue Oct 18 2022
  5. *
  6. * Copyright (c) 2012 - 2022 Developer Express Inc. ALL RIGHTS RESERVED
  7. * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
  8. */
  9. "use strict";
  10. var $ = require("../../core/renderer");
  11. var domAdapter = require("../../core/dom_adapter");
  12. var eventsEngine = require("../../events/core/events_engine");
  13. var commonUtils = require("../../core/utils/common");
  14. var typeUtils = require("../../core/utils/type");
  15. var mathUtils = require("../../core/utils/math");
  16. var extend = require("../../core/utils/extend").extend;
  17. var inArray = require("../../core/utils/array").inArray;
  18. var devices = require("../../core/devices");
  19. var browser = require("../../core/utils/browser");
  20. var TextEditor = require("../text_box/ui.text_editor");
  21. var eventUtils = require("../../events/utils");
  22. var SpinButtons = require("./number_box.spins").default;
  23. var messageLocalization = require("../../localization/message");
  24. var Deferred = require("../../core/utils/deferred").Deferred;
  25. var math = Math;
  26. var WIDGET_CLASS = "dx-numberbox";
  27. var FIREFOX_CONTROL_KEYS = ["tab", "del", "backspace", "leftArrow", "rightArrow", "home", "end", "enter"];
  28. var FORCE_VALUECHANGE_EVENT_NAMESPACE = "NumberBoxForceValueChange";
  29. var NumberBoxBase = TextEditor.inherit({
  30. _supportedKeys: function() {
  31. return extend(this.callBase(), {
  32. upArrow: function(e) {
  33. e.preventDefault();
  34. e.stopPropagation();
  35. this._spinUpChangeHandler(e)
  36. },
  37. downArrow: function(e) {
  38. e.preventDefault();
  39. e.stopPropagation();
  40. this._spinDownChangeHandler(e)
  41. },
  42. enter: function() {}
  43. })
  44. },
  45. _getDefaultOptions: function() {
  46. return extend(this.callBase(), {
  47. value: 0,
  48. min: void 0,
  49. max: void 0,
  50. step: 1,
  51. showSpinButtons: false,
  52. useLargeSpinButtons: true,
  53. mode: "text",
  54. invalidValueMessage: messageLocalization.format("dxNumberBox-invalidValueMessage"),
  55. buttons: void 0
  56. })
  57. },
  58. _getDefaultButtons: function() {
  59. return this.callBase().concat([{
  60. name: "spins",
  61. Ctor: SpinButtons
  62. }])
  63. },
  64. _isSupportInputMode: function() {
  65. var version = parseFloat(browser.version);
  66. return browser.chrome && version >= 66 || browser.safari && version >= 12 || browser.msie && version >= 75
  67. },
  68. _defaultOptionsRules: function() {
  69. return this.callBase().concat([{
  70. device: function() {
  71. return devices.real().generic && !devices.isSimulator()
  72. },
  73. options: {
  74. useLargeSpinButtons: false
  75. }
  76. }, {
  77. device: function() {
  78. return "generic" !== devices.real().platform && !this._isSupportInputMode()
  79. }.bind(this),
  80. options: {
  81. mode: "number"
  82. }
  83. }])
  84. },
  85. _initMarkup: function() {
  86. this._renderSubmitElement();
  87. this.$element().addClass(WIDGET_CLASS);
  88. this.callBase()
  89. },
  90. _applyInputAttributes: function($input, customAttributes) {
  91. $input.attr("inputmode", "decimal");
  92. this.callBase($input, customAttributes)
  93. },
  94. _renderContentImpl: function() {
  95. this.option("isValid") && this._validateValue(this.option("value"));
  96. this.setAria("role", "spinbutton")
  97. },
  98. _renderSubmitElement: function() {
  99. this._$submitElement = $("<input>").attr("type", "hidden").appendTo(this.$element());
  100. this._setSubmitValue(this.option("value"))
  101. },
  102. _setSubmitValue: function(value) {
  103. this._getSubmitElement().val(commonUtils.applyServerDecimalSeparator(value))
  104. },
  105. _getSubmitElement: function() {
  106. return this._$submitElement
  107. },
  108. _keyPressHandler: function(e) {
  109. this.callBase(e);
  110. var char = eventUtils.getChar(e);
  111. var validCharRegExp = /[\d.,eE\-+]|Subtract/;
  112. var isInputCharValid = validCharRegExp.test(char);
  113. if (!isInputCharValid) {
  114. var keyName = eventUtils.normalizeKeyName(e);
  115. if (e.metaKey || e.ctrlKey || keyName && inArray(keyName, FIREFOX_CONTROL_KEYS) >= 0) {
  116. return
  117. }
  118. e.preventDefault();
  119. return false
  120. }
  121. this._keyPressed = true
  122. },
  123. _onMouseWheel: function(dxEvent) {
  124. dxEvent.delta > 0 ? this._spinValueChange(1, dxEvent) : this._spinValueChange(-1, dxEvent)
  125. },
  126. _renderValue: function() {
  127. var inputValue = this._input().val();
  128. var value = this.option("value");
  129. if (!inputValue.length || Number(inputValue) !== value) {
  130. this._forceValueRender();
  131. this._toggleEmptinessEventHandler()
  132. }
  133. var valueText = typeUtils.isDefined(value) ? null : messageLocalization.format("dxNumberBox-noDataText");
  134. this.setAria({
  135. valuenow: commonUtils.ensureDefined(value, ""),
  136. valuetext: valueText
  137. });
  138. this.option("text", this._input().val());
  139. this._updateButtons();
  140. return (new Deferred).resolve()
  141. },
  142. _forceValueRender: function() {
  143. var value = this.option("value");
  144. var number = Number(value);
  145. var formattedValue = isNaN(number) ? "" : this._applyValueFormat(value);
  146. this._renderDisplayText(formattedValue)
  147. },
  148. _applyValueFormat: function(value) {
  149. return this.option("valueFormat")(value)
  150. },
  151. _renderProps: function() {
  152. this.callBase();
  153. this._input().prop({
  154. min: this.option("min"),
  155. max: this.option("max"),
  156. step: this.option("step")
  157. });
  158. this.setAria({
  159. valuemin: commonUtils.ensureDefined(this.option("min"), ""),
  160. valuemax: commonUtils.ensureDefined(this.option("max"), "")
  161. })
  162. },
  163. _spinButtonsPointerDownHandler: function() {
  164. var $input = this._input();
  165. if (!this.option("useLargeSpinButtons") && domAdapter.getActiveElement() !== $input[0]) {
  166. eventsEngine.trigger($input, "focus")
  167. }
  168. },
  169. _spinUpChangeHandler: function(e) {
  170. if (!this.option("readOnly")) {
  171. this._spinValueChange(1, e.event || e)
  172. }
  173. },
  174. _spinDownChangeHandler: function(e) {
  175. if (!this.option("readOnly")) {
  176. this._spinValueChange(-1, e.event || e)
  177. }
  178. },
  179. _spinValueChange: function(sign, dxEvent) {
  180. var step = parseFloat(this.option("step"));
  181. if (0 === step) {
  182. return
  183. }
  184. var value = parseFloat(this._normalizeInputValue()) || 0;
  185. value = this._correctRounding(value, step * sign);
  186. var min = this.option("min");
  187. var max = this.option("max");
  188. if (void 0 !== min) {
  189. value = Math.max(min, value)
  190. }
  191. if (void 0 !== max) {
  192. value = Math.min(max, value)
  193. }
  194. this._saveValueChangeEvent(dxEvent);
  195. this.option("value", value)
  196. },
  197. _correctRounding: function(value, step) {
  198. var regex = /[,.](.*)/;
  199. var isFloatValue = regex.test(value);
  200. var isFloatStep = regex.test(step);
  201. if (isFloatValue || isFloatStep) {
  202. var valueAccuracy = isFloatValue ? regex.exec(value)[0].length : 0;
  203. var stepAccuracy = isFloatStep ? regex.exec(step)[0].length : 0;
  204. var accuracy = math.max(valueAccuracy, stepAccuracy);
  205. value = this._round(value + step, accuracy);
  206. return value
  207. }
  208. return value + step
  209. },
  210. _round: function(value, precision) {
  211. precision = precision || 0;
  212. var multiplier = Math.pow(10, precision);
  213. value *= multiplier;
  214. value = Math.round(value) / multiplier;
  215. return value
  216. },
  217. _renderValueChangeEvent: function() {
  218. this.callBase();
  219. var forceValueChangeEvent = eventUtils.addNamespace("focusout", FORCE_VALUECHANGE_EVENT_NAMESPACE);
  220. eventsEngine.off(this.element(), forceValueChangeEvent);
  221. eventsEngine.on(this.element(), forceValueChangeEvent, this._forceRefreshInputValue.bind(this))
  222. },
  223. _forceRefreshInputValue: function() {
  224. if ("number" === this.option("mode")) {
  225. return
  226. }
  227. var $input = this._input();
  228. var formattedValue = this._applyValueFormat(this.option("value"));
  229. $input.val(null);
  230. $input.val(formattedValue)
  231. },
  232. _valueChangeEventHandler: function(e) {
  233. var $input = this._input();
  234. var inputValue = this._normalizeText();
  235. var value = this._parseValue(inputValue);
  236. var valueHasDigits = "." !== inputValue && "-" !== inputValue;
  237. if (this._isValueValid() && !this._validateValue(value)) {
  238. $input.val(this._applyValueFormat(value));
  239. return
  240. }
  241. if (valueHasDigits) {
  242. this.callBase(e, isNaN(value) ? null : value)
  243. }
  244. this._applyValueBoundaries(inputValue, value);
  245. this.validationRequest.fire({
  246. value: value,
  247. editor: this
  248. })
  249. },
  250. _applyValueBoundaries: function(inputValue, parsedValue) {
  251. var isValueIncomplete = this._isValueIncomplete(inputValue);
  252. var isValueCorrect = this._isValueInRange(inputValue);
  253. if (!isValueIncomplete && !isValueCorrect && null !== parsedValue) {
  254. if (Number(inputValue) !== parsedValue) {
  255. this._input().val(this._applyValueFormat(parsedValue))
  256. }
  257. }
  258. },
  259. _replaceCommaWithPoint: function(value) {
  260. return value.replace(",", ".")
  261. },
  262. _inputIsInvalid: function() {
  263. var isNumberMode = "number" === this.option("mode");
  264. var validityState = this._input().get(0).validity;
  265. return isNumberMode && validityState && validityState.badInput
  266. },
  267. _renderDisplayText: function(text) {
  268. if (this._inputIsInvalid()) {
  269. return
  270. }
  271. this.callBase(text)
  272. },
  273. _isValueIncomplete: function(value) {
  274. var incompleteRegex = /(^-$)|(^-?\d*\.$)|(\d+e-?$)/i;
  275. return incompleteRegex.test(value)
  276. },
  277. _isValueInRange: function(value) {
  278. return mathUtils.inRange(value, this.option("min"), this.option("max"))
  279. },
  280. _isNumber: function(value) {
  281. return null !== this._parseValue(value)
  282. },
  283. _validateValue: function(value) {
  284. var inputValue = this._normalizeText();
  285. var isValueValid = this._isValueValid();
  286. var isValid = true;
  287. var isNumber = this._isNumber(inputValue);
  288. if (isNaN(Number(value))) {
  289. isValid = false
  290. }
  291. if (!value && isValueValid) {
  292. isValid = true
  293. } else {
  294. if (!isNumber && !isValueValid) {
  295. isValid = false
  296. }
  297. }
  298. this.option({
  299. isValid: isValid,
  300. validationError: isValid ? null : {
  301. editorSpecific: true,
  302. message: this.option("invalidValueMessage")
  303. }
  304. });
  305. return isValid
  306. },
  307. _normalizeInputValue: function() {
  308. return this._parseValue(this._normalizeText())
  309. },
  310. _normalizeText: function() {
  311. var value = this._input().val().trim();
  312. return this._replaceCommaWithPoint(value)
  313. },
  314. _parseValue: function(value) {
  315. var number = parseFloat(value);
  316. if (isNaN(number)) {
  317. return null
  318. }
  319. return mathUtils.fitIntoRange(number, this.option("min"), this.option("max"))
  320. },
  321. _clearValue: function() {
  322. if (this._inputIsInvalid()) {
  323. this._input().val("");
  324. this._validateValue()
  325. }
  326. this.callBase()
  327. },
  328. reset: function() {
  329. if (null === this.option("value")) {
  330. this.option("text", "");
  331. this._renderValue()
  332. } else {
  333. this.option("value", null)
  334. }
  335. },
  336. _optionChanged: function(args) {
  337. switch (args.name) {
  338. case "value":
  339. this._validateValue(args.value);
  340. this._setSubmitValue(args.value);
  341. this.callBase(args);
  342. this._resumeValueChangeAction();
  343. break;
  344. case "step":
  345. this._renderProps();
  346. break;
  347. case "min":
  348. case "max":
  349. this._renderProps();
  350. this.option("value", this._parseValue(this.option("value")));
  351. break;
  352. case "showSpinButtons":
  353. case "useLargeSpinButtons":
  354. this._updateButtons(["spins"]);
  355. break;
  356. case "invalidValueMessage":
  357. break;
  358. default:
  359. this.callBase(args)
  360. }
  361. }
  362. });
  363. module.exports = NumberBoxBase;