/** * 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-core.react * @format * @flow */ 'use strict'; import type { BlockNodeRecord } from './BlockNodeRecord'; import type { DraftBlockRenderMap } from './DraftBlockRenderMap'; import type { DraftInlineStyle } from './DraftInlineStyle'; import type { BidiDirection } from 'fbjs/lib/UnicodeBidiDirection'; const DraftEditorBlock = require('./DraftEditorBlock.react'); const DraftOffsetKey = require('./DraftOffsetKey'); const EditorState = require('./EditorState'); const React = require('react'); const cx = require('fbjs/lib/cx'); const joinClasses = require('fbjs/lib/joinClasses'); const nullthrows = require('fbjs/lib/nullthrows'); type Props = { 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 * 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 DraftEditorContents extends React.Component { shouldComponentUpdate(nextProps: Props): boolean { const prevEditorState = this.props.editorState; const nextEditorState = nextProps.editorState; const prevDirectionMap = prevEditorState.getDirectionMap(); const nextDirectionMap = nextEditorState.getDirectionMap(); // Text direction has changed for one or more blocks. We must re-render. if (prevDirectionMap !== nextDirectionMap) { return true; } const didHaveFocus = prevEditorState.getSelection().getHasFocus(); const nowHasFocus = nextEditorState.getSelection().getHasFocus(); if (didHaveFocus !== nowHasFocus) { return true; } const nextNativeContent = nextEditorState.getNativelyRenderedContent(); const wasComposing = prevEditorState.isInCompositionMode(); const nowComposing = nextEditorState.isInCompositionMode(); // If the state is unchanged or we're currently rendering a natively // rendered state, there's nothing new to be done. if (prevEditorState === nextEditorState || nextNativeContent !== null && nextEditorState.getCurrentContent() === nextNativeContent || wasComposing && nowComposing) { return false; } const prevContent = prevEditorState.getCurrentContent(); const nextContent = nextEditorState.getCurrentContent(); const prevDecorator = prevEditorState.getDecorator(); const nextDecorator = nextEditorState.getDecorator(); return wasComposing !== nowComposing || prevContent !== nextContent || prevDecorator !== nextDecorator || nextEditorState.mustForceSelection(); } render(): React.Node { const { blockRenderMap, blockRendererFn, blockStyleFn, customStyleMap, customStyleFn, editorState, editorKey, textDirectionality } = this.props; const content = editorState.getCurrentContent(); const selection = editorState.getSelection(); const forceSelection = editorState.mustForceSelection(); const decorator = editorState.getDecorator(); const directionMap = nullthrows(editorState.getDirectionMap()); const blocksAsArray = content.getBlocksAsArray(); 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 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 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 = ''; if (blockStyleFn) { className = 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, ); processedBlocks.push({ block: child, wrapperTemplate, key, offsetKey }); if (wrapperTemplate) { currentDepth = block.getDepth(); } else { currentDepth = null; } lastWrapperTemplate = wrapperTemplate; } // Group contiguous runs of blocks that have the same wrapperTemplate const outputBlocks = []; for (let ii = 0; ii < processedBlocks.length;) { const info: any = processedBlocks[ii]; if (info.wrapperTemplate) { const blocks = []; do { blocks.push(processedBlocks[ii].block); ii++; } while (ii < processedBlocks.length && processedBlocks[ii].wrapperTemplate === info.wrapperTemplate); const wrapperElement = React.cloneElement(info.wrapperTemplate, { key: info.key + '-wrap', 'data-offset-key': info.offsetKey }, blocks); outputBlocks.push(wrapperElement); } else { outputBlocks.push(info.block); ii++; } } return
{outputBlocks}
; } } module.exports = DraftEditorContents;