Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

589 wiersze
19 KiB

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