Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

EditorState.js 19 KiB

há 3 anos
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. *
  9. * @emails oncall+draft_js
  10. */
  11. 'use strict';
  12. function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
  13. function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
  14. var BlockTree = require("./BlockTree");
  15. var ContentState = require("./ContentState");
  16. var EditorBidiService = require("./EditorBidiService");
  17. var SelectionState = require("./SelectionState");
  18. var Immutable = require("immutable");
  19. var OrderedSet = Immutable.OrderedSet,
  20. Record = Immutable.Record,
  21. Stack = Immutable.Stack; // When configuring an editor, the user can chose to provide or not provide
  22. // basically all keys. `currentContent` varies, so this type doesn't include it.
  23. // (See the types defined below.)
  24. var defaultRecord = {
  25. allowUndo: true,
  26. currentContent: null,
  27. decorator: null,
  28. directionMap: null,
  29. forceSelection: false,
  30. inCompositionMode: false,
  31. inlineStyleOverride: null,
  32. lastChangeType: null,
  33. nativelyRenderedContent: null,
  34. redoStack: Stack(),
  35. selection: null,
  36. treeMap: null,
  37. undoStack: Stack()
  38. };
  39. var EditorStateRecord = Record(defaultRecord);
  40. var EditorState =
  41. /*#__PURE__*/
  42. function () {
  43. EditorState.createEmpty = function createEmpty(decorator) {
  44. return EditorState.createWithContent(ContentState.createFromText(''), decorator);
  45. };
  46. EditorState.createWithContent = function createWithContent(contentState, decorator) {
  47. if (contentState.getBlockMap().count() === 0) {
  48. return EditorState.createEmpty(decorator);
  49. }
  50. var firstKey = contentState.getBlockMap().first().getKey();
  51. return EditorState.create({
  52. currentContent: contentState,
  53. undoStack: Stack(),
  54. redoStack: Stack(),
  55. decorator: decorator || null,
  56. selection: SelectionState.createEmpty(firstKey)
  57. });
  58. };
  59. EditorState.create = function create(config) {
  60. var currentContent = config.currentContent,
  61. decorator = config.decorator;
  62. var recordConfig = _objectSpread({}, config, {
  63. treeMap: generateNewTreeMap(currentContent, decorator),
  64. directionMap: EditorBidiService.getDirectionMap(currentContent)
  65. });
  66. return new EditorState(new EditorStateRecord(recordConfig));
  67. };
  68. EditorState.set = function set(editorState, put) {
  69. var map = editorState.getImmutable().withMutations(function (state) {
  70. var existingDecorator = state.get('decorator');
  71. var decorator = existingDecorator;
  72. if (put.decorator === null) {
  73. decorator = null;
  74. } else if (put.decorator) {
  75. decorator = put.decorator;
  76. }
  77. var newContent = put.currentContent || editorState.getCurrentContent();
  78. if (decorator !== existingDecorator) {
  79. var treeMap = state.get('treeMap');
  80. var newTreeMap;
  81. if (decorator && existingDecorator) {
  82. newTreeMap = regenerateTreeForNewDecorator(newContent, newContent.getBlockMap(), treeMap, decorator, existingDecorator);
  83. } else {
  84. newTreeMap = generateNewTreeMap(newContent, decorator);
  85. }
  86. state.merge({
  87. decorator: decorator,
  88. treeMap: newTreeMap,
  89. nativelyRenderedContent: null
  90. });
  91. return;
  92. }
  93. var existingContent = editorState.getCurrentContent();
  94. if (newContent !== existingContent) {
  95. state.set('treeMap', regenerateTreeForNewBlocks(editorState, newContent.getBlockMap(), newContent.getEntityMap(), decorator));
  96. }
  97. state.merge(put);
  98. });
  99. return new EditorState(map);
  100. };
  101. var _proto = EditorState.prototype;
  102. _proto.toJS = function toJS() {
  103. return this.getImmutable().toJS();
  104. };
  105. _proto.getAllowUndo = function getAllowUndo() {
  106. return this.getImmutable().get('allowUndo');
  107. };
  108. _proto.getCurrentContent = function getCurrentContent() {
  109. return this.getImmutable().get('currentContent');
  110. };
  111. _proto.getUndoStack = function getUndoStack() {
  112. return this.getImmutable().get('undoStack');
  113. };
  114. _proto.getRedoStack = function getRedoStack() {
  115. return this.getImmutable().get('redoStack');
  116. };
  117. _proto.getSelection = function getSelection() {
  118. return this.getImmutable().get('selection');
  119. };
  120. _proto.getDecorator = function getDecorator() {
  121. return this.getImmutable().get('decorator');
  122. };
  123. _proto.isInCompositionMode = function isInCompositionMode() {
  124. return this.getImmutable().get('inCompositionMode');
  125. };
  126. _proto.mustForceSelection = function mustForceSelection() {
  127. return this.getImmutable().get('forceSelection');
  128. };
  129. _proto.getNativelyRenderedContent = function getNativelyRenderedContent() {
  130. return this.getImmutable().get('nativelyRenderedContent');
  131. };
  132. _proto.getLastChangeType = function getLastChangeType() {
  133. return this.getImmutable().get('lastChangeType');
  134. }
  135. /**
  136. * While editing, the user may apply inline style commands with a collapsed
  137. * cursor, intending to type text that adopts the specified style. In this
  138. * case, we track the specified style as an "override" that takes precedence
  139. * over the inline style of the text adjacent to the cursor.
  140. *
  141. * If null, there is no override in place.
  142. */
  143. ;
  144. _proto.getInlineStyleOverride = function getInlineStyleOverride() {
  145. return this.getImmutable().get('inlineStyleOverride');
  146. };
  147. EditorState.setInlineStyleOverride = function setInlineStyleOverride(editorState, inlineStyleOverride) {
  148. return EditorState.set(editorState, {
  149. inlineStyleOverride: inlineStyleOverride
  150. });
  151. }
  152. /**
  153. * Get the appropriate inline style for the editor state. If an
  154. * override is in place, use it. Otherwise, the current style is
  155. * based on the location of the selection state.
  156. */
  157. ;
  158. _proto.getCurrentInlineStyle = function getCurrentInlineStyle() {
  159. var override = this.getInlineStyleOverride();
  160. if (override != null) {
  161. return override;
  162. }
  163. var content = this.getCurrentContent();
  164. var selection = this.getSelection();
  165. if (selection.isCollapsed()) {
  166. return getInlineStyleForCollapsedSelection(content, selection);
  167. }
  168. return getInlineStyleForNonCollapsedSelection(content, selection);
  169. };
  170. _proto.getBlockTree = function getBlockTree(blockKey) {
  171. return this.getImmutable().getIn(['treeMap', blockKey]);
  172. };
  173. _proto.isSelectionAtStartOfContent = function isSelectionAtStartOfContent() {
  174. var firstKey = this.getCurrentContent().getBlockMap().first().getKey();
  175. return this.getSelection().hasEdgeWithin(firstKey, 0, 0);
  176. };
  177. _proto.isSelectionAtEndOfContent = function isSelectionAtEndOfContent() {
  178. var content = this.getCurrentContent();
  179. var blockMap = content.getBlockMap();
  180. var last = blockMap.last();
  181. var end = last.getLength();
  182. return this.getSelection().hasEdgeWithin(last.getKey(), end, end);
  183. };
  184. _proto.getDirectionMap = function getDirectionMap() {
  185. return this.getImmutable().get('directionMap');
  186. }
  187. /**
  188. * Incorporate native DOM selection changes into the EditorState. This
  189. * method can be used when we simply want to accept whatever the DOM
  190. * has given us to represent selection, and we do not need to re-render
  191. * the editor.
  192. *
  193. * To forcibly move the DOM selection, see `EditorState.forceSelection`.
  194. */
  195. ;
  196. EditorState.acceptSelection = function acceptSelection(editorState, selection) {
  197. return updateSelection(editorState, selection, false);
  198. }
  199. /**
  200. * At times, we need to force the DOM selection to be where we
  201. * need it to be. This can occur when the anchor or focus nodes
  202. * are non-text nodes, for instance. In this case, we want to trigger
  203. * a re-render of the editor, which in turn forces selection into
  204. * the correct place in the DOM. The `forceSelection` method
  205. * accomplishes this.
  206. *
  207. * This method should be used in cases where you need to explicitly
  208. * move the DOM selection from one place to another without a change
  209. * in ContentState.
  210. */
  211. ;
  212. EditorState.forceSelection = function forceSelection(editorState, selection) {
  213. if (!selection.getHasFocus()) {
  214. selection = selection.set('hasFocus', true);
  215. }
  216. return updateSelection(editorState, selection, true);
  217. }
  218. /**
  219. * Move selection to the end of the editor without forcing focus.
  220. */
  221. ;
  222. EditorState.moveSelectionToEnd = function moveSelectionToEnd(editorState) {
  223. var content = editorState.getCurrentContent();
  224. var lastBlock = content.getLastBlock();
  225. var lastKey = lastBlock.getKey();
  226. var length = lastBlock.getLength();
  227. return EditorState.acceptSelection(editorState, new SelectionState({
  228. anchorKey: lastKey,
  229. anchorOffset: length,
  230. focusKey: lastKey,
  231. focusOffset: length,
  232. isBackward: false
  233. }));
  234. }
  235. /**
  236. * Force focus to the end of the editor. This is useful in scenarios
  237. * where we want to programmatically focus the input and it makes sense
  238. * to allow the user to continue working seamlessly.
  239. */
  240. ;
  241. EditorState.moveFocusToEnd = function moveFocusToEnd(editorState) {
  242. var afterSelectionMove = EditorState.moveSelectionToEnd(editorState);
  243. return EditorState.forceSelection(afterSelectionMove, afterSelectionMove.getSelection());
  244. }
  245. /**
  246. * Push the current ContentState onto the undo stack if it should be
  247. * considered a boundary state, and set the provided ContentState as the
  248. * new current content.
  249. */
  250. ;
  251. EditorState.push = function push(editorState, contentState, changeType) {
  252. var forceSelection = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
  253. if (editorState.getCurrentContent() === contentState) {
  254. return editorState;
  255. }
  256. var directionMap = EditorBidiService.getDirectionMap(contentState, editorState.getDirectionMap());
  257. if (!editorState.getAllowUndo()) {
  258. return EditorState.set(editorState, {
  259. currentContent: contentState,
  260. directionMap: directionMap,
  261. lastChangeType: changeType,
  262. selection: contentState.getSelectionAfter(),
  263. forceSelection: forceSelection,
  264. inlineStyleOverride: null
  265. });
  266. }
  267. var selection = editorState.getSelection();
  268. var currentContent = editorState.getCurrentContent();
  269. var undoStack = editorState.getUndoStack();
  270. var newContent = contentState;
  271. if (selection !== currentContent.getSelectionAfter() || mustBecomeBoundary(editorState, changeType)) {
  272. undoStack = undoStack.push(currentContent);
  273. newContent = newContent.set('selectionBefore', selection);
  274. } else if (changeType === 'insert-characters' || changeType === 'backspace-character' || changeType === 'delete-character') {
  275. // Preserve the previous selection.
  276. newContent = newContent.set('selectionBefore', currentContent.getSelectionBefore());
  277. }
  278. var inlineStyleOverride = editorState.getInlineStyleOverride(); // Don't discard inline style overrides for the following change types:
  279. var overrideChangeTypes = ['adjust-depth', 'change-block-type', 'split-block'];
  280. if (overrideChangeTypes.indexOf(changeType) === -1) {
  281. inlineStyleOverride = null;
  282. }
  283. var editorStateChanges = {
  284. currentContent: newContent,
  285. directionMap: directionMap,
  286. undoStack: undoStack,
  287. redoStack: Stack(),
  288. lastChangeType: changeType,
  289. selection: contentState.getSelectionAfter(),
  290. forceSelection: forceSelection,
  291. inlineStyleOverride: inlineStyleOverride
  292. };
  293. return EditorState.set(editorState, editorStateChanges);
  294. }
  295. /**
  296. * Make the top ContentState in the undo stack the new current content and
  297. * push the current content onto the redo stack.
  298. */
  299. ;
  300. EditorState.undo = function undo(editorState) {
  301. if (!editorState.getAllowUndo()) {
  302. return editorState;
  303. }
  304. var undoStack = editorState.getUndoStack();
  305. var newCurrentContent = undoStack.peek();
  306. if (!newCurrentContent) {
  307. return editorState;
  308. }
  309. var currentContent = editorState.getCurrentContent();
  310. var directionMap = EditorBidiService.getDirectionMap(newCurrentContent, editorState.getDirectionMap());
  311. return EditorState.set(editorState, {
  312. currentContent: newCurrentContent,
  313. directionMap: directionMap,
  314. undoStack: undoStack.shift(),
  315. redoStack: editorState.getRedoStack().push(currentContent),
  316. forceSelection: true,
  317. inlineStyleOverride: null,
  318. lastChangeType: 'undo',
  319. nativelyRenderedContent: null,
  320. selection: currentContent.getSelectionBefore()
  321. });
  322. }
  323. /**
  324. * Make the top ContentState in the redo stack the new current content and
  325. * push the current content onto the undo stack.
  326. */
  327. ;
  328. EditorState.redo = function redo(editorState) {
  329. if (!editorState.getAllowUndo()) {
  330. return editorState;
  331. }
  332. var redoStack = editorState.getRedoStack();
  333. var newCurrentContent = redoStack.peek();
  334. if (!newCurrentContent) {
  335. return editorState;
  336. }
  337. var currentContent = editorState.getCurrentContent();
  338. var directionMap = EditorBidiService.getDirectionMap(newCurrentContent, editorState.getDirectionMap());
  339. return EditorState.set(editorState, {
  340. currentContent: newCurrentContent,
  341. directionMap: directionMap,
  342. undoStack: editorState.getUndoStack().push(currentContent),
  343. redoStack: redoStack.shift(),
  344. forceSelection: true,
  345. inlineStyleOverride: null,
  346. lastChangeType: 'redo',
  347. nativelyRenderedContent: null,
  348. selection: newCurrentContent.getSelectionAfter()
  349. });
  350. }
  351. /**
  352. * Not for public consumption.
  353. */
  354. ;
  355. function EditorState(immutable) {
  356. _defineProperty(this, "_immutable", void 0);
  357. this._immutable = immutable;
  358. }
  359. /**
  360. * Not for public consumption.
  361. */
  362. _proto.getImmutable = function getImmutable() {
  363. return this._immutable;
  364. };
  365. return EditorState;
  366. }();
  367. /**
  368. * Set the supplied SelectionState as the new current selection, and set
  369. * the `force` flag to trigger manual selection placement by the view.
  370. */
  371. function updateSelection(editorState, selection, forceSelection) {
  372. return EditorState.set(editorState, {
  373. selection: selection,
  374. forceSelection: forceSelection,
  375. nativelyRenderedContent: null,
  376. inlineStyleOverride: null
  377. });
  378. }
  379. /**
  380. * Regenerate the entire tree map for a given ContentState and decorator.
  381. * Returns an OrderedMap that maps all available ContentBlock objects.
  382. */
  383. function generateNewTreeMap(contentState, decorator) {
  384. return contentState.getBlockMap().map(function (block) {
  385. return BlockTree.generate(contentState, block, decorator);
  386. }).toOrderedMap();
  387. }
  388. /**
  389. * Regenerate tree map objects for all ContentBlocks that have changed
  390. * between the current editorState and newContent. Returns an OrderedMap
  391. * with only changed regenerated tree map objects.
  392. */
  393. function regenerateTreeForNewBlocks(editorState, newBlockMap, newEntityMap, decorator) {
  394. var contentState = editorState.getCurrentContent().set('entityMap', newEntityMap);
  395. var prevBlockMap = contentState.getBlockMap();
  396. var prevTreeMap = editorState.getImmutable().get('treeMap');
  397. return prevTreeMap.merge(newBlockMap.toSeq().filter(function (block, key) {
  398. return block !== prevBlockMap.get(key);
  399. }).map(function (block) {
  400. return BlockTree.generate(contentState, block, decorator);
  401. }));
  402. }
  403. /**
  404. * Generate tree map objects for a new decorator object, preserving any
  405. * decorations that are unchanged from the previous decorator.
  406. *
  407. * Note that in order for this to perform optimally, decoration Lists for
  408. * decorators should be preserved when possible to allow for direct immutable
  409. * List comparison.
  410. */
  411. function regenerateTreeForNewDecorator(content, blockMap, previousTreeMap, decorator, existingDecorator) {
  412. return previousTreeMap.merge(blockMap.toSeq().filter(function (block) {
  413. return decorator.getDecorations(block, content) !== existingDecorator.getDecorations(block, content);
  414. }).map(function (block) {
  415. return BlockTree.generate(content, block, decorator);
  416. }));
  417. }
  418. /**
  419. * Return whether a change should be considered a boundary state, given
  420. * the previous change type. Allows us to discard potential boundary states
  421. * during standard typing or deletion behavior.
  422. */
  423. function mustBecomeBoundary(editorState, changeType) {
  424. var lastChangeType = editorState.getLastChangeType();
  425. return changeType !== lastChangeType || changeType !== 'insert-characters' && changeType !== 'backspace-character' && changeType !== 'delete-character';
  426. }
  427. function getInlineStyleForCollapsedSelection(content, selection) {
  428. var startKey = selection.getStartKey();
  429. var startOffset = selection.getStartOffset();
  430. var startBlock = content.getBlockForKey(startKey); // If the cursor is not at the start of the block, look backward to
  431. // preserve the style of the preceding character.
  432. if (startOffset > 0) {
  433. return startBlock.getInlineStyleAt(startOffset - 1);
  434. } // The caret is at position zero in this block. If the block has any
  435. // text at all, use the style of the first character.
  436. if (startBlock.getLength()) {
  437. return startBlock.getInlineStyleAt(0);
  438. } // Otherwise, look upward in the document to find the closest character.
  439. return lookUpwardForInlineStyle(content, startKey);
  440. }
  441. function getInlineStyleForNonCollapsedSelection(content, selection) {
  442. var startKey = selection.getStartKey();
  443. var startOffset = selection.getStartOffset();
  444. var startBlock = content.getBlockForKey(startKey); // If there is a character just inside the selection, use its style.
  445. if (startOffset < startBlock.getLength()) {
  446. return startBlock.getInlineStyleAt(startOffset);
  447. } // Check if the selection at the end of a non-empty block. Use the last
  448. // style in the block.
  449. if (startOffset > 0) {
  450. return startBlock.getInlineStyleAt(startOffset - 1);
  451. } // Otherwise, look upward in the document to find the closest character.
  452. return lookUpwardForInlineStyle(content, startKey);
  453. }
  454. function lookUpwardForInlineStyle(content, fromKey) {
  455. var lastNonEmpty = content.getBlockMap().reverse().skipUntil(function (_, k) {
  456. return k === fromKey;
  457. }).skip(1).skipUntil(function (block, _) {
  458. return block.getLength();
  459. }).first();
  460. if (lastNonEmpty) {
  461. return lastNonEmpty.getInlineStyleAt(lastNonEmpty.getLength() - 1);
  462. }
  463. return OrderedSet();
  464. }
  465. module.exports = EditorState;