Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Commit

Permalink
Add fromJS() API to Draft model objects
Browse files Browse the repository at this point in the history
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
  • Loading branch information
shalabhvyas authored and facebook-github-bot committed Apr 6, 2020
1 parent 13989e3 commit 3ee5a23
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 12 deletions.
11 changes: 11 additions & 0 deletions src/model/immutable/BlockNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<CharacterMetadataRawConfig>,
data?: Map<any, any>,
depth?: number,
key?: BlockNodeKey,
text?: string,
type?: DraftBlockType,
...
};

export type BlockNodeConfig = {
characterList?: List<CharacterMetadata>,
data?: Map<any, any>,
Expand Down
31 changes: 27 additions & 4 deletions src/model/immutable/BlockTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<LeafRange>,
...
};

type DecoratorRangeType = {
start: ?number,
end: ?number,
decoratorKey: ?string,
leaves: ?List<LeafRange>,
...
} = {
};

const defaultDecoratorRange: DecoratorRangeType = {
start: null,
end: null,
decoratorKey: null,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
});
},
};

/**
Expand Down
17 changes: 17 additions & 0 deletions src/model/immutable/CharacterMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -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> | ?string;

export type CharacterMetadataRawConfig = {
style?: CharacterMetadataConfigRawValueType,
entity?: CharacterMetadataConfigRawValueType,
...
};

type CharacterMetadataConfig = {
style?: CharacterMetadataConfigValueType,
Expand Down Expand Up @@ -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();
Expand Down
49 changes: 43 additions & 6 deletions src/model/immutable/ContentState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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;
22 changes: 22 additions & 0 deletions src/model/immutable/ContentStateRawType.js
Original file line number Diff line number Diff line change
@@ -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<string, BlockNodeRawConfig>,
selectionBefore: ?{...},
selectionAfter: ?{...},
...
};
64 changes: 62 additions & 2 deletions src/model/immutable/EditorState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
Expand All @@ -45,12 +46,32 @@ type BaseEditorStateConfig = {|
undoStack?: Stack<ContentState>,
|};

type BaseEditorStateRawConfig = {|
allowUndo?: boolean,
decorator?: ?DraftDecoratorType,
directionMap?: ?{...},
forceSelection?: boolean,
inCompositionMode?: boolean,
inlineStyleOverride?: ?Array<String>,
lastChangeType?: ?EditorChangeType,
nativelyRenderedContent?: ?ContentStateRawType,
redoStack?: Array<ContentStateRawType>,
selection?: ?{...},
treeMap?: ?Map<string, Array<DecoratorRangeRawType>>,
undoStack?: Array<ContentStateRawType>,
|};

// 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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 3ee5a23

Please sign in to comment.