ui.text_editor.mask.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * DevExtreme (ui/text_box/ui.text_editor.mask.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 caret = require("./utils.caret");
  12. var devices = require("../../core/devices");
  13. var each = require("../../core/utils/iterator").each;
  14. var eventUtils = require("../../events/utils");
  15. var eventsEngine = require("../../events/core/events_engine");
  16. var extend = require("../../core/utils/extend").extend;
  17. var focused = require("../widget/selectors").focused;
  18. var isDefined = require("../../core/utils/type").isDefined;
  19. var messageLocalization = require("../../localization/message");
  20. var noop = require("../../core/utils/common").noop;
  21. var stringUtils = require("../../core/utils/string");
  22. var wheelEvent = require("../../events/core/wheel");
  23. var MaskRules = require("./ui.text_editor.mask.rule");
  24. var TextEditorBase = require("./ui.text_editor.base");
  25. var DefaultMaskStrategy = require("./ui.text_editor.mask.strategy.default").default;
  26. var AndroidMaskStrategy = require("./ui.text_editor.mask.strategy.android").default;
  27. var stubCaret = function() {
  28. return {}
  29. };
  30. var EMPTY_CHAR = " ";
  31. var ESCAPED_CHAR = "\\";
  32. var TEXTEDITOR_MASKED_CLASS = "dx-texteditor-masked";
  33. var FORWARD_DIRECTION = "forward";
  34. var BACKWARD_DIRECTION = "backward";
  35. var buildInMaskRules = {
  36. 0: /[0-9]/,
  37. 9: /[0-9\s]/,
  38. "#": /[-+0-9\s]/,
  39. L: function(char) {
  40. return isLiteralChar(char)
  41. },
  42. l: function(char) {
  43. return isLiteralChar(char) || isSpaceChar(char)
  44. },
  45. C: /\S/,
  46. c: /./,
  47. A: function(char) {
  48. return isLiteralChar(char) || isNumericChar(char)
  49. },
  50. a: function(char) {
  51. return isLiteralChar(char) || isNumericChar(char) || isSpaceChar(char)
  52. }
  53. };
  54. var isNumericChar = function(char) {
  55. return /[0-9]/.test(char)
  56. };
  57. var isLiteralChar = function(char) {
  58. var code = char.charCodeAt();
  59. return 64 < code && code < 91 || 96 < code && code < 123 || code > 127
  60. };
  61. var isSpaceChar = function(char) {
  62. return " " === char
  63. };
  64. var TextEditorMask = TextEditorBase.inherit({
  65. _getDefaultOptions: function() {
  66. return extend(this.callBase(), {
  67. mask: "",
  68. maskChar: "_",
  69. maskRules: {},
  70. maskInvalidMessage: messageLocalization.format("validation-mask"),
  71. useMaskedValue: false,
  72. showMaskMode: "always"
  73. })
  74. },
  75. _supportedKeys: function() {
  76. var that = this;
  77. var keyHandlerMap = {
  78. backspace: that._maskStrategy.getHandler("backspace"),
  79. del: that._maskStrategy.getHandler("del"),
  80. enter: that._changeHandler
  81. };
  82. var result = that.callBase();
  83. each(keyHandlerMap, function(key, callback) {
  84. var parentHandler = result[key];
  85. result[key] = function(e) {
  86. that.option("mask") && callback.call(that, e);
  87. parentHandler && parentHandler(e)
  88. }
  89. });
  90. return result
  91. },
  92. _getSubmitElement: function() {
  93. return !this.option("mask") ? this.callBase() : this._$hiddenElement
  94. },
  95. _init: function() {
  96. this.callBase();
  97. this._initMaskStrategy()
  98. },
  99. _initMaskStrategy: function() {
  100. var device = devices.real();
  101. this._maskStrategy = device.android && device.version[0] > 4 ? new AndroidMaskStrategy(this) : new DefaultMaskStrategy(this)
  102. },
  103. _initMarkup: function() {
  104. this._renderHiddenElement();
  105. this.callBase()
  106. },
  107. _attachMouseWheelEventHandlers: function() {
  108. var hasMouseWheelHandler = this._onMouseWheel !== noop;
  109. if (!hasMouseWheelHandler) {
  110. return
  111. }
  112. var input = this._input();
  113. var eventName = eventUtils.addNamespace(wheelEvent.name, this.NAME);
  114. var mouseWheelAction = this._createAction(function(e) {
  115. if (focused(input)) {
  116. var dxEvent = e.event;
  117. this._onMouseWheel(dxEvent);
  118. dxEvent.preventDefault();
  119. dxEvent.stopPropagation()
  120. }
  121. }.bind(this));
  122. eventsEngine.off(input, eventName);
  123. eventsEngine.on(input, eventName, function(e) {
  124. mouseWheelAction({
  125. event: e
  126. })
  127. })
  128. },
  129. _onMouseWheel: noop,
  130. _render: function() {
  131. this.callBase();
  132. this._renderMask();
  133. this._attachMouseWheelEventHandlers()
  134. },
  135. _renderHiddenElement: function() {
  136. if (this.option("mask")) {
  137. this._$hiddenElement = $("<input>").attr("type", "hidden").appendTo(this._inputWrapper())
  138. }
  139. },
  140. _removeHiddenElement: function() {
  141. this._$hiddenElement && this._$hiddenElement.remove()
  142. },
  143. _renderMask: function() {
  144. this.$element().removeClass(TEXTEDITOR_MASKED_CLASS);
  145. this._maskRulesChain = null;
  146. this._maskStrategy.detachEvents();
  147. if (!this.option("mask")) {
  148. return
  149. }
  150. this.$element().addClass(TEXTEDITOR_MASKED_CLASS);
  151. this._maskStrategy.attachEvents();
  152. this._parseMask();
  153. this._renderMaskedValue()
  154. },
  155. _suppressCaretChanging: function(callback, args) {
  156. var originalCaret = caret;
  157. caret = stubCaret;
  158. try {
  159. callback.apply(this, args)
  160. } finally {
  161. caret = originalCaret
  162. }
  163. },
  164. _changeHandler: function(e) {
  165. var $input = this._input();
  166. var inputValue = $input.val();
  167. if (inputValue === this._changedValue) {
  168. return
  169. }
  170. this._changedValue = inputValue;
  171. var changeEvent = eventUtils.createEvent(e, {
  172. type: "change"
  173. });
  174. eventsEngine.trigger($input, changeEvent)
  175. },
  176. _parseMask: function() {
  177. this._maskRules = extend({}, buildInMaskRules, this.option("maskRules"));
  178. this._maskRulesChain = this._parseMaskRule(0)
  179. },
  180. _parseMaskRule: function(index) {
  181. var mask = this.option("mask");
  182. if (index >= mask.length) {
  183. return new MaskRules.EmptyMaskRule
  184. }
  185. var currentMaskChar = mask[index];
  186. var isEscapedChar = currentMaskChar === ESCAPED_CHAR;
  187. var result = isEscapedChar ? new MaskRules.StubMaskRule({
  188. maskChar: mask[index + 1]
  189. }) : this._getMaskRule(currentMaskChar);
  190. result.next(this._parseMaskRule(index + 1 + isEscapedChar));
  191. return result
  192. },
  193. _getMaskRule: function(pattern) {
  194. var ruleConfig;
  195. each(this._maskRules, function(rulePattern, allowedChars) {
  196. if (rulePattern === pattern) {
  197. ruleConfig = {
  198. pattern: rulePattern,
  199. allowedChars: allowedChars
  200. };
  201. return false
  202. }
  203. });
  204. return isDefined(ruleConfig) ? new MaskRules.MaskRule(extend({
  205. maskChar: this.option("maskChar")
  206. }, ruleConfig)) : new MaskRules.StubMaskRule({
  207. maskChar: pattern
  208. })
  209. },
  210. _renderMaskedValue: function() {
  211. if (!this._maskRulesChain) {
  212. return
  213. }
  214. var value = this.option("value") || "";
  215. this._maskRulesChain.clear(this._normalizeChainArguments());
  216. var chainArgs = {
  217. length: value.length
  218. };
  219. chainArgs[this._isMaskedValueMode() ? "text" : "value"] = value;
  220. this._handleChain(chainArgs);
  221. this._displayMask()
  222. },
  223. _replaceSelectedText: function(text, selection, char) {
  224. if (void 0 === char) {
  225. return text
  226. }
  227. var textBefore = text.slice(0, selection.start);
  228. var textAfter = text.slice(selection.end);
  229. var edited = textBefore + char + textAfter;
  230. return edited
  231. },
  232. _isMaskedValueMode: function() {
  233. return this.option("useMaskedValue")
  234. },
  235. _displayMask: function(caret) {
  236. caret = caret || this._caret();
  237. this._renderValue();
  238. this._caret(caret)
  239. },
  240. _isValueEmpty: function() {
  241. return stringUtils.isEmpty(this._value)
  242. },
  243. _shouldShowMask: function() {
  244. var showMaskMode = this.option("showMaskMode");
  245. if ("onFocus" === showMaskMode) {
  246. return focused(this._input()) || !this._isValueEmpty()
  247. }
  248. return true
  249. },
  250. _showMaskPlaceholder: function() {
  251. if (this._shouldShowMask()) {
  252. var text = this._maskRulesChain.text();
  253. this.option("text", text);
  254. if ("onFocus" === this.option("showMaskMode")) {
  255. this._renderDisplayText(text)
  256. }
  257. }
  258. },
  259. _renderValue: function() {
  260. if (this._maskRulesChain) {
  261. var text = this._maskRulesChain.text();
  262. this._showMaskPlaceholder();
  263. if (this._$hiddenElement) {
  264. var value = this._maskRulesChain.value();
  265. var hiddenElementValue = this._isMaskedValueMode() ? text : value;
  266. this._$hiddenElement.val(!stringUtils.isEmpty(value) ? hiddenElementValue : "")
  267. }
  268. }
  269. return this.callBase()
  270. },
  271. _valueChangeEventHandler: function(e) {
  272. if (!this._maskRulesChain) {
  273. this.callBase.apply(this, arguments);
  274. return
  275. }
  276. this._saveValueChangeEvent(e);
  277. this.option("value", this._convertToValue().replace(/\s+$/, ""))
  278. },
  279. _isControlKeyFired: function(e) {
  280. return this._isControlKey(eventUtils.normalizeKeyName(e)) || e.ctrlKey || e.metaKey
  281. },
  282. _handleChain: function(args) {
  283. var handledCount = this._maskRulesChain.handle(this._normalizeChainArguments(args));
  284. this._value = this._maskRulesChain.value();
  285. this._textValue = this._maskRulesChain.text();
  286. return handledCount
  287. },
  288. _normalizeChainArguments: function(args) {
  289. args = args || {};
  290. args.index = 0;
  291. args.fullText = this._maskRulesChain.text();
  292. return args
  293. },
  294. _convertToValue: function(text) {
  295. if (this._isMaskedValueMode()) {
  296. text = this._replaceMaskCharWithEmpty(text || this._textValue || "")
  297. } else {
  298. text = text || this._value || ""
  299. }
  300. return text
  301. },
  302. _replaceMaskCharWithEmpty: function(text) {
  303. return text.replace(new RegExp(this.option("maskChar"), "g"), EMPTY_CHAR)
  304. },
  305. _maskKeyHandler: function(e, keyHandler) {
  306. var _this = this;
  307. if (this.option("readOnly")) {
  308. return
  309. }
  310. this.setForwardDirection();
  311. e.preventDefault();
  312. this._handleSelection();
  313. var previousText = this._input().val();
  314. var raiseInputEvent = function() {
  315. if (previousText !== _this._input().val()) {
  316. _this._maskStrategy.runWithoutEventProcessing(function() {
  317. return eventsEngine.trigger(_this._input(), "input")
  318. })
  319. }
  320. };
  321. var handled = keyHandler();
  322. if (handled) {
  323. handled.then(raiseInputEvent)
  324. } else {
  325. this.setForwardDirection();
  326. this._adjustCaret();
  327. this._displayMask();
  328. this._maskRulesChain.reset();
  329. raiseInputEvent()
  330. }
  331. },
  332. _handleKey: function(key, direction) {
  333. this._direction(direction || FORWARD_DIRECTION);
  334. this._adjustCaret(key);
  335. this._handleKeyChain(key);
  336. this._moveCaret()
  337. },
  338. _handleSelection: function() {
  339. if (!this._hasSelection()) {
  340. return
  341. }
  342. var caret = this._caret();
  343. var emptyChars = new Array(caret.end - caret.start + 1).join(EMPTY_CHAR);
  344. this._handleKeyChain(emptyChars)
  345. },
  346. _handleKeyChain: function(chars) {
  347. var caret = this._caret();
  348. var start = this.isForwardDirection() ? caret.start : caret.start - 1;
  349. var end = this.isForwardDirection() ? caret.end : caret.end - 1;
  350. var length = start === end ? 1 : end - start;
  351. this._handleChain({
  352. text: chars,
  353. start: start,
  354. length: length
  355. })
  356. },
  357. _tryMoveCaretBackward: function() {
  358. this.setBackwardDirection();
  359. var currentCaret = this._caret().start;
  360. this._adjustCaret();
  361. return !currentCaret || currentCaret !== this._caret().start
  362. },
  363. _adjustCaret: function(char) {
  364. var caret = this._maskRulesChain.adjustedCaret(this._caret().start, this.isForwardDirection(), char);
  365. this._caret({
  366. start: caret,
  367. end: caret
  368. })
  369. },
  370. _moveCaret: function() {
  371. var currentCaret = this._caret().start;
  372. var maskRuleIndex = currentCaret + (this.isForwardDirection() ? 0 : -1);
  373. var caret = this._maskRulesChain.isAccepted(maskRuleIndex) ? currentCaret + (this.isForwardDirection() ? 1 : -1) : currentCaret;
  374. this._caret({
  375. start: caret,
  376. end: caret
  377. })
  378. },
  379. _caret: function(position) {
  380. var $input = this._input();
  381. if (!$input.length) {
  382. return
  383. }
  384. if (!arguments.length) {
  385. return caret($input)
  386. }
  387. caret($input, position)
  388. },
  389. _hasSelection: function() {
  390. var caret = this._caret();
  391. return caret.start !== caret.end
  392. },
  393. _direction: function(direction) {
  394. if (!arguments.length) {
  395. return this._typingDirection
  396. }
  397. this._typingDirection = direction
  398. },
  399. setForwardDirection: function() {
  400. this._direction(FORWARD_DIRECTION)
  401. },
  402. setBackwardDirection: function() {
  403. this._direction(BACKWARD_DIRECTION)
  404. },
  405. isForwardDirection: function() {
  406. return this._direction() === FORWARD_DIRECTION
  407. },
  408. _clean: function() {
  409. this._maskStrategy && this._maskStrategy.clean();
  410. this.callBase()
  411. },
  412. _validateMask: function() {
  413. if (!this._maskRulesChain) {
  414. return
  415. }
  416. var isValid = stringUtils.isEmpty(this.option("value")) || this._maskRulesChain.isValid(this._normalizeChainArguments());
  417. this.option({
  418. isValid: isValid,
  419. validationError: isValid ? null : {
  420. editorSpecific: true,
  421. message: this.option("maskInvalidMessage")
  422. }
  423. })
  424. },
  425. _updateHiddenElement: function() {
  426. this._removeHiddenElement();
  427. if (this.option("mask")) {
  428. this._input().removeAttr("name");
  429. this._renderHiddenElement()
  430. }
  431. this._setSubmitElementName(this.option("name"))
  432. },
  433. _updateMaskOption: function() {
  434. this._updateHiddenElement();
  435. this._renderMask();
  436. this._validateMask()
  437. },
  438. _processEmptyMask: function(mask) {
  439. if (mask) {
  440. return
  441. }
  442. var value = this.option("value");
  443. this.option({
  444. text: value,
  445. isValid: true
  446. });
  447. this.validationRequest.fire({
  448. value: value,
  449. editor: this
  450. });
  451. this._renderValue()
  452. },
  453. _optionChanged: function(args) {
  454. switch (args.name) {
  455. case "mask":
  456. this._updateMaskOption();
  457. this._processEmptyMask(args.value);
  458. break;
  459. case "maskChar":
  460. case "maskRules":
  461. case "useMaskedValue":
  462. this._updateMaskOption();
  463. break;
  464. case "value":
  465. this._renderMaskedValue();
  466. this._validateMask();
  467. this.callBase(args);
  468. break;
  469. case "maskInvalidMessage":
  470. break;
  471. case "showMaskMode":
  472. this.option("text", "");
  473. this._renderValue();
  474. break;
  475. default:
  476. this.callBase(args)
  477. }
  478. }
  479. });
  480. module.exports = TextEditorMask;