UNPKG

draft-js

Version:

A React framework for building text editors.

245 lines (207 loc) 8.63 kB
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow * @emails oncall+draft_js */ 'use strict'; import type { BlockNodeRecord } from "./BlockNodeRecord"; import type { DraftBlockRenderMap } from "./DraftBlockRenderMap"; import type { DraftInlineStyle } from "./DraftInlineStyle"; import type EditorState from "./EditorState"; import type { BidiDirection } from "fbjs/lib/UnicodeBidiDirection"; const DraftEditorBlock = require("./DraftEditorBlock.react"); const DraftOffsetKey = require("./DraftOffsetKey"); const React = require("react"); const cx = require("fbjs/lib/cx"); const joinClasses: (className?: ?string, ...classes: Array<?string>) => string = 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, preventScroll?: boolean, 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<Props> { 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, preventScroll, 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, offsetKey, preventScroll, 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, /* $FlowFixMe[incompatible-type] (>=0.112.0 site=www,mobile) This * comment suppresses an error found when Flow v0.112 was deployed. To * see the error delete this comment and run Flow. */ <Component {...componentProps} key={key} />); 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 <div data-contents="true">{outputBlocks}</div>; } } module.exports = DraftEditorContents;