From 8f4a5eee15253c49dd41a2e7cfc501bb88daa646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Tue, 10 May 2016 15:59:47 +0200 Subject: [PATCH 01/25] Add property "blockList" to ContentBlock Decode "blocks" from RawContentState Add base for tree example Add base for component DraftEditorBlocks Change blockList to blockMap in ContentBlock Extend DraftOffsetKeyPath format Switch to keep a flatten map of blocks in ContentState Adapt splitBlockInContentState to split in the same parent --- examples/tree/tree.html | 131 +++++++++++ .../contents/DraftEditorBlock.react.js | 10 + .../contents/DraftEditorBlocks.react.js | 210 ++++++++++++++++++ .../contents/DraftEditorContents.react.js | 168 ++------------ src/component/selection/DraftOffsetKey.js | 3 +- .../encoding/convertFromRawToDraftState.js | 52 +++-- src/model/immutable/ContentBlock.js | 9 + src/model/immutable/ContentState.js | 11 + src/model/immutable/EditorState.js | 4 +- .../transaction/splitBlockInContentState.js | 5 + 10 files changed, 433 insertions(+), 170 deletions(-) create mode 100644 examples/tree/tree.html create mode 100644 src/component/contents/DraftEditorBlocks.react.js diff --git a/examples/tree/tree.html b/examples/tree/tree.html new file mode 100644 index 0000000000..bb0f9e480d --- /dev/null +++ b/examples/tree/tree.html @@ -0,0 +1,131 @@ + + + + + + Draft • Tree Editor + + + + +
+ + + + + + + + + diff --git a/src/component/contents/DraftEditorBlock.react.js b/src/component/contents/DraftEditorBlock.react.js index 81d5a5c583..0e32878426 100644 --- a/src/component/contents/DraftEditorBlock.react.js +++ b/src/component/contents/DraftEditorBlock.react.js @@ -193,6 +193,15 @@ class DraftEditorBlock extends React.Component { }).toArray(); } + _renderBlockMap(): React.Element { + var {getBlockChildren, block} = this.props; + var DraftEditorBlocks = this.props.DraftEditorBlocks; + + var blocks = getBlockChildren(block.getKey()); + + return ; + } + render(): React.Element { const {direction, offsetKey} = this.props; const className = cx({ @@ -204,6 +213,7 @@ class DraftEditorBlock extends React.Component { return (
{this._renderChildren()} + {this._renderBlockMap()}
); } diff --git a/src/component/contents/DraftEditorBlocks.react.js b/src/component/contents/DraftEditorBlocks.react.js new file mode 100644 index 0000000000..9f36036673 --- /dev/null +++ b/src/component/contents/DraftEditorBlocks.react.js @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule DraftEditorContents.react + * @typechecks + * @flow + */ + +'use strict'; + +const DraftEditorBlock = require('DraftEditorBlock.react'); +const DraftOffsetKey = require('DraftOffsetKey'); +const React = require('React'); + +const cx = require('cx'); +const joinClasses = require('joinClasses'); +const nullthrows = require('nullthrows'); + +import type {BidiDirection} from 'UnicodeBidiDirection'; +import type ContentBlock from 'ContentBlock'; + +type Props = { + blockRendererFn: Function, + blockStyleFn: (block: ContentBlock) => string, + blocksAsArray: Array, +}; + +/** + * `DraftEditorContents` is the container component for all block components + * rendered for a `DraftEditor`. It is optimized to aggressively avoid + * re-rendering blocks whenever possible. + * + * This component is separate from `DraftEditor` because certain props + * (for instance, ARIA props) must be allowed to update without affecting + * the contents of the editor. + */ +class DraftEditorBlocks extends React.Component { + render(): React.Element { + const { + blockRenderMap, + blockRendererFn, + blockStyleFn, + customStyleMap, + blocksAsArray, + selection, + forceSelection, + decorator, + directionMap, + getBlockTree, + getBlockChildren + } = this.props; + + const blocks = []; + let currentWrapperElement = null; + let currentWrapperTemplate = null; + let currentDepth = null; + let currentWrappedBlocks; + let block, key, blockType, child, childProps, wrapperTemplate; + + for (let ii = 0; ii < blocksAsArray.length; ii++) { + block = blocksAsArray[ii]; + key = block.getKey(); + blockType = block.getType(); + + const customRenderer = blockRendererFn(block); + let CustomComponent, customProps, customEditable; + if (customRenderer) { + CustomComponent = customRenderer.component; + customProps = customRenderer.props; + customEditable = customRenderer.editable; + } + + const direction = directionMap.get(key); + const offsetKey = DraftOffsetKey.encode(key, 0, 0); + const componentProps = { + block, + blockProps: customProps, + customStyleMap, + decorator, + direction, + directionMap, + forceSelection, + key, + offsetKey, + selection, + blockRenderMap, + blockRendererFn, + blockStyleFn, + blocksAsArray, + getBlockTree, + getBlockChildren, + DraftEditorBlocks: DraftEditorBlocks, + tree: getBlockTree(key) + }; + + // Block render map must have a configuration specified for this + // block type. + const configForType = nullthrows(blockRenderMap.get(blockType)); + + wrapperTemplate = configForType.wrapper; + + const useNewWrapper = wrapperTemplate !== currentWrapperTemplate; + + const Element = ( + blockRenderMap.get(blockType).element || + blockRenderMap.get('unstyled').element + ); + + const depth = block.getDepth(); + let className = blockStyleFn(block); + + // List items are special snowflakes, since we handle nesting and + // counters manually. + if (Element === 'li') { + const shouldResetCount = ( + useNewWrapper || + currentDepth === null || + depth > currentDepth + ); + className = joinClasses( + className, + getListItemClasses(blockType, depth, shouldResetCount, direction) + ); + } + + const Component = CustomComponent || DraftEditorBlock; + childProps = { + className, + 'data-block': true, + 'data-editor': this.props.editorKey, + 'data-offset-key': offsetKey, + key, + }; + if (customEditable !== undefined) { + childProps = { + ...childProps, + contentEditable: customEditable, + suppressContentEditableWarning: true, + }; + } + + child = React.createElement( + Element, + childProps, + , + ); + + if (wrapperTemplate) { + if (useNewWrapper) { + currentWrappedBlocks = []; + currentWrapperElement = React.cloneElement( + wrapperTemplate, + { + key: key + '-wrap', + 'data-offset-key': offsetKey, + }, + currentWrappedBlocks + ); + currentWrapperTemplate = wrapperTemplate; + blocks.push(currentWrapperElement); + } + currentDepth = block.getDepth(); + nullthrows(currentWrappedBlocks).push(child); + } else { + currentWrappedBlocks = null; + currentWrapperElement = null; + currentWrapperTemplate = null; + currentDepth = null; + blocks.push(child); + } + } + + return
{blocks}
; + } +} + +/** + * Provide default styling for list items. This way, lists will be styled with + * proper counters and indentation even if the caller does not specify + * their own styling at all. If more than five levels of nesting are needed, + * the necessary CSS classes can be provided via `blockStyleFn` configuration. + */ +function getListItemClasses( + type: string, + depth: number, + shouldResetCount: boolean, + direction: BidiDirection +): string { + return cx({ + 'public/DraftStyleDefault/unorderedListItem': + type === 'unordered-list-item', + 'public/DraftStyleDefault/orderedListItem': + type === 'ordered-list-item', + 'public/DraftStyleDefault/reset': shouldResetCount, + 'public/DraftStyleDefault/depth0': depth === 0, + 'public/DraftStyleDefault/depth1': depth === 1, + 'public/DraftStyleDefault/depth2': depth === 2, + 'public/DraftStyleDefault/depth3': depth === 3, + 'public/DraftStyleDefault/depth4': depth === 4, + 'public/DraftStyleDefault/listLTR': direction === 'LTR', + 'public/DraftStyleDefault/listRTL': direction === 'RTL', + }); +} + +module.exports = DraftEditorBlocks; diff --git a/src/component/contents/DraftEditorContents.react.js b/src/component/contents/DraftEditorContents.react.js index a630dae779..ee63a07d18 100644 --- a/src/component/contents/DraftEditorContents.react.js +++ b/src/component/contents/DraftEditorContents.react.js @@ -13,16 +13,11 @@ 'use strict'; -const DraftEditorBlock = require('DraftEditorBlock.react'); -const DraftOffsetKey = require('DraftOffsetKey'); +const DraftEditorBlocks = require('DraftEditorBlocks.react'); const EditorState = require('EditorState'); const React = require('React'); - -const cx = require('cx'); -const joinClasses = require('joinClasses'); const nullthrows = require('nullthrows'); -import type {BidiDirection} from 'UnicodeBidiDirection'; import type ContentBlock from 'ContentBlock'; type Props = { @@ -94,6 +89,7 @@ class DraftEditorContents extends React.Component { const { blockRenderMap, blockRendererFn, + blockStyleFn, customStyleMap, editorState, } = this.props; @@ -103,150 +99,24 @@ class DraftEditorContents extends React.Component { const forceSelection = editorState.mustForceSelection(); const decorator = editorState.getDecorator(); const directionMap = nullthrows(editorState.getDirectionMap()); - - const blocksAsArray = content.getBlocksAsArray(); - const blocks = []; - let currentWrapperElement = null; - let currentWrapperTemplate = null; - let currentDepth = null; - let currentWrappedBlocks; - let block, key, blockType, child, childProps, wrapperTemplate; - - for (let ii = 0; ii < blocksAsArray.length; ii++) { - block = blocksAsArray[ii]; - key = block.getKey(); - blockType = block.getType(); - - const customRenderer = blockRendererFn(block); - let CustomComponent, customProps, customEditable; - if (customRenderer) { - CustomComponent = customRenderer.component; - customProps = customRenderer.props; - customEditable = customRenderer.editable; - } - - const direction = directionMap.get(key); - const offsetKey = DraftOffsetKey.encode(key, 0, 0); - const componentProps = { - block, - blockProps: customProps, - customStyleMap, - decorator, - direction, - forceSelection, - key, - offsetKey, - selection, - tree: editorState.getBlockTree(key), - }; - - // Block render map must have a configuration specified for this - // block type. - const configForType = nullthrows(blockRenderMap.get(blockType)); - - wrapperTemplate = configForType.wrapper; - - const useNewWrapper = wrapperTemplate !== currentWrapperTemplate; - - const Element = ( - blockRenderMap.get(blockType).element || - blockRenderMap.get('unstyled').element - ); - - const depth = block.getDepth(); - let className = this.props.blockStyleFn(block); - - // List items are special snowflakes, since we handle nesting and - // counters manually. - if (Element === 'li') { - const shouldResetCount = ( - useNewWrapper || - currentDepth === null || - depth > currentDepth - ); - className = joinClasses( - className, - getListItemClasses(blockType, depth, shouldResetCount, direction) - ); - } - - const Component = CustomComponent || DraftEditorBlock; - childProps = { - className, - 'data-block': true, - 'data-editor': this.props.editorKey, - 'data-offset-key': offsetKey, - key, - }; - if (customEditable !== undefined) { - childProps = { - ...childProps, - contentEditable: customEditable, - suppressContentEditableWarning: true, - }; - } - - child = React.createElement( - Element, - childProps, - , - ); - - if (wrapperTemplate) { - if (useNewWrapper) { - currentWrappedBlocks = []; - currentWrapperElement = React.cloneElement( - wrapperTemplate, - { - key: key + '-wrap', - 'data-offset-key': offsetKey, - }, - currentWrappedBlocks - ); - currentWrapperTemplate = wrapperTemplate; - blocks.push(currentWrapperElement); - } - currentDepth = block.getDepth(); - nullthrows(currentWrappedBlocks).push(child); - } else { - currentWrappedBlocks = null; - currentWrapperElement = null; - currentWrapperTemplate = null; - currentDepth = null; - blocks.push(child); - } - } - - return
{blocks}
; + const blocksAsArray = content.getFirstLevelBlocks().toArray(); + + return
+ +
; } } -/** - * Provide default styling for list items. This way, lists will be styled with - * proper counters and indentation even if the caller does not specify - * their own styling at all. If more than five levels of nesting are needed, - * the necessary CSS classes can be provided via `blockStyleFn` configuration. - */ -function getListItemClasses( - type: string, - depth: number, - shouldResetCount: boolean, - direction: BidiDirection -): string { - return cx({ - 'public/DraftStyleDefault/unorderedListItem': - type === 'unordered-list-item', - 'public/DraftStyleDefault/orderedListItem': - type === 'ordered-list-item', - 'public/DraftStyleDefault/reset': shouldResetCount, - 'public/DraftStyleDefault/depth0': depth === 0, - 'public/DraftStyleDefault/depth1': depth === 1, - 'public/DraftStyleDefault/depth2': depth === 2, - 'public/DraftStyleDefault/depth3': depth === 3, - 'public/DraftStyleDefault/depth4': depth === 4, - 'public/DraftStyleDefault/listLTR': direction === 'LTR', - 'public/DraftStyleDefault/listRTL': direction === 'RTL', - }); -} - module.exports = DraftEditorContents; diff --git a/src/component/selection/DraftOffsetKey.js b/src/component/selection/DraftOffsetKey.js index 626c683c02..c5a14dfde6 100644 --- a/src/component/selection/DraftOffsetKey.js +++ b/src/component/selection/DraftOffsetKey.js @@ -27,10 +27,11 @@ var DraftOffsetKey = { decode: function(offsetKey: string): DraftOffsetKeyPath { var [blockKey, decoratorKey, leafKey] = offsetKey.split(KEY_DELIMITER); + return { blockKey, decoratorKey: parseInt(decoratorKey, 10), - leafKey: parseInt(leafKey, 10), + leafKey: parseInt(leafKey, 10) }; }, }; diff --git a/src/model/encoding/convertFromRawToDraftState.js b/src/model/encoding/convertFromRawToDraftState.js index a48141929c..ff9e8cf45f 100644 --- a/src/model/encoding/convertFromRawToDraftState.js +++ b/src/model/encoding/convertFromRawToDraftState.js @@ -23,28 +23,21 @@ var generateRandomKey = require('generateRandomKey'); import type {RawDraftContentState} from 'RawDraftContentState'; -function convertFromRawToDraftState( - rawState: RawDraftContentState -): ContentState { - var {blocks, entityMap} = rawState; - - var fromStorageToLocal = {}; - Object.keys(entityMap).forEach( - storageKey => { - var encodedEntity = entityMap[storageKey]; - var {type, mutability, data} = encodedEntity; - var newKey = DraftEntity.create(type, mutability, data || {}); - fromStorageToLocal[storageKey] = newKey; - } - ); - - var contentBlocks = blocks.map( - block => { - var {key, type, text, depth, inlineStyleRanges, entityRanges} = block; +function convertBlocksFromRaw( + inputBlocks: Array, + fromStorageToLocal: Object, + parentKey: ?string +) : Array { + return inputBlocks.reduce( + (result, block) => { + var {key, type, text, depth, inlineStyleRanges, entityRanges, blocks} = block; key = key || generateRandomKey(); depth = depth || 0; inlineStyleRanges = inlineStyleRanges || []; entityRanges = entityRanges || []; + blocks = blocks || []; + + key = (parentKey || '') + key; var inlineStyles = decodeInlineStyleRanges(text, inlineStyleRanges); @@ -58,10 +51,31 @@ function convertFromRawToDraftState( var entities = decodeEntityRanges(text, filteredEntityRanges); var characterList = createCharacterList(inlineStyles, entities); - return new ContentBlock({key, type, text, depth, characterList}); + result = result.concat(convertBlocksFromRaw(blocks, fromStorageToLocal, key + '/')); + result.push(new ContentBlock({key, type, text, depth, characterList})); + + return result; + }, [] + ); +} + +function convertFromRawToDraftState( + rawState: RawDraftContentState +): ContentState { + var {blocks, entityMap} = rawState; + + var fromStorageToLocal = {}; + Object.keys(entityMap).forEach( + storageKey => { + var encodedEntity = entityMap[storageKey]; + var {type, mutability, data} = encodedEntity; + var newKey = DraftEntity.create(type, mutability, data || {}); + fromStorageToLocal[storageKey] = newKey; } ); + var contentBlocks = convertBlocksFromRaw(blocks, fromStorageToLocal); + return ContentState.createFromBlockArray(contentBlocks); } diff --git a/src/model/immutable/ContentBlock.js b/src/model/immutable/ContentBlock.js index 1303597c54..8fb02b37da 100644 --- a/src/model/immutable/ContentBlock.js +++ b/src/model/immutable/ContentBlock.js @@ -20,10 +20,12 @@ var findRangesImmutable = require('findRangesImmutable'); import type CharacterMetadata from 'CharacterMetadata'; import type {DraftBlockType} from 'DraftBlockType'; import type {DraftInlineStyle} from 'DraftInlineStyle'; +import type {BlockMap} from 'BlockMap'; var { List, OrderedSet, + OrderedMap, Record, } = Immutable; @@ -70,6 +72,13 @@ class ContentBlock extends ContentBlockRecord { return this.get('depth'); } + getParentKey(): string { + var key = this.getKey(); + var parts = key.split('/'); + + return parts.slice(0, -1).join('/'); + } + getInlineStyleAt(offset: number): DraftInlineStyle { var character = this.getCharacterList().get(offset); return character ? character.getStyle() : EMPTY_SET; diff --git a/src/model/immutable/ContentState.js b/src/model/immutable/ContentState.js index eb8faf897d..dd71ce34c9 100644 --- a/src/model/immutable/ContentState.js +++ b/src/model/immutable/ContentState.js @@ -56,6 +56,17 @@ class ContentState extends ContentStateRecord { return block; } + getFirstLevelBlocks(): BlockMap { + return this.getBlockChildren(''); + } + + getBlockChildren(key: string): BlockMap { + return this.getBlockMap() + .filter(function(block) { + return block.getParentKey() === key; + }); + } + getKeyBefore(key: string): ?string { return this.getBlockMap() .reverse() diff --git a/src/model/immutable/EditorState.js b/src/model/immutable/EditorState.js index 3934b5af50..a87587dab2 100644 --- a/src/model/immutable/EditorState.js +++ b/src/model/immutable/EditorState.js @@ -14,6 +14,7 @@ var BlockTree = require('BlockTree'); var ContentState = require('ContentState'); +var ContentBlock = require('ContentBlock'); var EditorBidiService = require('EditorBidiService'); var Immutable = require('immutable'); var SelectionState = require('SelectionState'); @@ -21,11 +22,12 @@ var SelectionState = require('SelectionState'); import type {BlockMap} from 'BlockMap'; import type {DraftDecoratorType} from 'DraftDecoratorType'; import type {DraftInlineStyle} from 'DraftInlineStyle'; -import type {List, OrderedMap} from 'immutable'; +import type {List} from 'immutable'; import type {EditorChangeType} from 'EditorChangeType'; var { OrderedSet, + OrderedMap, Record, Stack, } = Immutable; diff --git a/src/model/transaction/splitBlockInContentState.js b/src/model/transaction/splitBlockInContentState.js index a2619c540b..fd4f8eea28 100644 --- a/src/model/transaction/splitBlockInContentState.js +++ b/src/model/transaction/splitBlockInContentState.js @@ -32,6 +32,7 @@ function splitBlockInContentState( var offset = selectionState.getAnchorOffset(); var blockMap = contentState.getBlockMap(); var blockToSplit = blockMap.get(key); + var parentKey = blockToSplit.getParentKey(); var text = blockToSplit.getText(); var chars = blockToSplit.getCharacterList(); @@ -42,6 +43,10 @@ function splitBlockInContentState( }); var keyBelow = generateRandomKey(); + if (parentKey) { + keyBelow = parentKey + '/' + keyBelow; + } + var blockBelow = blockAbove.merge({ key: keyBelow, text: text.slice(offset), From 9a091c1f221c13211b3d6872a67f34fff8ad7b60 Mon Sep 17 00:00:00 2001 From: miter Date: Fri, 13 May 2016 13:42:06 +1000 Subject: [PATCH 02/25] Fixing commoner providesModule definition Allowing nested blocks component to update blocks that have nesting Adding rich text integration to the tree demo Added tables to the example so that we can prove the tree mapping - included basic table to prove we can edit inside table, also integrating blockRenderMap --- examples/tree/tree.css | 75 +++++ examples/tree/tree.html | 296 ++++++++++++++++-- .../contents/DraftEditorBlock.react.js | 4 + .../contents/DraftEditorBlocks.react.js | 2 +- .../encoding/convertFromRawToDraftState.js | 2 + 5 files changed, 358 insertions(+), 21 deletions(-) create mode 100644 examples/tree/tree.css diff --git a/examples/tree/tree.css b/examples/tree/tree.css new file mode 100644 index 0000000000..fcc45b9dc7 --- /dev/null +++ b/examples/tree/tree.css @@ -0,0 +1,75 @@ +.RichEditor-root { + background: #fff; + border: 1px solid #ddd; + font-family: 'Georgia', serif; + font-size: 14px; + padding: 15px; +} + +.RichEditor-editor { + border-top: 1px solid #ddd; + cursor: text; + font-size: 16px; + margin-top: 10px; +} + +.RichEditor-editor .public-DraftEditorPlaceholder-root, +.RichEditor-editor .public-DraftEditor-content { + margin: 0 -15px -15px; + padding: 15px; +} + +.RichEditor-editor .public-DraftEditor-content { + min-height: 100px; +} + +.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root { + display: none; +} + +.RichEditor-editor .RichEditor-blockquote { + border-left: 5px solid #eee; + color: #666; + font-family: 'Hoefler Text', 'Georgia', serif; + font-style: italic; + margin: 16px 0; + padding: 10px 20px; +} + +.RichEditor-editor .public-DraftStyleDefault-pre { + background-color: rgba(0, 0, 0, 0.05); + font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; + font-size: 16px; + padding: 20px; +} + +.RichEditor-controls { + font-family: 'Helvetica', sans-serif; + font-size: 14px; + margin-bottom: 5px; + user-select: none; +} + +.RichEditor-styleButton { + color: #999; + cursor: pointer; + margin-right: 16px; + padding: 2px 0; + display: inline-block; +} + +.RichEditor-activeButton { + color: #5890ff; +} + +.RichEditor-editor table { + border: 1px solid #ccc; +} + +.RichEditor-editor tr { + padding: 0; + margin: 0; +} +.RichEditor-editor td { + border: 1px solid black; +} diff --git a/examples/tree/tree.html b/examples/tree/tree.html index bb0f9e480d..d7e5d02ed2 100644 --- a/examples/tree/tree.html +++ b/examples/tree/tree.html @@ -17,7 +17,9 @@ Draft • Tree Editor +
diff --git a/src/model/immutable/ContentState.js b/src/model/immutable/ContentState.js index cb92b7e591..a970c7040f 100644 --- a/src/model/immutable/ContentState.js +++ b/src/model/immutable/ContentState.js @@ -8,7 +8,8 @@ * * @providesModule ContentState * @typechecks - * @flow */ + * @flow + */ 'use strict'; const BlockMapBuilder = require('BlockMapBuilder'); @@ -54,7 +55,6 @@ class ContentState extends ContentStateRecord { return block; } - // return all blocks getFirstLevelBlocks(): BlockMap { return this.getBlockChildren(''); } From b952567056bbe0c9eedca0a2a447feaabde3d908 Mon Sep 17 00:00:00 2001 From: miter Date: Wed, 15 Jun 2016 17:22:35 +1000 Subject: [PATCH 20/25] Nesting pasting bugs --- src/model/encoding/convertFromHTMLToContentBlocks.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/model/encoding/convertFromHTMLToContentBlocks.js b/src/model/encoding/convertFromHTMLToContentBlocks.js index 86813340ea..b4ae697464 100644 --- a/src/model/encoding/convertFromHTMLToContentBlocks.js +++ b/src/model/encoding/convertFromHTMLToContentBlocks.js @@ -408,8 +408,14 @@ function genFragment( // if we are on an invalid block we can re-use the key since it wont generate a block isValidBlock = blockTags.indexOf(nodeName) !== -1; + var insideANestableBlock = ( + chunk.keys.indexOf(blockKey) !== -1 && + lastBlock && blockRenderMap.get(lastBlock) && + blockRenderMap.get(lastBlock).nestingEnabled + ); + var chunkKey = ( - blockKey ? + blockKey && (hasNestingEnabled || insideANestableBlock) ? ( isValidBlock ? generateNestedKey(blockKey) : From 4f73295dc7e5e01d61a4529803a6c0e0d8707dfa Mon Sep 17 00:00:00 2001 From: miter Date: Thu, 16 Jun 2016 17:01:19 +1000 Subject: [PATCH 21/25] Using immutable blockMap instead of an array --- src/component/contents/DraftEditorBlock.react.js | 4 ++-- src/component/contents/DraftEditorBlocks.react.js | 9 ++++----- src/component/contents/DraftEditorContents.react.js | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/component/contents/DraftEditorBlock.react.js b/src/component/contents/DraftEditorBlock.react.js index 71db56a853..ba0029a632 100644 --- a/src/component/contents/DraftEditorBlock.react.js +++ b/src/component/contents/DraftEditorBlock.react.js @@ -214,7 +214,7 @@ class DraftEditorBlock extends React.Component { blocks: BlockMap ): React.Element { var DraftEditorBlocks = this.props.DraftEditorBlocks; - return ; + return ; } render(): React.Element { @@ -225,7 +225,7 @@ class DraftEditorBlock extends React.Component { 'public/DraftStyleDefault/rtl': direction === 'RTL', }); - const nestedBlocks = getBlockChildren ? getBlockChildren(block.getKey()) : []; + const nestedBlocks = getBlockChildren(block.getKey()); // Render nested blocks or text but never both at the same time. return ( diff --git a/src/component/contents/DraftEditorBlocks.react.js b/src/component/contents/DraftEditorBlocks.react.js index 8d33f353a3..4b59b02edf 100644 --- a/src/component/contents/DraftEditorBlocks.react.js +++ b/src/component/contents/DraftEditorBlocks.react.js @@ -40,7 +40,7 @@ class DraftEditorBlocks extends React.Component { blockRendererFn, blockStyleFn, customStyleMap, - blocksAsArray, + blockMap, selection, forceSelection, decorator, @@ -56,8 +56,7 @@ class DraftEditorBlocks extends React.Component { let currentWrappedBlocks; let block, key, blockType, child, childProps, wrapperTemplate; - for (let ii = 0; ii < blocksAsArray.length; ii++) { - block = blocksAsArray[ii]; + blockMap.forEach((block) => { key = block.getKey(); blockType = block.getType(); @@ -85,7 +84,7 @@ class DraftEditorBlocks extends React.Component { blockRenderMap, blockRendererFn, blockStyleFn, - blocksAsArray, + blockMap, getBlockTree, getBlockChildren, DraftEditorBlocks: DraftEditorBlocks, @@ -167,7 +166,7 @@ class DraftEditorBlocks extends React.Component { currentDepth = null; blocks.push(child); } - } + }); const dataContents = type === 'contents' ? true : null; const dataBlocks = dataContents ? null : true; diff --git a/src/component/contents/DraftEditorContents.react.js b/src/component/contents/DraftEditorContents.react.js index fd863495e5..a6e0df0d53 100644 --- a/src/component/contents/DraftEditorContents.react.js +++ b/src/component/contents/DraftEditorContents.react.js @@ -99,7 +99,7 @@ class DraftEditorContents extends React.Component { const forceSelection = editorState.mustForceSelection(); const decorator = editorState.getDecorator(); const directionMap = nullthrows(editorState.getDirectionMap()); - const blocksAsArray = content.getFirstLevelBlocks().toArray(); + const blockMap = content.getFirstLevelBlocks(); return Date: Fri, 17 Jun 2016 15:08:06 +1000 Subject: [PATCH 22/25] Performance improvements - making sure we do not compute the blockChildren unless needed --- .../contents/DraftEditorBlock.react.js | 19 +++++--------- .../contents/DraftEditorBlocks.react.js | 12 ++++++--- .../contents/DraftEditorContents.react.js | 3 +++ .../convertFromHTMLToContentBlocks.js | 1 + src/model/immutable/ContentState.js | 26 +++++++++++++++++++ src/model/modifier/NestedTextEditorUtil.js | 1 - 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/component/contents/DraftEditorBlock.react.js b/src/component/contents/DraftEditorBlock.react.js index ba0029a632..188178faa4 100644 --- a/src/component/contents/DraftEditorBlock.react.js +++ b/src/component/contents/DraftEditorBlock.react.js @@ -61,18 +61,15 @@ class DraftEditorBlock extends React.Component { const { block, direction, - getBlockChildren, + blockMap, tree } = this.props; - const nestedBlocks = ( - getBlockChildren ? - getBlockChildren(block.getKey()) : - false + const hasNestedBlocks = ( + blockMap && blockMap.size > 0 || + nextProps.blockMap && nextProps.blockMap.size > 0 ); - const hasNestedBlocks = nestedBlocks && nestedBlocks.size; - return ( hasNestedBlocks || block !== nextProps.block || @@ -218,20 +215,18 @@ class DraftEditorBlock extends React.Component { } render(): React.Element { - const {direction, offsetKey, getBlockChildren, block} = this.props; + const {direction, offsetKey, blockMap} = this.props; const className = cx({ 'public/DraftStyleDefault/block': true, 'public/DraftStyleDefault/ltr': direction === 'LTR', 'public/DraftStyleDefault/rtl': direction === 'RTL', }); - const nestedBlocks = getBlockChildren(block.getKey()); - // Render nested blocks or text but never both at the same time. return (
- {nestedBlocks && nestedBlocks.size && nestedBlocks.size > 0 ? - this._renderBlockMap(nestedBlocks) : + {blockMap && blockMap.size && blockMap.size > 0 ? + this._renderBlockMap(blockMap) : this._renderChildren() }
diff --git a/src/component/contents/DraftEditorBlocks.react.js b/src/component/contents/DraftEditorBlocks.react.js index 4b59b02edf..b35a6d446d 100644 --- a/src/component/contents/DraftEditorBlocks.react.js +++ b/src/component/contents/DraftEditorBlocks.react.js @@ -41,12 +41,14 @@ class DraftEditorBlocks extends React.Component { blockStyleFn, customStyleMap, blockMap, + blockMapTree, selection, forceSelection, decorator, directionMap, getBlockTree, - getBlockChildren + getBlockChildren, + getBlockDescendants } = this.props; const blocks = []; @@ -54,7 +56,7 @@ class DraftEditorBlocks extends React.Component { let currentWrapperTemplate = null; let currentDepth = null; let currentWrappedBlocks; - let block, key, blockType, child, childProps, wrapperTemplate; + let key, blockType, child, childProps, wrapperTemplate; blockMap.forEach((block) => { key = block.getKey(); @@ -70,6 +72,8 @@ class DraftEditorBlocks extends React.Component { const direction = directionMap.get(key); const offsetKey = DraftOffsetKey.encode(key, 0, 0); + const blockChildren = blockMapTree.get(key); + const componentProps = { block, blockProps: customProps, @@ -84,9 +88,11 @@ class DraftEditorBlocks extends React.Component { blockRenderMap, blockRendererFn, blockStyleFn, - blockMap, + blockMapTree, + blockMap: blockChildren, getBlockTree, getBlockChildren, + getBlockDescendants, DraftEditorBlocks: DraftEditorBlocks, tree: getBlockTree(key) }; diff --git a/src/component/contents/DraftEditorContents.react.js b/src/component/contents/DraftEditorContents.react.js index a6e0df0d53..7afef0b06e 100644 --- a/src/component/contents/DraftEditorContents.react.js +++ b/src/component/contents/DraftEditorContents.react.js @@ -100,6 +100,7 @@ class DraftEditorContents extends React.Component { const decorator = editorState.getDecorator(); const directionMap = nullthrows(editorState.getDirectionMap()); const blockMap = content.getFirstLevelBlocks(); + const blockMapTree = content.getBlockDescendants(); return ; } } diff --git a/src/model/encoding/convertFromHTMLToContentBlocks.js b/src/model/encoding/convertFromHTMLToContentBlocks.js index b4ae697464..8ba7b29c63 100644 --- a/src/model/encoding/convertFromHTMLToContentBlocks.js +++ b/src/model/encoding/convertFromHTMLToContentBlocks.js @@ -409,6 +409,7 @@ function genFragment( isValidBlock = blockTags.indexOf(nodeName) !== -1; var insideANestableBlock = ( + blockKey && chunk.keys.indexOf(blockKey) !== -1 && lastBlock && blockRenderMap.get(lastBlock) && blockRenderMap.get(lastBlock).nestingEnabled diff --git a/src/model/immutable/ContentState.js b/src/model/immutable/ContentState.js index a970c7040f..af36ec446f 100644 --- a/src/model/immutable/ContentState.js +++ b/src/model/immutable/ContentState.js @@ -59,6 +59,32 @@ class ContentState extends ContentStateRecord { return this.getBlockChildren(''); } + getBlockDescendants(): Object { + return this.getBlockMap() + .reverse() + .reduce((treeMap, block) => { + const key = block.getKey(); + const parentKey = block.getParentKey(); + const blockList = treeMap.set(key, treeMap.get(key) || new Immutable.OrderedMap()); + + if (parentKey) { + const parentList = blockList.set(parentKey, blockList.get(parentKey) || new Immutable.OrderedMap()); + const addBlockToParentList = parentList.setIn([parentKey, key], block); + + const mergedParent = ( + addBlockToParentList.get(key).size ? + addBlockToParentList.mergeIn(parentKey, addBlockToParentList.get(key)) : + addBlockToParentList + ); + + return mergedParent; + } + + return blockList; + }, new Immutable.OrderedMap()) + .map(blockMap => blockMap.reverse()); + } + getBlockChildren(key: string): BlockMap { return this.getBlockMap() .filter(function(block) { diff --git a/src/model/modifier/NestedTextEditorUtil.js b/src/model/modifier/NestedTextEditorUtil.js index b0ab5ae4c2..a0109d6d33 100644 --- a/src/model/modifier/NestedTextEditorUtil.js +++ b/src/model/modifier/NestedTextEditorUtil.js @@ -18,7 +18,6 @@ const EditorState = require('EditorState'); const Immutable = require('immutable'); const generateNestedKey = require('generateNestedKey'); const generateRandomKey = require('generateRandomKey'); -const splitBlockInContentState = require('splitBlockInContentState'); const splitBlockWithNestingInContentState = require('splitBlockWithNestingInContentState'); import type { From ab30436e52f5fa4a52a817cba2874939e5629088 Mon Sep 17 00:00:00 2001 From: miter Date: Sat, 18 Jun 2016 23:49:04 +1000 Subject: [PATCH 23/25] Improving rendering performance algorithm for nested trees --- .../contents/DraftEditorBlock.react.js | 12 ++-- .../contents/DraftEditorBlocks.react.js | 2 +- .../__tests__/DraftEditorBlock.react-test.js | 1 + src/model/immutable/ContentState.js | 57 ++++++++++++++----- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/component/contents/DraftEditorBlock.react.js b/src/component/contents/DraftEditorBlock.react.js index 188178faa4..3f8a78b66e 100644 --- a/src/component/contents/DraftEditorBlock.react.js +++ b/src/component/contents/DraftEditorBlock.react.js @@ -39,6 +39,7 @@ const SCROLL_BUFFER = 10; type Props = { block: ContentBlock, + blockMapTree: Object, customStyleMap: Object, tree: List, selection: SelectionState, @@ -61,17 +62,14 @@ class DraftEditorBlock extends React.Component { const { block, direction, - blockMap, - tree + blockMapTree, + tree, } = this.props; - const hasNestedBlocks = ( - blockMap && blockMap.size > 0 || - nextProps.blockMap && nextProps.blockMap.size > 0 - ); + const key = block.getKey(); return ( - hasNestedBlocks || + blockMapTree.getIn([key, 'childrenBlocks']) !== nextProps.blockMapTree.getIn([key, 'childrenBlocks']) || block !== nextProps.block || tree !== nextProps.tree || direction !== nextProps.direction || diff --git a/src/component/contents/DraftEditorBlocks.react.js b/src/component/contents/DraftEditorBlocks.react.js index b35a6d446d..0cd7dd61db 100644 --- a/src/component/contents/DraftEditorBlocks.react.js +++ b/src/component/contents/DraftEditorBlocks.react.js @@ -72,7 +72,7 @@ class DraftEditorBlocks extends React.Component { const direction = directionMap.get(key); const offsetKey = DraftOffsetKey.encode(key, 0, 0); - const blockChildren = blockMapTree.get(key); + const blockChildren = blockMapTree.getIn([key, 'firstLevelBlocks']); const componentProps = { block, diff --git a/src/component/contents/__tests__/DraftEditorBlock.react-test.js b/src/component/contents/__tests__/DraftEditorBlock.react-test.js index 367688cef6..fe04e47212 100644 --- a/src/component/contents/__tests__/DraftEditorBlock.react-test.js +++ b/src/component/contents/__tests__/DraftEditorBlock.react-test.js @@ -99,6 +99,7 @@ function getSelection() { function getProps(block, decorator) { return { + blockMapTree: new Immutable.Map(), block, tree: BlockTree.generate(block, decorator), selection: getSelection(), diff --git a/src/model/immutable/ContentState.js b/src/model/immutable/ContentState.js index af36ec446f..af4a47a31d 100644 --- a/src/model/immutable/ContentState.js +++ b/src/model/immutable/ContentState.js @@ -59,30 +59,61 @@ class ContentState extends ContentStateRecord { return this.getBlockChildren(''); } - getBlockDescendants(): Object { + /* + * This algorithm is used to create the blockMap nesting as well as to + * enhance performance checks for nested blocks allowing each block to + * know when any of it's children has changed. + */ + getBlockDescendants() { return this.getBlockMap() .reverse() .reduce((treeMap, block) => { const key = block.getKey(); const parentKey = block.getParentKey(); - const blockList = treeMap.set(key, treeMap.get(key) || new Immutable.OrderedMap()); + + // create one if does not exist + const blockList = ( + treeMap.get(key) ? + treeMap : + treeMap.set(key, new Immutable.Map({ + firstLevelBlocks: new Immutable.OrderedMap(), + childrenBlocks: new Immutable.Set() + })) + ); if (parentKey) { - const parentList = blockList.set(parentKey, blockList.get(parentKey) || new Immutable.OrderedMap()); - const addBlockToParentList = parentList.setIn([parentKey, key], block); + // create one if does not exist + const parentList = ( + blockList.get(parentKey) ? + blockList : + blockList.set(parentKey, new Immutable.Map({ + firstLevelBlocks: new Immutable.OrderedMap(), + childrenBlocks: new Immutable.Set() + })) + ); - const mergedParent = ( - addBlockToParentList.get(key).size ? - addBlockToParentList.mergeIn(parentKey, addBlockToParentList.get(key)) : - addBlockToParentList + // add current block to parent children list + const addBlockToParentList = parentList.setIn([parentKey, 'firstLevelBlocks', key], block); + const addGrandChildren = addBlockToParentList.setIn( + [parentKey, 'childrenBlocks'], + addBlockToParentList.getIn([parentKey, 'childrenBlocks']) + .add( + // we include all the current block children and itself + addBlockToParentList.getIn([key, 'firstLevelBlocks']).set(key, block) + ) ); - return mergedParent; - } + return addGrandChildren; + } else { + // Since the iteration is done backwards, we either have no children or + // we already have all children's defined, we should now revert back the order + // since we are doing a reversed loop + const currentBlock = blockList.getIn([key, 'firstLevelBlocks']); - return blockList; - }, new Immutable.OrderedMap()) - .map(blockMap => blockMap.reverse()); + // we reverse it back since we will use this to create our blocks + return blockList.setIn([key, 'firstLevelBlocks'], currentBlock.reverse()); + } + }, new Immutable.Map()); } getBlockChildren(key: string): BlockMap { From c6fd4a2435d481267a1f013c341de7e37cba89d1 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 18 Jun 2016 22:44:33 +0200 Subject: [PATCH 24/25] Fix convertFromRawToDraftState generating random keys for child blocks even key is defined --- src/model/encoding/convertFromRawToDraftState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/encoding/convertFromRawToDraftState.js b/src/model/encoding/convertFromRawToDraftState.js index 44672ea6d3..5d2f64d6c9 100644 --- a/src/model/encoding/convertFromRawToDraftState.js +++ b/src/model/encoding/convertFromRawToDraftState.js @@ -58,7 +58,7 @@ function convertBlocksFromRaw( key = parentKey && parentBlockRenderingConfig && parentBlockRenderingConfig.nestingEnabled ? - generateNestedKey(parentKey) : + generateNestedKey(parentKey, key) : key; var inlineStyles = decodeInlineStyleRanges(text, inlineStyleRanges); From 150adb8242dbd33f3b1f14677722ab0c614ed39d Mon Sep 17 00:00:00 2001 From: miter Date: Sun, 19 Jun 2016 12:33:30 +1000 Subject: [PATCH 25/25] Reducing iterations on the blockMap --- .../contents/DraftEditorContents.react.js | 2 +- src/model/immutable/ContentState.js | 34 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/component/contents/DraftEditorContents.react.js b/src/component/contents/DraftEditorContents.react.js index 7afef0b06e..fff8de8a11 100644 --- a/src/component/contents/DraftEditorContents.react.js +++ b/src/component/contents/DraftEditorContents.react.js @@ -99,8 +99,8 @@ class DraftEditorContents extends React.Component { const forceSelection = editorState.mustForceSelection(); const decorator = editorState.getDecorator(); const directionMap = nullthrows(editorState.getDirectionMap()); - const blockMap = content.getFirstLevelBlocks(); const blockMapTree = content.getBlockDescendants(); + const blockMap = blockMapTree.getIn(['__ROOT__', 'firstLevelBlocks']); return { const key = block.getKey(); const parentKey = block.getParentKey(); + const rootKey = '__ROOT__'; // create one if does not exist const blockList = ( @@ -99,21 +100,38 @@ class ContentState extends ContentStateRecord { addBlockToParentList.getIn([parentKey, 'childrenBlocks']) .add( // we include all the current block children and itself - addBlockToParentList.getIn([key, 'firstLevelBlocks']).set(key, block) + addBlockToParentList.getIn([key, 'childrenBlocks']).add(block) ) ); return addGrandChildren; } else { - // Since the iteration is done backwards, we either have no children or - // we already have all children's defined, we should now revert back the order - // since we are doing a reversed loop - const currentBlock = blockList.getIn([key, 'firstLevelBlocks']); + // we are root level block + // lets create a new key called firstLevelBlocks + const rootLevelBlocks = ( + blockList.get(rootKey) ? + blockList : + blockList.set(rootKey, new Immutable.Map({ + firstLevelBlocks: new Immutable.OrderedMap(), + childrenBlocks: new Immutable.Set() + })) + ); + + const rootFirstLevelBlocks = rootLevelBlocks.setIn([rootKey, 'firstLevelBlocks', key], block); + + const addToRootChildren = rootFirstLevelBlocks.setIn( + [rootKey, 'childrenBlocks'], + rootFirstLevelBlocks.getIn([rootKey, 'childrenBlocks']) + .add( + // we include all the current block children and itself + rootFirstLevelBlocks.getIn([key, 'childrenBlocks']).add(block) + ) + ); - // we reverse it back since we will use this to create our blocks - return blockList.setIn([key, 'firstLevelBlocks'], currentBlock.reverse()); + return addToRootChildren; } - }, new Immutable.Map()); + }, new Immutable.Map()) + .map((block) => block.set('firstLevelBlocks', block.get('firstLevelBlocks').reverse())); } getBlockChildren(key: string): BlockMap {