draft-js
Version:
A React framework for building text editors.
245 lines (207 loc) • 8.63 kB
Flow
/**
* 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
*/
;
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;