src/model/encoding/convertFromRawToDraftState.js (240 lines of code) (raw):

/** * 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 {BlockMap} from 'BlockMap'; import type {BlockNodeConfig} from 'BlockNode'; import type CharacterMetadata from 'CharacterMetadata'; import type {DraftBlockType} from 'DraftBlockType'; import type {EntityRange} from 'EntityRange'; import type {InlineStyleRange} from 'InlineStyleRange'; import type {RawDraftContentBlock} from 'RawDraftContentBlock'; import type {RawDraftContentState} from 'RawDraftContentState'; const ContentBlock = require('ContentBlock'); const ContentBlockNode = require('ContentBlockNode'); const ContentState = require('ContentState'); const DraftTreeAdapter = require('DraftTreeAdapter'); const DraftTreeInvariants = require('DraftTreeInvariants'); const SelectionState = require('SelectionState'); const createCharacterList = require('createCharacterList'); const decodeEntityRanges = require('decodeEntityRanges'); const decodeInlineStyleRanges = require('decodeInlineStyleRanges'); const generateRandomKey = require('generateRandomKey'); const gkx = require('gkx'); const Immutable = require('immutable'); const invariant = require('invariant'); const experimentalTreeDataSupport = gkx('draft_tree_data_support'); const {List, Map, OrderedMap} = Immutable; type EntityKeyMap = {[key: number]: number}; const decodeBlockNodeConfig = ( block: RawDraftContentBlock, entityKeyMap: EntityKeyMap, ): BlockNodeConfig => { const {key, type, data, text, depth} = block; const blockNodeConfig: BlockNodeConfig = { text, depth: depth || 0, type: type || 'unstyled', key: key || generateRandomKey(), data: Map(data), characterList: decodeCharacterList(block, entityKeyMap), }; return blockNodeConfig; }; const decodeCharacterList = ( block: RawDraftContentBlock, entityKeyMap: EntityKeyMap, ): List<CharacterMetadata> => { const { text, entityRanges: rawEntityRanges, inlineStyleRanges: rawInlineStyleRanges, } = block; const entityRanges = rawEntityRanges || []; const inlineStyleRanges = rawInlineStyleRanges || []; // Translate entity range keys to the DraftEntity map. return createCharacterList( decodeInlineStyleRanges(text, inlineStyleRanges), decodeEntityRanges( text, entityRanges .filter(range => entityKeyMap.hasOwnProperty(range.key)) .map(range => ({...range, key: entityKeyMap[range.key]})), ), ); }; const addKeyIfMissing = (block: RawDraftContentBlock): RawDraftContentBlock => { return { ...block, key: block.key || generateRandomKey(), }; }; /** * Node stack is responsible to ensure we traverse the tree only once * in depth order, while also providing parent refs to inner nodes to * construct their links. */ const updateNodeStack = ( stack: Array< | any | { children?: Array<RawDraftContentBlock>, data?: any, depth: ?number, entityRanges: ?Array<EntityRange>, inlineStyleRanges: ?Array<InlineStyleRange>, key: ?string, parentRef: ContentBlockNode, text: string, type: DraftBlockType, ... }, >, nodes: Array<any>, parentRef: ContentBlockNode, ): Array< | any | { children?: Array<RawDraftContentBlock>, data?: any, depth: ?number, entityRanges: ?Array<EntityRange>, inlineStyleRanges: ?Array<InlineStyleRange>, key: ?string, parentRef: ContentBlockNode, text: string, type: DraftBlockType, ... }, > => { const nodesWithParentRef = nodes.map(block => { return { ...block, parentRef, }; }); // since we pop nodes from the stack we need to insert them in reverse return stack.concat(nodesWithParentRef.reverse()); }; /** * This will build a tree draft content state by creating the node * reference links into a single tree walk. Each node has a link * reference to "parent", "children", "nextSibling" and "prevSibling" * blockMap will be created using depth ordering. */ const decodeContentBlockNodes = ( blocks: Array<RawDraftContentBlock>, entityMap: EntityKeyMap, ): BlockMap => { return ( blocks // ensure children have valid keys to enable sibling links .map(addKeyIfMissing) .reduce( (blockMap: BlockMap, block: RawDraftContentBlock, index: number) => { invariant( Array.isArray(block.children), 'invalid RawDraftContentBlock can not be converted to ContentBlockNode', ); // ensure children have valid keys to enable sibling links const children = block.children.map(addKeyIfMissing); // root level nodes const contentBlockNode = new ContentBlockNode({ ...decodeBlockNodeConfig(block, entityMap), prevSibling: index === 0 ? null : blocks[index - 1].key, nextSibling: index === blocks.length - 1 ? null : blocks[index + 1].key, children: List(children.map((child: any) => child.key)), }); // push root node to blockMap blockMap = blockMap.set(contentBlockNode.getKey(), contentBlockNode); // this stack is used to ensure we visit all nodes respecting depth ordering let stack = updateNodeStack([], children, contentBlockNode); // start computing children nodes while (stack.length > 0) { // we pop from the stack and start processing this node const node: any = stack.pop(); // parentRef already points to a converted ContentBlockNode const parentRef: ContentBlockNode = node.parentRef; const siblings = parentRef.getChildKeys(); const index = siblings.indexOf(node.key); const isValidBlock = Array.isArray(node.children); if (!isValidBlock) { invariant( isValidBlock, 'invalid RawDraftContentBlock can not be converted to ContentBlockNode', ); break; } // ensure children have valid keys to enable sibling links const children = node.children.map(addKeyIfMissing); const contentBlockNode = new ContentBlockNode({ ...decodeBlockNodeConfig(node, entityMap), parent: parentRef.getKey(), children: List(children.map((child: any) => child.key)), prevSibling: index === 0 ? null : siblings.get(index - 1), nextSibling: index === siblings.size - 1 ? null : siblings.get(index + 1), }); // push node to blockMap blockMap = blockMap.set( contentBlockNode.getKey(), contentBlockNode, ); // this stack is used to ensure we visit all nodes respecting depth ordering stack = updateNodeStack(stack, children, contentBlockNode); } return blockMap; }, OrderedMap(), ) ); }; const decodeContentBlocks = ( blocks: Array<RawDraftContentBlock>, entityKeyMap: EntityKeyMap, ): BlockMap => { return OrderedMap( blocks.map((block: RawDraftContentBlock) => { const contentBlock = new ContentBlock( decodeBlockNodeConfig(block, entityKeyMap), ); return [contentBlock.getKey(), contentBlock]; }), ); }; const decodeRawBlocks = ( rawState: RawDraftContentState, entityKeyMap: EntityKeyMap, ): BlockMap => { const isTreeRawBlock = rawState.blocks.find( block => Array.isArray(block.children) && block.children.length > 0, ); const rawBlocks = experimentalTreeDataSupport && !isTreeRawBlock ? DraftTreeAdapter.fromRawStateToRawTreeState(rawState).blocks : rawState.blocks; if (!experimentalTreeDataSupport) { return decodeContentBlocks( isTreeRawBlock ? DraftTreeAdapter.fromRawTreeStateToRawState(rawState).blocks : rawBlocks, entityKeyMap, ); } const blockMap = decodeContentBlockNodes(rawBlocks, entityKeyMap); // in dev mode, check that the tree invariants are met if (__DEV__) { invariant( DraftTreeInvariants.isValidTree(blockMap), 'Should be a valid tree', ); } return blockMap; }; const decodeRawEntityMap = ( contentStateArg: ContentState, rawState: RawDraftContentState, ): {entityKeyMap: EntityKeyMap, contentState: ContentState} => { const {entityMap: rawEntityMap} = rawState; const entityKeyMap: {[string]: string} = {}; let contentState = contentStateArg; Object.keys(rawEntityMap).forEach(rawEntityKey => { const {type, mutability, data} = rawEntityMap[rawEntityKey]; contentState = contentState.createEntity(type, mutability, data || {}); // get the key reference to created entity entityKeyMap[rawEntityKey] = contentState.getLastCreatedEntityKey(); }); // $FlowFixMe[incompatible-return] return {entityKeyMap, contentState}; }; const convertFromRawToDraftState = ( rawState: RawDraftContentState, ): ContentState => { invariant(Array.isArray(rawState.blocks), 'invalid RawDraftContentState'); // decode entities const {contentState, entityKeyMap} = decodeRawEntityMap( ContentState.createFromText(''), rawState, ); // decode blockMap const blockMap = decodeRawBlocks(rawState, entityKeyMap); // create initial selection const selectionState = blockMap.isEmpty() ? new SelectionState() : SelectionState.createEmpty(blockMap.first().getKey()); return new ContentState({ blockMap, entityMap: contentState.getEntityMap(), selectionBefore: selectionState, selectionAfter: selectionState, }); }; module.exports = convertFromRawToDraftState;