ui.text_editor.base.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. /**
  2. * DevExtreme (ui/text_box/ui.text_editor.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 domUtils = require("../../core/utils/dom");
  14. var focused = require("../widget/selectors").focused;
  15. var isDefined = require("../../core/utils/type").isDefined;
  16. var extend = require("../../core/utils/extend").extend;
  17. var inArray = require("../../core/utils/array").inArray;
  18. var each = require("../../core/utils/iterator").each;
  19. var themes = require("../themes");
  20. var Editor = require("../editor/editor");
  21. var eventUtils = require("../../events/utils");
  22. var pointerEvents = require("../../events/pointer");
  23. var ClearButton = require("./ui.text_editor.clear").default;
  24. var TextEditorButtonCollection = require("./texteditor_button_collection/index").default;
  25. var config = require("../../core/config");
  26. var errors = require("../widget/ui.errors");
  27. var Deferred = require("../../core/utils/deferred").Deferred;
  28. var TEXTEDITOR_CLASS = "dx-texteditor";
  29. var TEXTEDITOR_INPUT_CONTAINER_CLASS = "dx-texteditor-input-container";
  30. var TEXTEDITOR_INPUT_CLASS = "dx-texteditor-input";
  31. var TEXTEDITOR_INPUT_SELECTOR = "." + TEXTEDITOR_INPUT_CLASS;
  32. var TEXTEDITOR_CONTAINER_CLASS = "dx-texteditor-container";
  33. var TEXTEDITOR_BUTTONS_CONTAINER_CLASS = "dx-texteditor-buttons-container";
  34. var TEXTEDITOR_PLACEHOLDER_CLASS = "dx-placeholder";
  35. var TEXTEDITOR_EMPTY_INPUT_CLASS = "dx-texteditor-empty";
  36. var TEXTEDITOR_STYLING_MODE_PREFIX = "dx-editor-";
  37. var ALLOWED_STYLE_CLASSES = [TEXTEDITOR_STYLING_MODE_PREFIX + "outlined", TEXTEDITOR_STYLING_MODE_PREFIX + "filled", TEXTEDITOR_STYLING_MODE_PREFIX + "underlined"];
  38. var STATE_INVISIBLE_CLASS = "dx-state-invisible";
  39. var EVENTS_LIST = ["KeyDown", "KeyPress", "KeyUp", "Change", "Cut", "Copy", "Paste", "Input"];
  40. var CONTROL_KEYS = ["tab", "enter", "shift", "control", "alt", "escape", "pageUp", "pageDown", "end", "home", "leftArrow", "upArrow", "rightArrow", "downArrow"];
  41. function checkButtonsOptionType(buttons) {
  42. if (isDefined(buttons) && !Array.isArray(buttons)) {
  43. throw errors.Error("E1053")
  44. }
  45. }
  46. var TextEditorBase = Editor.inherit({
  47. ctor: function(_, options) {
  48. if (options) {
  49. checkButtonsOptionType(options.buttons)
  50. }
  51. this._buttonCollection = new TextEditorButtonCollection(this, this._getDefaultButtons());
  52. this._$beforeButtonsContainer = null;
  53. this._$afterButtonsContainer = null;
  54. this.callBase.apply(this, arguments)
  55. },
  56. _getDefaultOptions: function() {
  57. return extend(this.callBase(), {
  58. buttons: void 0,
  59. value: "",
  60. spellcheck: false,
  61. showClearButton: false,
  62. valueChangeEvent: "change",
  63. placeholder: "",
  64. inputAttr: {},
  65. onFocusIn: null,
  66. onFocusOut: null,
  67. onKeyDown: null,
  68. onKeyPress: null,
  69. onKeyUp: null,
  70. onChange: null,
  71. onInput: null,
  72. onCut: null,
  73. onCopy: null,
  74. onPaste: null,
  75. onEnterKey: null,
  76. mode: "text",
  77. hoverStateEnabled: true,
  78. focusStateEnabled: true,
  79. text: void 0,
  80. valueFormat: function(value) {
  81. return isDefined(value) && false !== value ? value : ""
  82. },
  83. stylingMode: config().editorStylingMode || "outlined"
  84. })
  85. },
  86. _defaultOptionsRules: function() {
  87. var themeName = themes.current();
  88. return this.callBase().concat([{
  89. device: function() {
  90. return themes.isMaterial(themeName)
  91. },
  92. options: {
  93. stylingMode: config().editorStylingMode || "underlined"
  94. }
  95. }])
  96. },
  97. _getDefaultButtons: function() {
  98. return [{
  99. name: "clear",
  100. Ctor: ClearButton
  101. }]
  102. },
  103. _isClearButtonVisible: function() {
  104. return this.option("showClearButton") && !this.option("readOnly")
  105. },
  106. _input: function() {
  107. return this.$element().find(TEXTEDITOR_INPUT_SELECTOR).first()
  108. },
  109. _isFocused: function() {
  110. return focused(this._input()) || this.callBase()
  111. },
  112. _inputWrapper: function() {
  113. return this.$element()
  114. },
  115. _buttonsContainer: function() {
  116. return this._inputWrapper().find("." + TEXTEDITOR_BUTTONS_CONTAINER_CLASS).eq(0)
  117. },
  118. _isControlKey: function(key) {
  119. return CONTROL_KEYS.indexOf(key) !== -1
  120. },
  121. _renderStylingMode: function() {
  122. var _this = this;
  123. var optionName = "stylingMode";
  124. var optionValue = this.option(optionName);
  125. ALLOWED_STYLE_CLASSES.forEach(function(className) {
  126. return _this.$element().removeClass(className)
  127. });
  128. var stylingModeClass = TEXTEDITOR_STYLING_MODE_PREFIX + optionValue;
  129. if (ALLOWED_STYLE_CLASSES.indexOf(stylingModeClass) === -1) {
  130. var defaultOptionValue = this._getDefaultOptions()[optionName];
  131. var platformOptionValue = this._convertRulesToOptions(this._defaultOptionsRules())[optionName];
  132. stylingModeClass = TEXTEDITOR_STYLING_MODE_PREFIX + (platformOptionValue || defaultOptionValue)
  133. }
  134. this.$element().addClass(stylingModeClass);
  135. this._updateButtonsStyling(optionValue)
  136. },
  137. _initMarkup: function() {
  138. this.$element().addClass(TEXTEDITOR_CLASS);
  139. this._renderInput();
  140. this._renderStylingMode();
  141. this._renderInputType();
  142. this._renderPlaceholder();
  143. this._renderProps();
  144. this.callBase();
  145. this._renderValue()
  146. },
  147. _render: function() {
  148. this._renderPlaceholder();
  149. this._refreshValueChangeEvent();
  150. this._renderEvents();
  151. this._renderEnterKeyAction();
  152. this._renderEmptinessEvent();
  153. this.callBase()
  154. },
  155. _renderInput: function() {
  156. this._$textEditorContainer = $("<div>").addClass(TEXTEDITOR_CONTAINER_CLASS).appendTo(this.$element());
  157. this._$textEditorInputContainer = $("<div>").addClass(TEXTEDITOR_INPUT_CONTAINER_CLASS).appendTo(this._$textEditorContainer);
  158. this._$textEditorInputContainer.append(this._createInput());
  159. this._renderButtonContainers()
  160. },
  161. _renderButtonContainers: function() {
  162. var buttons = this.option("buttons");
  163. this._$beforeButtonsContainer = this._buttonCollection.renderBeforeButtons(buttons, this._$textEditorContainer);
  164. this._$afterButtonsContainer = this._buttonCollection.renderAfterButtons(buttons, this._$textEditorContainer)
  165. },
  166. _clean: function() {
  167. this._buttonCollection.clean();
  168. this._$beforeButtonsContainer = null;
  169. this._$afterButtonsContainer = null;
  170. this._$textEditorContainer = null;
  171. this.callBase()
  172. },
  173. _createInput: function() {
  174. var $input = $("<input>");
  175. this._applyInputAttributes($input, this.option("inputAttr"));
  176. return $input
  177. },
  178. _setSubmitElementName: function(name) {
  179. var inputAttrName = this.option("inputAttr.name");
  180. return this.callBase(name || inputAttrName || "")
  181. },
  182. _applyInputAttributes: function($input, customAttributes) {
  183. $input.attr("autocomplete", "off").attr(customAttributes).addClass(TEXTEDITOR_INPUT_CLASS).css("minHeight", this.option("height") ? "0" : "")
  184. },
  185. _updateButtons: function(names) {
  186. this._buttonCollection.updateButtons(names)
  187. },
  188. _updateButtonsStyling: function(editorStylingMode) {
  189. var that = this;
  190. each(this.option("buttons"), function(_, buttonOptions) {
  191. if (buttonOptions.options && !buttonOptions.options.stylingMode) {
  192. var buttonInstance = that.getButton(buttonOptions.name);
  193. buttonInstance.option && buttonInstance.option("stylingMode", "underlined" === editorStylingMode ? "text" : "contained")
  194. }
  195. })
  196. },
  197. _renderValue: function() {
  198. var renderInputPromise = this._renderInputValue();
  199. return renderInputPromise.promise()
  200. },
  201. _renderInputValue: function(value) {
  202. value = value || this.option("value");
  203. var text = this.option("text");
  204. var displayValue = this.option("displayValue");
  205. var valueFormat = this.option("valueFormat");
  206. if (void 0 !== displayValue && null !== value) {
  207. text = valueFormat(displayValue)
  208. } else {
  209. if (!isDefined(text)) {
  210. text = valueFormat(value)
  211. }
  212. }
  213. this.option("text", text);
  214. if (this._input().val() !== (isDefined(text) ? text : "")) {
  215. this._renderDisplayText(text)
  216. } else {
  217. this._toggleEmptinessEventHandler()
  218. }
  219. return (new Deferred).resolve()
  220. },
  221. _renderDisplayText: function(text) {
  222. this._input().val(text);
  223. this._toggleEmptinessEventHandler()
  224. },
  225. _isValueValid: function() {
  226. if (this._input().length) {
  227. var validity = this._input().get(0).validity;
  228. if (validity) {
  229. return validity.valid
  230. }
  231. }
  232. return true
  233. },
  234. _toggleEmptiness: function(isEmpty) {
  235. this.$element().toggleClass(TEXTEDITOR_EMPTY_INPUT_CLASS, isEmpty);
  236. this._togglePlaceholder(isEmpty)
  237. },
  238. _togglePlaceholder: function(isEmpty) {
  239. if (!this._$placeholder) {
  240. return
  241. }
  242. this._$placeholder.toggleClass(STATE_INVISIBLE_CLASS, !isEmpty)
  243. },
  244. _renderProps: function() {
  245. this._toggleReadOnlyState();
  246. this._toggleSpellcheckState();
  247. this._toggleTabIndex()
  248. },
  249. _toggleDisabledState: function(value) {
  250. this.callBase.apply(this, arguments);
  251. var $input = this._input();
  252. if (value) {
  253. $input.attr("disabled", true)
  254. } else {
  255. $input.removeAttr("disabled")
  256. }
  257. },
  258. _toggleTabIndex: function() {
  259. var $input = this._input();
  260. var disabled = this.option("disabled");
  261. var focusStateEnabled = this.option("focusStateEnabled");
  262. if (disabled || !focusStateEnabled) {
  263. $input.attr("tabIndex", -1)
  264. } else {
  265. $input.removeAttr("tabIndex")
  266. }
  267. },
  268. _toggleReadOnlyState: function() {
  269. this._input().prop("readOnly", this._readOnlyPropValue());
  270. this.callBase()
  271. },
  272. _readOnlyPropValue: function() {
  273. return this.option("readOnly")
  274. },
  275. _toggleSpellcheckState: function() {
  276. this._input().prop("spellcheck", this.option("spellcheck"))
  277. },
  278. _renderPlaceholder: function() {
  279. this._renderPlaceholderMarkup();
  280. this._attachPlaceholderEvents()
  281. },
  282. _renderPlaceholderMarkup: function() {
  283. if (this._$placeholder) {
  284. this._$placeholder.remove();
  285. this._$placeholder = null
  286. }
  287. var $input = this._input();
  288. var placeholderText = this.option("placeholder");
  289. var $placeholder = this._$placeholder = $("<div>").attr("data-dx_placeholder", placeholderText);
  290. $placeholder.insertAfter($input);
  291. $placeholder.addClass(TEXTEDITOR_PLACEHOLDER_CLASS)
  292. },
  293. _attachPlaceholderEvents: function() {
  294. var that = this;
  295. var startEvent = eventUtils.addNamespace(pointerEvents.up, that.NAME);
  296. eventsEngine.on(that._$placeholder, startEvent, function() {
  297. eventsEngine.trigger(that._input(), "focus")
  298. });
  299. that._toggleEmptinessEventHandler()
  300. },
  301. _placeholder: function() {
  302. return this._$placeholder || $()
  303. },
  304. _clearValueHandler: function(e) {
  305. var $input = this._input();
  306. e.stopPropagation();
  307. this._saveValueChangeEvent(e);
  308. this._clearValue();
  309. !this._isFocused() && eventsEngine.trigger($input, "focus");
  310. eventsEngine.trigger($input, "input")
  311. },
  312. _clearValue: function() {
  313. this.reset()
  314. },
  315. _renderEvents: function() {
  316. var that = this;
  317. var $input = that._input();
  318. each(EVENTS_LIST, function(_, event) {
  319. if (that.hasActionSubscription("on" + event)) {
  320. var action = that._createActionByOption("on" + event, {
  321. excludeValidators: ["readOnly"]
  322. });
  323. eventsEngine.on($input, eventUtils.addNamespace(event.toLowerCase(), that.NAME), function(e) {
  324. if (that._disposed) {
  325. return
  326. }
  327. action({
  328. event: e
  329. })
  330. })
  331. }
  332. })
  333. },
  334. _refreshEvents: function() {
  335. var that = this;
  336. var $input = this._input();
  337. each(EVENTS_LIST, function(_, event) {
  338. eventsEngine.off($input, eventUtils.addNamespace(event.toLowerCase(), that.NAME))
  339. });
  340. this._renderEvents()
  341. },
  342. _keyPressHandler: function() {
  343. this.option("text", this._input().val())
  344. },
  345. _keyDownHandler: function(e) {
  346. var $input = this._input();
  347. var isCtrlEnter = e.ctrlKey && "enter" === eventUtils.normalizeKeyName(e);
  348. var isNewValue = $input.val() !== this.option("value");
  349. if (isCtrlEnter && isNewValue) {
  350. eventsEngine.trigger($input, "change")
  351. }
  352. },
  353. _renderValueChangeEvent: function() {
  354. var keyPressEvent = eventUtils.addNamespace(this._renderValueEventName(), "".concat(this.NAME, "TextChange"));
  355. var valueChangeEvent = eventUtils.addNamespace(this.option("valueChangeEvent"), "".concat(this.NAME, "ValueChange"));
  356. var keyDownEvent = eventUtils.addNamespace("keydown", "".concat(this.NAME, "TextChange"));
  357. var $input = this._input();
  358. eventsEngine.on($input, keyPressEvent, this._keyPressHandler.bind(this));
  359. eventsEngine.on($input, valueChangeEvent, this._valueChangeEventHandler.bind(this));
  360. eventsEngine.on($input, keyDownEvent, this._keyDownHandler.bind(this))
  361. },
  362. _cleanValueChangeEvent: function() {
  363. var valueChangeNamespace = ".".concat(this.NAME, "ValueChange");
  364. var textChangeNamespace = ".".concat(this.NAME, "TextChange");
  365. eventsEngine.off(this._input(), valueChangeNamespace);
  366. eventsEngine.off(this._input(), textChangeNamespace)
  367. },
  368. _refreshValueChangeEvent: function() {
  369. this._cleanValueChangeEvent();
  370. this._renderValueChangeEvent()
  371. },
  372. _renderValueEventName: function() {
  373. return "input change keypress"
  374. },
  375. _focusTarget: function() {
  376. return this._input()
  377. },
  378. _focusEventTarget: function() {
  379. return this.element()
  380. },
  381. _preventNestedFocusEvent: function(event) {
  382. if (event.isDefaultPrevented()) {
  383. return true
  384. }
  385. var result = this._isNestedTarget(event.relatedTarget);
  386. if ("focusin" === event.type) {
  387. result = result && this._isNestedTarget(event.target)
  388. }
  389. result && event.preventDefault();
  390. return result
  391. },
  392. _isNestedTarget: function(target) {
  393. return !!this.$element().find(target).length
  394. },
  395. _focusClassTarget: function() {
  396. return this.$element()
  397. },
  398. _focusInHandler: function(event) {
  399. this._preventNestedFocusEvent(event);
  400. this.callBase.apply(this, arguments)
  401. },
  402. _focusOutHandler: function(event) {
  403. this._preventNestedFocusEvent(event);
  404. this.callBase.apply(this, arguments)
  405. },
  406. _toggleFocusClass: function(isFocused, $element) {
  407. this.callBase(isFocused, this._focusClassTarget($element))
  408. },
  409. _hasFocusClass: function(element) {
  410. return this.callBase($(element || this.$element()))
  411. },
  412. _renderEmptinessEvent: function() {
  413. var $input = this._input();
  414. eventsEngine.on($input, "input blur", this._toggleEmptinessEventHandler.bind(this))
  415. },
  416. _toggleEmptinessEventHandler: function() {
  417. var text = this._input().val();
  418. var isEmpty = ("" === text || null === text) && this._isValueValid();
  419. this._toggleEmptiness(isEmpty)
  420. },
  421. _valueChangeEventHandler: function(e, formattedValue) {
  422. this._saveValueChangeEvent(e);
  423. this.option("value", arguments.length > 1 ? formattedValue : this._input().val());
  424. this._saveValueChangeEvent(void 0)
  425. },
  426. _renderEnterKeyAction: function() {
  427. this._enterKeyAction = this._createActionByOption("onEnterKey", {
  428. excludeValidators: ["readOnly"]
  429. });
  430. eventsEngine.off(this._input(), "keyup.onEnterKey.dxTextEditor");
  431. eventsEngine.on(this._input(), "keyup.onEnterKey.dxTextEditor", this._enterKeyHandlerUp.bind(this))
  432. },
  433. _enterKeyHandlerUp: function(e) {
  434. if (this._disposed) {
  435. return
  436. }
  437. if ("enter" === eventUtils.normalizeKeyName(e)) {
  438. this._enterKeyAction({
  439. event: e
  440. })
  441. }
  442. },
  443. _updateValue: function() {
  444. this.option("text", void 0);
  445. this._renderValue()
  446. },
  447. _dispose: function() {
  448. this._enterKeyAction = void 0;
  449. this.callBase()
  450. },
  451. _getSubmitElement: function() {
  452. return this._input()
  453. },
  454. _optionChanged: function(args) {
  455. var name = args.name;
  456. if (inArray(name.replace("on", ""), EVENTS_LIST) > -1) {
  457. this._refreshEvents();
  458. return
  459. }
  460. switch (name) {
  461. case "valueChangeEvent":
  462. this._refreshValueChangeEvent();
  463. this._refreshFocusEvent();
  464. this._refreshEvents();
  465. break;
  466. case "onValueChanged":
  467. this._createValueChangeAction();
  468. break;
  469. case "focusStateEnabled":
  470. this.callBase(args);
  471. this._toggleTabIndex();
  472. break;
  473. case "spellcheck":
  474. this._toggleSpellcheckState();
  475. break;
  476. case "mode":
  477. this._renderInputType();
  478. break;
  479. case "onEnterKey":
  480. this._renderEnterKeyAction();
  481. break;
  482. case "placeholder":
  483. this._renderPlaceholder();
  484. break;
  485. case "readOnly":
  486. case "disabled":
  487. this._updateButtons();
  488. this.callBase(args);
  489. break;
  490. case "showClearButton":
  491. this._updateButtons(["clear"]);
  492. break;
  493. case "text":
  494. break;
  495. case "value":
  496. this._updateValue();
  497. this.callBase(args);
  498. break;
  499. case "inputAttr":
  500. this._applyInputAttributes(this._input(), args.value);
  501. break;
  502. case "stylingMode":
  503. this._renderStylingMode();
  504. break;
  505. case "buttons":
  506. if (args.fullName === args.name) {
  507. checkButtonsOptionType(args.value)
  508. }
  509. this._$beforeButtonsContainer && this._$beforeButtonsContainer.remove();
  510. this._$afterButtonsContainer && this._$afterButtonsContainer.remove();
  511. this._buttonCollection.clean();
  512. this._renderButtonContainers();
  513. break;
  514. case "valueFormat":
  515. this._invalidate();
  516. break;
  517. default:
  518. this.callBase(args)
  519. }
  520. },
  521. _renderInputType: function() {
  522. this._setInputType(this.option("mode"))
  523. },
  524. _setInputType: function(type) {
  525. var input = this._input();
  526. if ("search" === type) {
  527. type = "text"
  528. }
  529. try {
  530. input.prop("type", type)
  531. } catch (e) {
  532. input.prop("type", "text")
  533. }
  534. },
  535. getButton: function(name) {
  536. return this._buttonCollection.getButton(name)
  537. },
  538. focus: function() {
  539. eventsEngine.trigger(this._input(), "focus")
  540. },
  541. blur: function() {
  542. if (this._input().is(domAdapter.getActiveElement())) {
  543. domUtils.resetActiveElement()
  544. }
  545. },
  546. reset: function() {
  547. var defaultOptions = this._getDefaultOptions();
  548. if (this.option("value") === defaultOptions.value) {
  549. this.option("text", "");
  550. this._renderValue()
  551. } else {
  552. this.option("value", defaultOptions.value)
  553. }
  554. },
  555. on: function(eventName, eventHandler) {
  556. var result = this.callBase(eventName, eventHandler);
  557. var event = eventName.charAt(0).toUpperCase() + eventName.substr(1);
  558. if (EVENTS_LIST.indexOf(event) >= 0) {
  559. this._refreshEvents()
  560. }
  561. return result
  562. }
  563. });
  564. module.exports = TextEditorBase;