From 3ee5a23ec681f6e903c4bacfe97d9d83f76bda9e Mon Sep 17 00:00:00 2001 From: Shalabh Vyas Date: Mon, 6 Apr 2020 07:38:41 -0700 Subject: [PATCH] Add fromJS() API to Draft model objects Summary: # Facebook Co-Editing Prototype is based on the approach of serializing the DraftJS state and syncing the states between users through GraphQL subscription. This is done on a node-by-node basis. Users lock the nodes they are editing and send updates to the other users when they update a node. They also receive broadcast from other users for the nodes those users updated. The above approach requires serialization of DraftJS's `EditorState` and the ability to recreate this state (and it's hierarchy of Immutable JS records like `ContentState`, `BlockMap` etc) when loading an update from other user. This diff adds a `fromJS()` API (following the ImmutableJS terminology) to all the DraftJS model objects so that they can be constructed from a plain JS object (received post de-serialization). Reviewed By: mrkev Differential Revision: D20625291 fbshipit-source-id: d3f6c028b351dc19a5c352998884869b7158a435 --- src/model/immutable/BlockNode.js | 11 ++++ src/model/immutable/BlockTree.js | 31 +++++++++-- src/model/immutable/CharacterMetadata.js | 17 ++++++ src/model/immutable/ContentState.js | 49 +++++++++++++++-- src/model/immutable/ContentStateRawType.js | 22 ++++++++ src/model/immutable/EditorState.js | 64 +++++++++++++++++++++- 6 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 src/model/immutable/ContentStateRawType.js diff --git a/src/model/immutable/BlockNode.js b/src/model/immutable/BlockNode.js index e4bc0f77f0..2d9c4966dd 100644 --- a/src/model/immutable/BlockNode.js +++ b/src/model/immutable/BlockNode.js @@ -12,12 +12,23 @@ 'use strict'; import type CharacterMetadata from 'CharacterMetadata'; +import type {CharacterMetadataRawConfig} from 'CharacterMetadata'; import type {DraftBlockType} from 'DraftBlockType'; import type {DraftInlineStyle} from 'DraftInlineStyle'; import type {List, Map} from 'immutable'; export type BlockNodeKey = string; +export type BlockNodeRawConfig = { + characterList?: Array, + data?: Map, + depth?: number, + key?: BlockNodeKey, + text?: string, + type?: DraftBlockType, + ... +}; + export type BlockNodeConfig = { characterList?: List, data?: Map, diff --git a/src/model/immutable/BlockTree.js b/src/model/immutable/BlockTree.js index c5a39701d9..ee034871de 100644 --- a/src/model/immutable/BlockTree.js +++ b/src/model/immutable/BlockTree.js @@ -17,6 +17,7 @@ import type ContentState from 'ContentState'; import type {DraftDecoratorType} from 'DraftDecoratorType'; const findRangesImmutable = require('findRangesImmutable'); +const getOwnObjectValues = require('getOwnObjectValues'); const Immutable = require('immutable'); const {List, Repeat, Record} = Immutable; @@ -34,15 +35,25 @@ const defaultLeafRange: { end: null, }; -const LeafRange = Record(defaultLeafRange); +const LeafRange = (Record(defaultLeafRange): any); -const defaultDecoratorRange: { +export type DecoratorRangeRawType = { + start: ?number, + end: ?number, + decoratorKey: ?string, + leaves: ?Array, + ... +}; + +type DecoratorRangeType = { start: ?number, end: ?number, decoratorKey: ?string, leaves: ?List, ... -} = { +}; + +const defaultDecoratorRange: DecoratorRangeType = { start: null, end: null, decoratorKey: null, @@ -55,7 +66,7 @@ const BlockTree = { /** * Generate a block tree for a given ContentBlock/decorator pair. */ - generate: function( + generate( contentState: ContentState, block: BlockNodeRecord, decorator: ?DraftDecoratorType, @@ -92,6 +103,18 @@ const BlockTree = { return List(leafSets); }, + + fromJS({leaves, ...other}: DecoratorRangeRawType): DecoratorRange { + return new DecoratorRange({ + ...other, + leaves: + leaves != null + ? List( + Array.isArray(leaves) ? leaves : getOwnObjectValues(leaves), + ).map(leaf => LeafRange(leaf)) + : null, + }); + }, }; /** diff --git a/src/model/immutable/CharacterMetadata.js b/src/model/immutable/CharacterMetadata.js index b0851241fe..d659f691ad 100644 --- a/src/model/immutable/CharacterMetadata.js +++ b/src/model/immutable/CharacterMetadata.js @@ -18,6 +18,13 @@ const {Map, OrderedSet, Record} = require('immutable'); // Immutable.map is typed such that the value for every key in the map // must be the same type type CharacterMetadataConfigValueType = DraftInlineStyle | ?string; +type CharacterMetadataConfigRawValueType = Array | ?string; + +export type CharacterMetadataRawConfig = { + style?: CharacterMetadataConfigRawValueType, + entity?: CharacterMetadataConfigRawValueType, + ... +}; type CharacterMetadataConfig = { style?: CharacterMetadataConfigValueType, @@ -102,6 +109,16 @@ class CharacterMetadata extends CharacterMetadataRecord { pool = pool.set(configMap, newCharacter); return newCharacter; } + + static fromJS({ + style, + entity, + }: CharacterMetadataRawConfig): CharacterMetadata { + return new CharacterMetadata({ + style: Array.isArray(style) ? OrderedSet(style) : style, + entity: Array.isArray(entity) ? OrderedSet(entity) : entity, + }); + } } const EMPTY = new CharacterMetadata(); diff --git a/src/model/immutable/ContentState.js b/src/model/immutable/ContentState.js index 9075b9dcd6..f75f93584f 100644 --- a/src/model/immutable/ContentState.js +++ b/src/model/immutable/ContentState.js @@ -12,7 +12,9 @@ 'use strict'; import type {BlockMap} from 'BlockMap'; +import type {BlockNodeRawConfig} from 'BlockNode'; import type {BlockNodeRecord} from 'BlockNodeRecord'; +import type {ContentStateRawType} from 'ContentStateRawType'; import type DraftEntityInstance from 'DraftEntityInstance'; import type {DraftEntityMutability} from 'DraftEntityMutability'; import type {DraftEntityType} from 'DraftEntityType'; @@ -26,19 +28,22 @@ const DraftEntity = require('DraftEntity'); const SelectionState = require('SelectionState'); const generateRandomKey = require('generateRandomKey'); +const getOwnObjectValues = require('getOwnObjectValues'); const gkx = require('gkx'); const Immutable = require('immutable'); const sanitizeDraftText = require('sanitizeDraftText'); -const {List, Record, Repeat} = Immutable; +const {List, Record, Repeat, Map: ImmutableMap, OrderedMap} = Immutable; -const defaultRecord: { +type ContentStateRecordType = { entityMap: ?any, blockMap: ?BlockMap, selectionBefore: ?SelectionState, selectionAfter: ?SelectionState, ... -} = { +}; + +const defaultRecord: ContentStateRecordType = { entityMap: null, blockMap: null, selectionBefore: null, @@ -47,6 +52,10 @@ const defaultRecord: { const ContentStateRecord = (Record(defaultRecord): any); +const ContentBlockNodeRecord = gkx('draft_tree_data_support') + ? ContentBlockNode + : ContentBlock; + class ContentState extends ContentStateRecord { getEntityMap(): any { // TODO: update this when we fully remove DraftEntity @@ -211,9 +220,6 @@ class ContentState extends ContentStateRecord { const strings = text.split(delimiter); const blocks = strings.map(block => { block = sanitizeDraftText(block); - const ContentBlockNodeRecord = gkx('draft_tree_data_support') - ? ContentBlockNode - : ContentBlock; return new ContentBlockNodeRecord({ key: generateRandomKey(), text: block, @@ -223,6 +229,37 @@ class ContentState extends ContentStateRecord { }); return ContentState.createFromBlockArray(blocks); } + + static fromJS(state: ContentStateRawType): ContentState { + return new ContentState({ + ...state, + blockMap: OrderedMap(state.blockMap).map( + ContentState._createContentBlockFromRaw, + ), + selectionBefore: new SelectionState(state.selectionBefore), + selectionAfter: new SelectionState(state.selectionAfter), + }); + } + + static _createContentBlockFromRaw( + block: BlockNodeRawConfig, + ): ContentBlockNodeRecord { + const characterList = block.characterList; + + return new ContentBlockNodeRecord({ + ...block, + data: ImmutableMap(block.data), + characterList: + characterList != null + ? List( + (Array.isArray(characterList) + ? characterList + : getOwnObjectValues(characterList) + ).map(c => CharacterMetadata.fromJS(c)), + ) + : undefined, + }); + } } module.exports = ContentState; diff --git a/src/model/immutable/ContentStateRawType.js b/src/model/immutable/ContentStateRawType.js new file mode 100644 index 0000000000..a64609eb51 --- /dev/null +++ b/src/model/immutable/ContentStateRawType.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + * @emails oncall+draft_js + */ + +'use strict'; + +import type {BlockNodeRawConfig} from 'BlockNode'; + +export type ContentStateRawType = { + entityMap: ?{...}, + blockMap: ?Map, + selectionBefore: ?{...}, + selectionAfter: ?{...}, + ... +}; diff --git a/src/model/immutable/EditorState.js b/src/model/immutable/EditorState.js index 17a66e3fe9..65d5ad26ab 100644 --- a/src/model/immutable/EditorState.js +++ b/src/model/immutable/EditorState.js @@ -12,11 +12,12 @@ 'use strict'; import type {BlockMap} from 'BlockMap'; +import type {DecoratorRangeRawType} from 'BlockTree'; +import type {ContentStateRawType} from 'ContentStateRawType'; import type {DraftDecoratorType} from 'DraftDecoratorType'; import type {DraftInlineStyle} from 'DraftInlineStyle'; import type {EditorChangeType} from 'EditorChangeType'; import type {EntityMap} from 'EntityMap'; -import type {List, OrderedMap} from 'immutable'; const BlockTree = require('BlockTree'); const ContentState = require('ContentState'); @@ -25,7 +26,7 @@ const SelectionState = require('SelectionState'); const Immutable = require('immutable'); -const {OrderedSet, Record, Stack} = Immutable; +const {OrderedSet, Record, Stack, OrderedMap, List} = Immutable; // When configuring an editor, the user can chose to provide or not provide // basically all keys. `currentContent` varies, so this type doesn't include it. @@ -45,12 +46,32 @@ type BaseEditorStateConfig = {| undoStack?: Stack, |}; +type BaseEditorStateRawConfig = {| + allowUndo?: boolean, + decorator?: ?DraftDecoratorType, + directionMap?: ?{...}, + forceSelection?: boolean, + inCompositionMode?: boolean, + inlineStyleOverride?: ?Array, + lastChangeType?: ?EditorChangeType, + nativelyRenderedContent?: ?ContentStateRawType, + redoStack?: Array, + selection?: ?{...}, + treeMap?: ?Map>, + undoStack?: Array, +|}; + // When crating an editor, we want currentContent to be set. type EditorStateCreationConfigType = {| ...BaseEditorStateConfig, currentContent: ContentState, |}; +type EditorStateCreationConfigRawType = {| + ...BaseEditorStateRawConfig, + currentContent: ContentStateRawType, +|}; + // When using EditorState.set(...), currentContent is optional type EditorStateChangeConfigType = {| ...BaseEditorStateConfig, @@ -132,6 +153,45 @@ class EditorState { return new EditorState(new EditorStateRecord(recordConfig)); } + static fromJS(config: EditorStateCreationConfigRawType): EditorState { + return new EditorState( + new EditorStateRecord({ + ...config, + directionMap: + config.directionMap != null + ? OrderedMap(config.directionMap) + : config.directionMap, + inlineStyleOverride: + config.inlineStyleOverride != null + ? OrderedSet(config.inlineStyleOverride) + : config.inlineStyleOverride, + nativelyRenderedContent: + config.nativelyRenderedContent != null + ? ContentState.fromJS(config.nativelyRenderedContent) + : config.nativelyRenderedContent, + redoStack: + config.redoStack != null + ? Stack(config.redoStack.map(v => ContentState.fromJS(v))) + : config.redoStack, + selection: + config.selection != null + ? new SelectionState(config.selection) + : config.selection, + treeMap: + config.treeMap != null + ? OrderedMap(config.treeMap).map(v => + List(v).map(v => BlockTree.fromJS(v)), + ) + : config.treeMap, + undoStack: + config.undoStack != null + ? Stack(config.undoStack.map(v => ContentState.fromJS(v))) + : config.undoStack, + currentContent: ContentState.fromJS(config.currentContent), + }), + ); + } + static set( editorState: EditorState, put: EditorStateChangeConfigType,