diff --git a/src/component/contents/DraftEditorBlock.react.js b/src/component/contents/DraftEditorBlock.react.js index 102ba14384..2c62b8df54 100644 --- a/src/component/contents/DraftEditorBlock.react.js +++ b/src/component/contents/DraftEditorBlock.react.js @@ -16,6 +16,7 @@ import type {BlockNodeRecord} from 'BlockNodeRecord'; import type ContentState from 'ContentState'; import type {DraftDecoratorType} from 'DraftDecoratorType'; +import type {DraftInlineStyle} from 'DraftInlineStyle'; import type SelectionState from 'SelectionState'; import type {BidiDirection} from 'UnicodeBidiDirection'; import type {List} from 'immutable'; @@ -39,18 +40,29 @@ const nullthrows = require('nullthrows'); const SCROLL_BUFFER = 10; type Props = { - contentState: ContentState, block: BlockNodeRecord, + blockProps?: Object, + blockStyleFn: (block: BlockNodeRecord) => string, + contentState: ContentState, + customStyleFn: (style: DraftInlineStyle, block: BlockNodeRecord) => ?Object, customStyleMap: Object, - customStyleFn: Function, - tree: List, - selection: SelectionState, - decorator: DraftDecoratorType, - forceSelection: boolean, + decorator: ?DraftDecoratorType, direction: BidiDirection, - blockProps?: Object, + forceSelection: boolean, + offsetKey: string, + selection: SelectionState, startIndent?: boolean, - blockStyleFn: Function, + tree: List, +}; + +/** + * Return whether a block overlaps with either edge of the `SelectionState`. + */ +const isBlockOnSelectionEdge = ( + selection: SelectionState, + key: string, +): boolean => { + return selection.getAnchorKey() === key || selection.getFocusKey() === key; }; /** @@ -83,21 +95,21 @@ class DraftEditorBlock extends React.Component { * scroll parent. */ componentDidMount(): void { - var selection = this.props.selection; - var endKey = selection.getEndKey(); + const selection = this.props.selection; + const endKey = selection.getEndKey(); if (!selection.getHasFocus() || endKey !== this.props.block.getKey()) { return; } - var blockNode = ReactDOM.findDOMNode(this); - var scrollParent = Style.getScrollParent(blockNode); - var scrollPosition = getScrollPosition(scrollParent); - var scrollDelta; + const blockNode = ReactDOM.findDOMNode(this); + const scrollParent = Style.getScrollParent(blockNode); + const scrollPosition = getScrollPosition(scrollParent); + let scrollDelta; if (scrollParent === window) { - var nodePosition = getElementPosition(blockNode); - var nodeBottom = nodePosition.y + nodePosition.height; - var viewportHeight = getViewportDimensions().height; + const nodePosition = getElementPosition(blockNode); + const nodeBottom = nodePosition.y + nodePosition.height; + const viewportHeight = getViewportDimensions().height; scrollDelta = nodeBottom - viewportHeight; if (scrollDelta > 0) { window.scrollTo( @@ -110,8 +122,8 @@ class DraftEditorBlock extends React.Component { blockNode instanceof HTMLElement, 'blockNode is not an HTMLElement', ); - var blockBottom = blockNode.offsetHeight + blockNode.offsetTop; - var scrollBottom = scrollParent.offsetHeight + scrollPosition.y; + const blockBottom = blockNode.offsetHeight + blockNode.offsetTop; + const scrollBottom = scrollParent.offsetHeight + scrollPosition.y; scrollDelta = blockBottom - scrollBottom; if (scrollDelta > 0) { Scroll.setTop( @@ -123,32 +135,28 @@ class DraftEditorBlock extends React.Component { } _renderChildren(): Array> { - var block = this.props.block; - var blockKey = block.getKey(); - var text = block.getText(); - var lastLeafSet = this.props.tree.size - 1; - var hasSelection = isBlockOnSelectionEdge(this.props.selection, blockKey); + const block = this.props.block; + const blockKey = block.getKey(); + const text = block.getText(); + const lastLeafSet = this.props.tree.size - 1; + const hasSelection = isBlockOnSelectionEdge(this.props.selection, blockKey); return this.props.tree .map((leafSet, ii) => { - var leavesForLeafSet = leafSet.get('leaves'); - var lastLeaf = leavesForLeafSet.size - 1; - var leaves = leavesForLeafSet + const leavesForLeafSet = leafSet.get('leaves'); + const lastLeaf = leavesForLeafSet.size - 1; + const leaves = leavesForLeafSet .map((leaf, jj) => { - var offsetKey = DraftOffsetKey.encode(blockKey, ii, jj); - var start = leaf.get('start'); - var end = leaf.get('end'); + const offsetKey = DraftOffsetKey.encode(blockKey, ii, jj); + const start = leaf.get('start'); + const end = leaf.get('end'); return ( - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an - * error when upgrading Flow's support for React. Common errors found - * when upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ { }) .toArray(); - var decoratorKey = leafSet.get('decoratorKey'); + const decoratorKey = leafSet.get('decoratorKey'); if (decoratorKey == null) { return leaves; } @@ -169,23 +177,23 @@ class DraftEditorBlock extends React.Component { return leaves; } - var decorator = nullthrows(this.props.decorator); + const decorator = nullthrows(this.props.decorator); - var DecoratorComponent = decorator.getComponentForKey(decoratorKey); + const DecoratorComponent = decorator.getComponentForKey(decoratorKey); if (!DecoratorComponent) { return leaves; } - var decoratorProps = decorator.getPropsForKey(decoratorKey); - var decoratorOffsetKey = DraftOffsetKey.encode(blockKey, ii, 0); - var decoratedText = text.slice( + const decoratorProps = decorator.getPropsForKey(decoratorKey); + const decoratorOffsetKey = DraftOffsetKey.encode(blockKey, ii, 0); + const decoratedText = text.slice( leavesForLeafSet.first().get('start'), leavesForLeafSet.last().get('end'), ); // Resetting dir to the same value on a child node makes Chrome/Firefox // confused on cursor movement. See http://jsfiddle.net/d157kLck/3/ - var dir = UnicodeBidiDirection.getHTMLDirIfDifferent( + const dir = UnicodeBidiDirection.getHTMLDirIfDifferent( UnicodeBidi.getDirection(decoratedText), this.props.direction, ); @@ -207,10 +215,6 @@ class DraftEditorBlock extends React.Component { } render(): React.Node { - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an error - * when upgrading Flow's support for React. Common errors found when - * upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ const {direction, offsetKey} = this.props; const className = cx({ 'public/DraftStyleDefault/block': true, @@ -226,14 +230,4 @@ class DraftEditorBlock extends React.Component { } } -/** - * Return whether a block overlaps with either edge of the `SelectionState`. - */ -function isBlockOnSelectionEdge( - selection: SelectionState, - key: string, -): boolean { - return selection.getAnchorKey() === key || selection.getFocusKey() === key; -} - module.exports = DraftEditorBlock; diff --git a/src/component/contents/DraftEditorContents.react.js b/src/component/contents/DraftEditorContents.react.js index e1644542f5..6dff5f91c3 100644 --- a/src/component/contents/DraftEditorContents.react.js +++ b/src/component/contents/DraftEditorContents.react.js @@ -13,10 +13,14 @@ 'use strict'; -import type {BlockNodeRecord} from 'BlockNodeRecord'; import type {BidiDirection} from 'UnicodeBidiDirection'; +import type {BlockNodeRecord} from 'BlockNodeRecord'; +import type {DraftBlockRenderMap} from 'DraftBlockRenderMap'; +import type {DraftInlineStyle} from 'DraftInlineStyle'; +const ContentBlockNode = require('ContentBlockNode'); const DraftEditorBlock = require('DraftEditorBlock.react'); +const DraftEditorBlockNode = require('DraftEditorBlockNode.react'); const DraftOffsetKey = require('DraftOffsetKey'); const EditorState = require('EditorState'); const React = require('React'); @@ -26,12 +30,43 @@ const joinClasses = require('joinClasses'); const nullthrows = require('nullthrows'); type Props = { - blockRendererFn: Function, + blockRenderMap: DraftBlockRenderMap, + blockRendererFn: (block: BlockNodeRecord) => ?Object, blockStyleFn: (block: BlockNodeRecord) => string, + customStyleFn: (style: DraftInlineStyle, block: BlockNodeRecord) => ?Object, + customStyleMap?: Object, + editorKey: string, editorState: EditorState, textDirectionality?: BidiDirection, }; +/** + * 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. + */ +const 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', + }); +}; + /** * `DraftEditorContents` is the container component for all block components * rendered for a `DraftEditor`. It is optimized to aggressively avoid @@ -91,23 +126,14 @@ class DraftEditorContents extends React.Component { render(): React.Node { const { - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an error - * when upgrading Flow's support for React. Common errors found when - * upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ blockRenderMap, blockRendererFn, - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an error - * when upgrading Flow's support for React. Common errors found when - * upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ + blockStyleFn, customStyleMap, - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an error - * when upgrading Flow's support for React. Common errors found when - * upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ customStyleFn, editorState, + editorKey, + textDirectionality, } = this.props; const content = editorState.getCurrentContent(); @@ -117,109 +143,144 @@ class DraftEditorContents extends React.Component { const directionMap = nullthrows(editorState.getDirectionMap()); const blocksAsArray = content.getBlocksAsArray(); + const rootBlock = blocksAsArray[0]; const processedBlocks = []; - let currentDepth = null; - let lastWrapperTemplate = null; - - for (let ii = 0; ii < blocksAsArray.length; ii++) { - const block = blocksAsArray[ii]; - const key = block.getKey(); - const blockType = block.getType(); - - const customRenderer = blockRendererFn(block); - let CustomComponent, customProps, customEditable; - if (customRenderer) { - CustomComponent = customRenderer.component; - customProps = customRenderer.props; - customEditable = customRenderer.editable; - } - const {textDirectionality} = this.props; - const direction = textDirectionality - ? textDirectionality - : directionMap.get(key); - const offsetKey = DraftOffsetKey.encode(key, 0, 0); - const componentProps = { - contentState: content, - block, - blockProps: customProps, - customStyleMap, - customStyleFn, - decorator, - direction, - forceSelection, - key, - offsetKey, - selection, - tree: editorState.getBlockTree(key), - }; - - const configForType = - blockRenderMap.get(blockType) || blockRenderMap.get('unstyled'); - const wrapperTemplate = configForType.wrapper; - - const Element = - configForType.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 = - lastWrapperTemplate !== wrapperTemplate || - currentDepth === null || - depth > currentDepth; - className = joinClasses( - className, - getListItemClasses(blockType, depth, shouldResetCount, direction), - ); + if (rootBlock && rootBlock instanceof ContentBlockNode) { + let nodeBlock = rootBlock; + + while (nodeBlock) { + const blockKey = nodeBlock.getKey(); + const blockProps = { + blockRenderMap, + blockRendererFn, + blockStyleFn, + contentState: content, + customStyleFn, + customStyleMap, + decorator, + editorKey, + editorState, + forceSelection, + selection, + block: nodeBlock, + direction: textDirectionality + ? textDirectionality + : directionMap.get(blockKey), + tree: editorState.getBlockTree(blockKey), + }; + + const configForType = + blockRenderMap.get(nodeBlock.getType()) || + blockRenderMap.get('unstyled'); + const wrapperTemplate = configForType.wrapper; + processedBlocks.push({ + block: , + wrapperTemplate, + key: blockKey, + offsetKey: DraftOffsetKey.encode(blockKey, 0, 0), + }); + + const nextBlockKey = nodeBlock.getNextSiblingKey(); + nodeBlock = nextBlockKey ? content.getBlockForKey(nextBlockKey) : null; } + } else { + let currentDepth = null; + let lastWrapperTemplate = null; + + for (let ii = 0; ii < blocksAsArray.length; ii++) { + const block = blocksAsArray[ii]; + const key = block.getKey(); + const blockType = block.getType(); + + const customRenderer = blockRendererFn(block); + let CustomComponent, customProps, customEditable; + if (customRenderer) { + CustomComponent = customRenderer.component; + customProps = customRenderer.props; + customEditable = customRenderer.editable; + } + + const {textDirectionality} = this.props; + const direction = textDirectionality + ? textDirectionality + : directionMap.get(key); + const offsetKey = DraftOffsetKey.encode(key, 0, 0); + const componentProps = { + contentState: content, + block, + blockProps: customProps, + blockStyleFn, + customStyleMap, + customStyleFn, + decorator, + direction, + forceSelection, + key, + offsetKey, + selection, + tree: editorState.getBlockTree(key), + }; - const Component = CustomComponent || DraftEditorBlock; - let childProps = { - className, - 'data-block': true, - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an - * error when upgrading Flow's support for React. Common errors found - * when upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ - 'data-editor': this.props.editorKey, - 'data-offset-key': offsetKey, - key, - }; - if (customEditable !== undefined) { - childProps = { - ...childProps, - contentEditable: customEditable, - suppressContentEditableWarning: true, + const configForType = + blockRenderMap.get(blockType) || blockRenderMap.get('unstyled'); + const wrapperTemplate = configForType.wrapper; + + const Element = + configForType.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 = + lastWrapperTemplate !== wrapperTemplate || + currentDepth === null || + depth > currentDepth; + className = joinClasses( + className, + getListItemClasses(blockType, depth, shouldResetCount, direction), + ); + } + + const Component = CustomComponent || DraftEditorBlock; + let childProps = { + className, + 'data-block': true, + 'data-editor': editorKey, + 'data-offset-key': offsetKey, + key, }; - } + if (customEditable !== undefined) { + childProps = { + ...childProps, + contentEditable: customEditable, + suppressContentEditableWarning: true, + }; + } + + const child = React.createElement( + Element, + childProps, + , + ); - const child = React.createElement( - Element, - childProps, - /* $FlowFixMe(>=0.53.0 site=www,mobile) This comment suppresses an - * error when upgrading Flow's support for React. Common errors found - * when upgrading Flow's React support are documented at - * https://fburl.com/eq7bs81w */ - , - ); - - processedBlocks.push({ - block: child, - wrapperTemplate, - key, - offsetKey, - }); - - if (wrapperTemplate) { - currentDepth = block.getDepth(); - } else { - currentDepth = null; + processedBlocks.push({ + block: child, + wrapperTemplate, + key, + offsetKey, + }); + + if (wrapperTemplate) { + currentDepth = block.getDepth(); + } else { + currentDepth = null; + } + lastWrapperTemplate = wrapperTemplate; } - lastWrapperTemplate = wrapperTemplate; } // Group contiguous runs of blocks that have the same wrapperTemplate @@ -254,31 +315,4 @@ class DraftEditorContents extends React.Component { } } -/** - * 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/contents/DraftEditorLeaf.react.js b/src/component/contents/DraftEditorLeaf.react.js index a7ff274e8f..f237aae4d6 100644 --- a/src/component/contents/DraftEditorLeaf.react.js +++ b/src/component/contents/DraftEditorLeaf.react.js @@ -44,7 +44,7 @@ type Props = { // The current `SelectionState`, used to represent a selection range in the // editor - selection: SelectionState, + selection: ?SelectionState, // The offset of this string within its block. start: number, diff --git a/src/component/contents/__tests__/DraftEditorContent.react-test.js b/src/component/contents/__tests__/DraftEditorContents.react-test.js similarity index 96% rename from src/component/contents/__tests__/DraftEditorContent.react-test.js rename to src/component/contents/__tests__/DraftEditorContents.react-test.js index 54b045a6be..abf02f1fc9 100644 --- a/src/component/contents/__tests__/DraftEditorContent.react-test.js +++ b/src/component/contents/__tests__/DraftEditorContents.react-test.js @@ -85,8 +85,7 @@ test('defaults to "unstyled" block type for unknown block types', () => { } const block = ReactTestRenderer.create(); - const blockInstance = block.root; - const editorInstace = blockInstance._fiber.stateNode; + const editorInstace = block.getInstance(); expect(() => { editorInstace.toggleCustomBlock( diff --git a/src/component/contents/exploration/DraftEditorBlockNode.react.js b/src/component/contents/exploration/DraftEditorBlockNode.react.js new file mode 100644 index 0000000000..11c030d42b --- /dev/null +++ b/src/component/contents/exploration/DraftEditorBlockNode.react.js @@ -0,0 +1,377 @@ +/** + * 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 DraftEditorBlockNode.react + * @format + * @flow + * + * This file is a fork of DraftEditorBlock.react.js and DraftEditorContents.react.js + * + * This is unstable and not part of the public API and should not be used by + * production systems. This file may be update/removed without notice. + */ + +'use strict'; + +import type {BlockNodeRecord} from 'BlockNodeRecord'; +import type ContentState from 'ContentState'; +import type {DraftBlockRenderMap} from 'DraftBlockRenderMap'; +import type {DraftDecoratorType} from 'DraftDecoratorType'; +import type {DraftInlineStyle} from 'DraftInlineStyle'; +import type SelectionState from 'SelectionState'; +import type {BidiDirection} from 'UnicodeBidiDirection'; + +const DraftEditorNode = require('DraftEditorNode.react'); +const DraftOffsetKey = require('DraftOffsetKey'); +const EditorState = require('EditorState'); +const Immutable = require('immutable'); +const React = require('React'); +const ReactDOM = require('ReactDOM'); +const Scroll = require('Scroll'); +const Style = require('Style'); +const getElementPosition = require('getElementPosition'); +const getScrollPosition = require('getScrollPosition'); +const getViewportDimensions = require('getViewportDimensions'); +const invariant = require('invariant'); + +const SCROLL_BUFFER = 10; + +const {List} = Immutable; + +// we should harden up the bellow flow types to make them more strict +type CustomRenderConfig = Object; +type DraftRenderConfig = Object; +type BlockRenderFn = (block: BlockNodeRecord) => ?Object; +type BlockStyleFn = (block: BlockNodeRecord) => string; + +type Props = { + block: BlockNodeRecord, + blockProps?: Object, + blockRenderMap: DraftBlockRenderMap, + blockRendererFn: BlockRenderFn, + blockStyleFn: BlockStyleFn, + contentState: ContentState, + customStyleFn: (style: DraftInlineStyle, block: BlockNodeRecord) => ?Object, + customStyleMap: Object, + decorator: ?DraftDecoratorType, + direction: BidiDirection, + editorKey: string, + editorState: EditorState, + forceSelection: boolean, + selection: SelectionState, + startIndent?: boolean, + tree: List, +}; + +/** + * Return whether a block overlaps with either edge of the `SelectionState`. + */ +const isBlockOnSelectionEdge = ( + selection: SelectionState, + key: string, +): boolean => { + return selection.getAnchorKey() === key || selection.getFocusKey() === key; +}; + +/** + * We will use this helper to identify blocks that need to be wrapped but have siblings that + * also share the same wrapper element, this way we can do the wrapping once the last sibling + * is added. + */ +const shouldNotAddWrapperElement = ( + block: BlockNodeRecord, + contentState: ContentState, +): boolean => { + const nextSiblingKey = block.getNextSiblingKey(); + + return nextSiblingKey + ? contentState.getBlockForKey(nextSiblingKey).getType() === block.getType() + : false; +}; + +const applyWrapperElementToSiblings = ( + wrapperTemplate: *, + Element: string, + nodes: Array, +): Array => { + const wrappedSiblings = []; + + // we check back until we find a sibbling that does not have same wrapper + for (const sibling: any of nodes.reverse()) { + if (sibling.type !== Element) { + break; + } + wrappedSiblings.push(sibling); + } + + // we now should remove from acc the wrappedSiblings and add them back under same wrap + nodes.splice(nodes.indexOf(wrappedSiblings[0]), wrappedSiblings.length + 1); + + const childrenIs = wrappedSiblings.reverse(); + + const key = childrenIs[0].key; + + nodes.push( + React.cloneElement( + wrapperTemplate, + { + key: `${key}-wrap`, + 'data-offset-key': DraftOffsetKey.encode(key, 0, 0), + }, + childrenIs, + ), + ); + + return nodes; +}; + +const getDraftRenderConfig = ( + block: BlockNodeRecord, + blockRenderMap: DraftBlockRenderMap, +): DraftRenderConfig => { + const configForType = + blockRenderMap.get(block.getType()) || blockRenderMap.get('unstyled'); + + const wrapperTemplate = configForType.wrapper; + const Element = + configForType.element || blockRenderMap.get('unstyled').element; + + return { + Element, + wrapperTemplate, + }; +}; + +const getCustomRenderConfig = ( + block: BlockNodeRecord, + blockRendererFn: BlockRenderFn, +): CustomRenderConfig => { + const customRenderer = blockRendererFn(block); + + if (!customRenderer) { + return {}; + } + + const { + component: CustomComponent, + props: customProps, + editable: customEditable, + } = customRenderer; + + return { + CustomComponent, + customProps, + customEditable, + }; +}; + +const getElementPropsConfig = ( + block: BlockNodeRecord, + editorKey: string, + offsetKey: string, + blockStyleFn: BlockStyleFn, + customConfig: *, +): Object => { + let elementProps: Object = { + 'data-block': true, + 'data-editor': editorKey, + 'data-offset-key': offsetKey, + key: block.getKey(), + }; + const customClass = blockStyleFn(block); + + if (customClass) { + elementProps.className = customClass; + } + + if (customConfig.customEditable !== undefined) { + elementProps = { + ...elementProps, + contentEditable: customConfig.customEditable, + suppressContentEditableWarning: true, + }; + } + + return elementProps; +}; + +class DraftEditorBlockNode extends React.Component { + shouldComponentUpdate(nextProps: Props): boolean { + const {block, direction, tree} = this.props; + const isContainerNode = !block.getChildKeys.isEmpty(); + const blockHasChanged = + block !== nextProps.block || + tree !== nextProps.tree || + direction !== nextProps.direction || + (isBlockOnSelectionEdge(nextProps.selection, nextProps.block.getKey()) && + nextProps.forceSelection); + + // if we have children at this stage we always re-render container nodes + // else if its a root node we avoid re-rendering by checking for block updates + return isContainerNode || blockHasChanged; + } + + /** + * When a block is mounted and overlaps the selection state, we need to make + * sure that the cursor is visible to match native behavior. This may not + * be the case if the user has pressed `RETURN` or pasted some content, since + * programatically creating these new blocks and setting the DOM selection + * will miss out on the browser natively scrolling to that position. + * + * To replicate native behavior, if the block overlaps the selection state + * on mount, force the scroll position. Check the scroll state of the scroll + * parent, and adjust it to align the entire block to the bottom of the + * scroll parent. + */ + componentDidMount(): void { + const selection = this.props.selection; + const endKey = selection.getEndKey(); + if (!selection.getHasFocus() || endKey !== this.props.block.getKey()) { + return; + } + + const blockNode = ReactDOM.findDOMNode(this); + const scrollParent = Style.getScrollParent(blockNode); + const scrollPosition = getScrollPosition(scrollParent); + let scrollDelta; + + if (scrollParent === window) { + const nodePosition = getElementPosition(blockNode); + const nodeBottom = nodePosition.y + nodePosition.height; + const viewportHeight = getViewportDimensions().height; + scrollDelta = nodeBottom - viewportHeight; + if (scrollDelta > 0) { + window.scrollTo( + scrollPosition.x, + scrollPosition.y + scrollDelta + SCROLL_BUFFER, + ); + } + } else { + invariant( + blockNode instanceof HTMLElement, + 'blockNode is not an HTMLElement', + ); + const blockBottom = blockNode.offsetHeight + blockNode.offsetTop; + const scrollBottom = scrollParent.offsetHeight + scrollPosition.y; + scrollDelta = blockBottom - scrollBottom; + if (scrollDelta > 0) { + Scroll.setTop( + scrollParent, + Scroll.getTop(scrollParent) + scrollDelta + SCROLL_BUFFER, + ); + } + } + } + + render(): React.Node { + const { + block, + blockRenderMap, + blockRendererFn, + blockStyleFn, + contentState, + decorator, + editorKey, + editorState, + customStyleFn, + customStyleMap, + direction, + forceSelection, + selection, + tree, + } = this.props; + + let children = null; + + if (block.children.size) { + children = block.children.reduce((acc, key) => { + const offsetKey = DraftOffsetKey.encode(key, 0, 0); + const child = contentState.getBlockForKey(key); + const customConfig = getCustomRenderConfig(child, blockRendererFn); + const Component = customConfig.CustomComponent || DraftEditorBlockNode; + const {Element, wrapperTemplate} = getDraftRenderConfig( + child, + blockRenderMap, + ); + const elementProps = getElementPropsConfig( + child, + editorKey, + offsetKey, + blockStyleFn, + customConfig, + ); + const childProps = { + ...this.props, + tree: editorState.getBlockTree(key), + blockProps: customConfig.customProps, + offsetKey, + block: child, + }; + + acc.push( + React.createElement( + Element, + elementProps, + , + ), + ); + + if ( + !wrapperTemplate || + shouldNotAddWrapperElement(child, contentState) + ) { + return acc; + } + + // if we are here it means we are the last block + // that has a wrapperTemplate so we should wrap itself + // and all other previous siblings that share the same wrapper + applyWrapperElementToSiblings(wrapperTemplate, Element, acc); + + return acc; + }, []); + } + + const blockKey = block.getKey(); + const offsetKey = DraftOffsetKey.encode(blockKey, 0, 0); + + const blockNode = ( + + ); + + if (block.getParentKey()) { + return blockNode; + } + + const {Element} = getDraftRenderConfig(block, blockRenderMap); + const elementProps = getElementPropsConfig( + block, + editorKey, + offsetKey, + blockStyleFn, + getCustomRenderConfig(block, blockRendererFn), + ); + + // root block nodes needs to be wrapped + return React.createElement(Element, elementProps, blockNode); + } +} + +module.exports = DraftEditorBlockNode; diff --git a/src/component/contents/exploration/DraftEditorDecoratedLeaves.react.js b/src/component/contents/exploration/DraftEditorDecoratedLeaves.react.js new file mode 100644 index 0000000000..c13a5c989c --- /dev/null +++ b/src/component/contents/exploration/DraftEditorDecoratedLeaves.react.js @@ -0,0 +1,91 @@ +/** + * 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 DraftEditorDecoratedLeaves.react + * @format + * @flow + * + * This is unstable and not part of the public API and should not be used by + * production systems. This file may be update/removed without notice. + */ + +'use strict'; + +const DraftOffsetKey = require('DraftOffsetKey'); +const React = require('React'); +const UnicodeBidi = require('UnicodeBidi'); +const UnicodeBidiDirection = require('UnicodeBidiDirection'); + +import type {BlockNodeRecord} from 'BlockNodeRecord'; +import type ContentState from 'ContentState'; +import type {DraftDecoratorType} from 'DraftDecoratorType'; +import type {BidiDirection} from 'UnicodeBidiDirection'; +import type {List} from 'immutable'; + +type Props = { + block: BlockNodeRecord, + children: ?Array, + contentState: ContentState, + decorator: DraftDecoratorType, + decoratorKey: string, + direction: BidiDirection, + text: string, + leafSet: List, +}; + +class DraftEditorDecoratedLeaves extends React.Component { + render(): React.Node { + const { + block, + children, + contentState, + decorator, + decoratorKey, + direction, + leafSet, + text, + } = this.props; + + const blockKey = block.getKey(); + const leavesForLeafSet = leafSet.get('leaves'); + const DecoratorComponent = decorator.getComponentForKey(decoratorKey); + const decoratorProps = decorator.getPropsForKey(decoratorKey); + const decoratorOffsetKey = DraftOffsetKey.encode( + blockKey, + parseInt(decoratorKey, 10), + 0, + ); + + const decoratedText = text.slice( + leavesForLeafSet.first().get('start'), + leavesForLeafSet.last().get('end'), + ); + + // Resetting dir to the same value on a child node makes Chrome/Firefox + // confused on cursor movement. See http://jsfiddle.net/d157kLck/3/ + const dir = UnicodeBidiDirection.getHTMLDirIfDifferent( + UnicodeBidi.getDirection(decoratedText), + direction, + ); + + return ( + + {children} + + ); + } +} + +module.exports = DraftEditorDecoratedLeaves; diff --git a/src/component/contents/exploration/DraftEditorNode.react.js b/src/component/contents/exploration/DraftEditorNode.react.js new file mode 100644 index 0000000000..cf97ae0ea7 --- /dev/null +++ b/src/component/contents/exploration/DraftEditorNode.react.js @@ -0,0 +1,131 @@ +/** + * 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 DraftEditorNode.react + * @format + * @flow + * + * This is unstable and not part of the public API and should not be used by + * production systems. This file may be update/removed without notice. + */ + +'use strict'; + +import type {BlockNodeRecord} from 'BlockNodeRecord'; +import type ContentState from 'ContentState'; +import type {DraftDecoratorType} from 'DraftDecoratorType'; +import type SelectionState from 'SelectionState'; +import type {BidiDirection} from 'UnicodeBidiDirection'; + +const DraftEditorDecoratedLeaves = require('DraftEditorDecoratedLeaves.react'); +const DraftEditorLeaf = require('DraftEditorLeaf.react'); +const DraftOffsetKey = require('DraftOffsetKey'); +const Immutable = require('immutable'); +const React = require('React'); +const cx = require('cx'); + +const {List} = Immutable; + +type Props = { + block: BlockNodeRecord, + children: ?Array, + contentState: ContentState, + customStyleFn: Function, + customStyleMap: Object, + decorator: ?DraftDecoratorType, + direction: BidiDirection, + forceSelection: boolean, + hasSelection: boolean, + selection: SelectionState, + tree: List, +}; + +class DraftEditorNode extends React.Component { + render(): React.Node { + const { + block, + contentState, + customStyleFn, + customStyleMap, + decorator, + direction, + forceSelection, + hasSelection, + selection, + tree, + } = this.props; + + const blockKey = block.getKey(); + const text = block.getText(); + const lastLeafSet = tree.size - 1; + + const children = + this.props.children || + tree + .map((leafSet, ii) => { + const decoratorKey = leafSet.get('decoratorKey'); + const leavesForLeafSet = leafSet.get('leaves'); + const lastLeaf = leavesForLeafSet.size - 1; + const Leaves = leavesForLeafSet + .map((leaf, jj) => { + const offsetKey = DraftOffsetKey.encode(blockKey, ii, jj); + const start = leaf.get('start'); + const end = leaf.get('end'); + return ( + + ); + }) + .toArray(); + + if (!decoratorKey || !decorator) { + return Leaves; + } + + return ( + + ); + }) + .toArray(); + + return ( +
+ {children} +
+ ); + } +} + +module.exports = DraftEditorNode; diff --git a/src/component/contents/exploration/__tests__/DraftEditorBlockNode.react-test.js b/src/component/contents/exploration/__tests__/DraftEditorBlockNode.react-test.js new file mode 100644 index 0000000000..eaa8452cee --- /dev/null +++ b/src/component/contents/exploration/__tests__/DraftEditorBlockNode.react-test.js @@ -0,0 +1,539 @@ +/** + * 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. + * + * @emails oncall+ui_infra + * @format + */ + +'use strict'; + +jest + .disableAutomock() + .mock('Style') + .mock('getElementPosition') + .mock('getScrollPosition') + .mock('getViewportDimensions'); + +const BlockTree = require('BlockTree'); +const ContentBlockNode = require('ContentBlockNode'); +const ContentState = require('ContentState'); +const CompositeDraftDecorator = require('CompositeDraftDecorator'); +const DefaultDraftBlockRenderMap = require('DefaultDraftBlockRenderMap'); +const EditorState = require('EditorState'); +const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); +const SelectionState = require('SelectionState'); +const UnicodeBidiDirection = require('UnicodeBidiDirection'); +const Style = require('Style'); +const getElementPosition = require('getElementPosition'); +const getScrollPosition = require('getScrollPosition'); +const getViewportDimensions = require('getViewportDimensions'); + +const DraftEditorBlockNode = require('DraftEditorBlockNode.react'); +const Immutable = require('immutable'); +const TestHelper = require('_DraftTestHelper'); + +const {List} = Immutable; + +const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B', 'C', 'D']), +}); + +const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + nextSibling: 'C', + type: 'header-three', + key: 'B', + text: 'Left', + children: List([]), + }), + new ContentBlockNode({ + parent: 'A', + type: 'header-one', + prevSibling: 'B', + nextSibling: 'D', + key: 'C', + text: 'Middle', + children: List([]), + }), + new ContentBlockNode({ + parent: 'A', + prevSibling: 'C', + type: 'header-two', + key: 'D', + text: 'Right', + children: List([]), + }), +]); + +const selectionState = new SelectionState({ + anchorKey: 'A', + anchorOffset: 0, + focusKey: 'A', + focusOffset: 0, + isBackward: false, + hasFocus: false, +}); + +const PROPS = { + block: rootBlock, + blockRenderMap: DefaultDraftBlockRenderMap, + blockRendererFn: block => null, + blockStyleFn: block => '', + contentState, + customStyleFn: (style, block) => null, + customStyleMap: {}, + decorator: null, + direction: UnicodeBidiDirection.LTR, + editorKey: 'editor', + editorState: EditorState.createWithContent(contentState, null), + forceSelection: false, + offsetKey: 'A-0-0', + selection: selectionState, + tree: BlockTree.generate(contentState, rootBlock, null), +}; + +const setupDomMocks = () => { + Style.getScrollParent.mockReturnValue(window); + window.scrollTo = jest.fn(); + getElementPosition.mockReturnValue({ + x: 0, + y: 600, + width: 500, + height: 16, + }); + getScrollPosition.mockReturnValue({x: 0, y: 0}); + getViewportDimensions.mockReturnValue({width: 1200, height: 800}); +}; + +const assertDraftEditorBlockRendering = props => { + const childProps = { + ...props, + editorState: EditorState.createWithContent( + props.contentState, + props.decorator, + ), + }; + + const blockNode = ReactTestRenderer.create( + , + ); + + expect( + TestHelper.transformSnapshotProps(blockNode.toJSON()), + ).toMatchSnapshot(); +}; + +beforeEach(() => { + setupDomMocks(); +}); + +test('renders block with no children', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: 'some text', + type: 'header-one', + }); + + const contentState = ContentState.createFromBlockArray([rootBlock]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with children', () => { + assertDraftEditorBlockRendering(PROPS); +}); + +test('renders block with child that have wrapperElement', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with children that have same wrapperElement', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B', 'C']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + nextSibling: 'C', + }), + new ContentBlockNode({ + parent: 'A', + key: 'C', + text: 'second list item', + type: 'ordered-list-item', + prevSibling: 'B', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested child that have same wrapperElement', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: '', + type: 'unordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'deeply nested list', + type: 'unordered-list-item', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested child that is of different block type', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested child that have different wrapperElement', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: '', + type: 'unordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'deeply nested list', + type: 'ordered-list-item', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested children with decorator', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + ]); + + const DecoratedComponent = props => { + return {props.children}; + }; + + const decorator = new CompositeDraftDecorator([ + { + strategy: (block, callback, contentState) => { + if (block.getType() === 'header-one') { + callback(0, block.getLength()); + } + }, + component: DecoratedComponent, + }, + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + decorator, + block: rootBlock, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested children with different direction', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + direction: UnicodeBidiDirection.RTL, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested children with custom component', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + ]); + + const CustomBlock = props => { + return
Custom Component
; + }; + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + blockRendererFn: block => { + if (block.getType() === 'header-one') { + return { + component: CustomBlock, + }; + } + return null; + }, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested children with custom component and editable prop', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + ]); + + const CustomBlock = props => { + return ( +
+ Custom Component {props.blockProps.foo} +
+ ); + }; + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + blockRendererFn: block => { + if (block.getType() === 'header-one') { + return { + component: CustomBlock, + editable: false, + props: { + foo: 'bar', + }, + }; + } + return null; + }, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); + +test('renders block with nested children with blockStyleFn', () => { + const rootBlock = new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + }); + + const contentState = ContentState.createFromBlockArray([ + rootBlock, + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + ]); + + assertDraftEditorBlockRendering({ + ...PROPS, + block: rootBlock, + blockStyleFn: block => { + if (block.getType() === 'header-one') { + return 'My-fancy-custom-class'; + } + return null; + }, + contentState, + tree: BlockTree.generate(contentState, rootBlock, null), + }); +}); diff --git a/src/component/contents/exploration/__tests__/DraftEditorContentsExperimental.react-test.js b/src/component/contents/exploration/__tests__/DraftEditorContentsExperimental.react-test.js new file mode 100644 index 0000000000..b87ec39c97 --- /dev/null +++ b/src/component/contents/exploration/__tests__/DraftEditorContentsExperimental.react-test.js @@ -0,0 +1,144 @@ +/** + * 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. + * + * @emails oncall+ui_infra + * @format + */ + +'use strict'; + +jest.disableAutomock(); + +const ContentBlock = require('ContentBlock'); +const ContentBlockNode = require('ContentBlockNode'); +const ContentState = require('ContentState'); +const DefaultDraftBlockRenderMap = require('DefaultDraftBlockRenderMap'); +const DraftEditorContents = require('DraftEditorContents.react'); +const EditorState = require('EditorState'); +const Immutable = require('immutable'); +const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); +const SelectionState = require('SelectionState'); +const TestHelper = require('_DraftTestHelper'); + +const {List} = Immutable; + +const selectionState = new SelectionState({ + anchorKey: 'A', + anchorOffset: 0, + focusKey: 'A', + focusOffset: 0, + isBackward: false, + hasFocus: true, +}); + +const contentState = ContentState.createFromBlockArray([ + new ContentBlock({ + key: 'A', + text: 'Alpha', + type: 'blockquote', + }), + new ContentBlockNode({ + key: 'B', + text: 'Beta', + type: 'header-one', + }), + new ContentBlockNode({ + key: 'C', + text: 'Charlie', + type: 'ordered-list', + }), +]); + +const PROPS = { + blockRenderMap: DefaultDraftBlockRenderMap, + blockRendererFn: block => null, + blockStyleFn: block => '', + contentState, + customStyleFn: (style, block) => null, + editorKey: 'editor', + editorState: EditorState.createWithContent(contentState), + selection: selectionState, +}; + +const assertDraftEditorContentsRendering = props => { + const childProps = { + ...props, + editorState: EditorState.createWithContent(props.contentState), + }; + + const blockNode = ReactTestRenderer.create( + , + ); + + expect( + TestHelper.transformSnapshotProps(blockNode.toJSON()), + ).toMatchSnapshot(); +}; + +test('renders ContentBlocks', () => { + assertDraftEditorContentsRendering(PROPS); +}); + +test('renders ContentBlockNodes', () => { + const contentState = ContentState.createFromBlockArray([ + new ContentBlockNode({ + key: 'A', + text: '', + type: 'blockquote', + children: List(['B']), + nextSibling: 'D', + }), + new ContentBlockNode({ + parent: 'A', + key: 'B', + text: 'fist list item', + type: 'ordered-list-item', + children: List(['C']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + text: 'header inside list', + type: 'header-one', + }), + new ContentBlockNode({ + key: 'D', + prevSibling: 'A', + text: 'header two', + type: 'header-two', + }), + ]); + + assertDraftEditorContentsRendering({ + ...PROPS, + contentState, + }); +}); + +test('renders ContentBlockNodes with root blocks that have wrapperTemplate', () => { + const contentState = ContentState.createFromBlockArray([ + new ContentBlockNode({ + key: 'A', + text: 'list one', + type: 'ordered-list-item', + nextSibling: 'B', + }), + new ContentBlockNode({ + key: 'B', + text: 'list two', + type: 'ordered-list-item', + prevSibling: 'A', + }), + ]); + + assertDraftEditorContentsRendering({ + ...PROPS, + contentState, + }); +}); diff --git a/src/component/contents/exploration/__tests__/__snapshots__/DraftEditorBlockNode.react-test.js.snap b/src/component/contents/exploration/__tests__/__snapshots__/DraftEditorBlockNode.react-test.js.snap new file mode 100644 index 0000000000..7d7b497f23 --- /dev/null +++ b/src/component/contents/exploration/__tests__/__snapshots__/DraftEditorBlockNode.react-test.js.snap @@ -0,0 +1,558 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders block with child that have wrapperElement 1`] = ` +
+
+
    +
  1. +
    + + + fist list item + + +
    +
  2. +
+
+
+`; + +exports[`renders block with children 1`] = ` +
+
+

+
+ + + Left + + +
+

+

+
+ + + Middle + + +
+

+

+
+ + + Right + + +
+

+
+
+`; + +exports[`renders block with children that have same wrapperElement 1`] = ` +
+
+
    +
  1. +
    + + + fist list item + + +
    +
  2. +
  3. +
    + + + second list item + + +
    +
  4. +
+
+
+`; + +exports[`renders block with nested child that have different wrapperElement 1`] = ` +
+
+
    +
  • +
    +
      +
    1. +
      + + + deeply nested list + + +
      +
    2. +
    +
    +
  • +
+
+
+`; + +exports[`renders block with nested child that have same wrapperElement 1`] = ` +
+
+
    +
  • +
    +
      +
    • +
      + + + deeply nested list + + +
      +
    • +
    +
    +
  • +
+
+
+`; + +exports[`renders block with nested child that is of different block type 1`] = ` +
+
+
    +
  1. +
    +

    +
    + + + header inside list + + +
    +

    +
    +
  2. +
+
+
+`; + +exports[`renders block with nested children with blockStyleFn 1`] = ` +
+
+
    +
  1. +
    +

    +
    + + + header inside list + + +
    +

    +
    +
  2. +
+
+
+`; + +exports[`renders block with nested children with custom component 1`] = ` +
+
+
    +
  1. +
    +

    +
    + Custom Component +
    +

    +
    +
  2. +
+
+
+`; + +exports[`renders block with nested children with custom component and editable prop 1`] = ` +
+
+
    +
  1. +
    +

    +
    + Custom Component + bar +
    +

    +
    +
  2. +
+
+
+`; + +exports[`renders block with nested children with decorator 1`] = ` +
+
+
    +
  1. +
    +

    +
    + + + + header inside list + + + +
    +

    +
    +
  2. +
+
+
+`; + +exports[`renders block with nested children with different direction 1`] = ` +
+
+
    +
  1. +
    +

    +
    + + + header inside list + + +
    +

    +
    +
  2. +
+
+
+`; + +exports[`renders block with no children 1`] = ` +

+
+ + + some text + + +
+

+`; diff --git a/src/component/contents/exploration/__tests__/__snapshots__/DraftEditorContentsExperimental.react-test.js.snap b/src/component/contents/exploration/__tests__/__snapshots__/DraftEditorContentsExperimental.react-test.js.snap new file mode 100644 index 0000000000..4593cf7809 --- /dev/null +++ b/src/component/contents/exploration/__tests__/__snapshots__/DraftEditorContentsExperimental.react-test.js.snap @@ -0,0 +1,190 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ContentBlockNodes 1`] = ` +
+
+
+
    +
  1. +
    +

    +
    + + + header inside list + + +
    +

    +
    +
  2. +
+
+
+

+
+ + + header two + + +
+

+
+`; + +exports[`renders ContentBlockNodes with root blocks that have wrapperTemplate 1`] = ` +
+
    +
  1. +
    + + + list one + + +
    +
  2. +
  3. +
    + + + list two + + +
    +
  4. +
+
+`; + +exports[`renders ContentBlocks 1`] = ` +
+
+
+ + + Alpha + + +
+
+

+
+ + + Beta + + +
+

+
+
+ + + Charlie + + +
+
+
+`; diff --git a/src/component/utils/_DraftTestHelper.js b/src/component/utils/_DraftTestHelper.js new file mode 100644 index 0000000000..ceb8b267a2 --- /dev/null +++ b/src/component/utils/_DraftTestHelper.js @@ -0,0 +1,48 @@ +/** + * 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 _DraftTestHelper + * @flow + * @format + */ + +const BLACK_LIST_PROPS = ['class', 'className', 'data-reactroot']; +const transformSnapshotProps = ( + node: any, + blackList: Array = BLACK_LIST_PROPS, +) => { + const stack = [node]; + while (stack.length) { + const node = stack.pop(); + if (node.props) { + BLACK_LIST_PROPS.forEach(prop => delete node.props[prop]); + } + if (Array.isArray(node.children)) { + stack.push(...node.children); + } + } + return node; +}; + +const DraftTestHelper = { + /** + * This is meant to be used in combination with ReactTestRenderer + * to ensure compatibility with running our snapshot tests internally + * + * usage example: + * + * const blockNode = ReactTestRenderer.create( + * , + * ); + * + * expect(transformSnapshotProps(blockNode.toJSON())).toMatchSnapshot(); + */ + transformSnapshotProps, +}; + +module.exports = DraftTestHelper; diff --git a/src/model/immutable/ContentBlockNode.js b/src/model/immutable/ContentBlockNode.js index 3a20babe60..f60f9fb1f7 100644 --- a/src/model/immutable/ContentBlockNode.js +++ b/src/model/immutable/ContentBlockNode.js @@ -10,6 +10,9 @@ * @format * @flow * + * This file is a fork of ContentBlock adding support for nesting references by + * providing links to children, parent, prevSibling, and nextSibling. + * * This is unstable and not part of the public API and should not be used by * production systems. This file may be update/removed without notice. */