UNPKG

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