Files
DefinitelyTyped/types/draft-js/draft-js-tests.tsx
Ryan Schwers d28f34da92 [Draft-js] Support custom keyBindingFN strings
The current typings don't work for custom keyBindingFn
return values. keyBindingFn implementations may return
any string or null. TypeScript's handling of string unions is
preventing the fn type definition overloading from working
as intended. This change removes overloading and moves
the union to a type, and adds nullability to the type definition.
2017-05-16 15:10:22 -07:00

277 lines
7.7 KiB
TypeScript

import * as React from "react";
import * as ReactDOM from "react-dom";
import {Map} from "immutable";
import {
ContentBlock,
DefaultDraftBlockRenderMap,
Editor,
EditorState,
Modifier,
RichUtils,
SelectionState,
getDefaultKeyBinding,
} from 'draft-js';
const SPLIT_HEADER_BLOCK = 'split-header-block';
export type KeyName =
'ENTER';
export type KeyCode = number;
export const KEYCODES: Record<KeyName, KeyCode> = {
ENTER: 13,
};
type SyntheticKeyboardEvent = React.KeyboardEvent<{}>;
class RichEditorExample extends React.Component<{}, { editorState: EditorState }> {
constructor() {
super();
this.state = { editorState: EditorState.createEmpty() };
}
onChange: (editorState: EditorState) => void = (editorState: EditorState) => this.setState({ editorState });
keyBindingFn(e: SyntheticKeyboardEvent): string {
if (e.keyCode === KEYCODES.ENTER) {
const { editorState } = this.state;
const contentState = editorState.getCurrentContent();
const selectionState = editorState.getSelection();
// only split headers into header and unstyled if we press 'Enter'
// at the end of a header (without text selected)
if (selectionState.isCollapsed()) {
const endKey = selectionState.getEndKey();
const endOffset = selectionState.getEndOffset();
const endBlock = contentState.getBlockForKey(endKey);
if (isHeaderBlock(endBlock) && endOffset === endBlock.getText().length) {
return SPLIT_HEADER_BLOCK;
}
}
}
return getDefaultKeyBinding(e);
}
handleKeyCommand = (command: string) => {
if (command === SPLIT_HEADER_BLOCK) {
this.onChange(this.splitHeaderToNewBlock());
return 'handled';
}
const {editorState} = this.state;
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
this.onChange(newState);
return "handled";
}
return "not-handled";
}
toggleBlockType: (blockType: string) => void = (blockType: string) => {
this.onChange(RichUtils.toggleBlockType(this.state.editorState, blockType));
}
toggleInlineStyle: (inlineStyle: string) => void = (inlineStyle: string) => {
this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, inlineStyle));
}
splitHeaderToNewBlock(): EditorState {
const { editorState } = this.state;
const selection = editorState.getSelection();
// Add a new block after the cursor
const contentWithBlock = Modifier.splitBlock(
editorState.getCurrentContent(),
selection,
);
// Change the new block type to be normal 'unstyled' text,
const newBlock = contentWithBlock.getBlockAfter(selection.getEndKey());
const contentWithUnstyledBlock = Modifier.setBlockType(
contentWithBlock,
SelectionState.createEmpty(newBlock.getKey()),
'unstyled',
);
// push the new state with 'insert-characters' to preserve the undo/redo stack
const stateWithNewline = EditorState.push(
editorState,
contentWithUnstyledBlock,
'insert-characters'
);
// manually move the cursor to the next line (as expected)
const nextState = EditorState.forceSelection(
stateWithNewline,
SelectionState.createEmpty(newBlock.getKey()),
);
return nextState;
}
render(): React.ReactElement<{}> {
// If the user changes block type before entering any text, we can
// either style the placeholder or hide it. Let's just hide it now.
let className = 'RichEditor-editor';
var contentState = this.state.editorState.getCurrentContent();
if (!contentState.hasText()) {
if (contentState.getBlockMap().first().getType() !== 'unstyled') {
className += ' RichEditor-hidePlaceholder';
}
}
return (
<div className="RichEditor-root">
<BlockStyleControls
editorState={this.state.editorState}
onToggle={this.toggleBlockType}
/>
<InlineStyleControls
editorState={this.state.editorState}
onToggle={this.toggleInlineStyle}
/>
<div className={className}>
<Editor
blockStyleFn={getBlockStyle}
customStyleMap={styleMap}
editorState={this.state.editorState}
keyBindingFn={this.keyBindingFn}
handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange}
placeholder="Tell a story..."
ref="editor"
spellCheck={true}
/>
</div>
</div>
);
}
}
// Custom overrides for "code" style.
const styleMap = {
CODE: {
backgroundColor: 'rgba(0, 0, 0, 0.05)',
fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
fontSize: 16,
padding: 2,
},
};
function getBlockStyle(block: ContentBlock) {
switch (block.getType()) {
case 'blockquote': return 'RichEditor-blockquote';
default: return null;
}
}
class StyleButton extends React.Component<{key: string, active: boolean, label: string, onToggle: (blockType: string) => void, style: string}, {}> {
constructor() {
super();
}
onToggle: (event: Event) => void = (event: Event) => {
event.preventDefault();
this.props.onToggle(this.props.style);
};
render(): React.ReactElement<{}> {
let className = 'RichEditor-styleButton';
if (this.props.active) {
className += ' RichEditor-activeButton';
}
return (
<span className={className} onMouseDown={e => this.onToggle(e as any)}>
{this.props.label}
</span>
);
}
}
const BLOCK_TYPES = [
{ label: 'H1', style: 'header-one' },
{ label: 'H2', style: 'header-two' },
{ label: 'H3', style: 'header-three' },
{ label: 'H4', style: 'header-four' },
{ label: 'H5', style: 'header-five' },
{ label: 'H6', style: 'header-six' },
{ label: 'Blockquote', style: 'blockquote' },
{ label: 'UL', style: 'unordered-list-item' },
{ label: 'OL', style: 'ordered-list-item' },
{ label: 'Code Block', style: 'code-block' },
];
const isHeaderBlock = (block: ContentBlock): boolean => {
switch (block.getType()) {
case 'header-one':
case 'header-two':
case 'header-three':
case 'header-four':
case 'header-five':
case 'header-six': {
return true;
}
default: return false;
}
}
const BlockStyleControls = (props: {editorState: EditorState, onToggle: (blockType: string) => void}) => {
const {editorState} = props;
const selection = editorState.getSelection();
const blockType = editorState
.getCurrentContent()
.getBlockForKey(selection.getStartKey())
.getType();
return (
<div className="RichEditor-controls">
{BLOCK_TYPES.map((type) =>
<StyleButton
key={type.label}
active={type.style === blockType}
label={type.label}
onToggle={props.onToggle}
style={type.style}
/>
) }
</div>
);
};
var INLINE_STYLES = [
{ label: 'Bold', style: 'BOLD' },
{ label: 'Italic', style: 'ITALIC' },
{ label: 'Underline', style: 'UNDERLINE' },
{ label: 'Monospace', style: 'CODE' },
];
const InlineStyleControls = (props: {editorState: EditorState, onToggle: (blockType: string) => void}) => {
var currentStyle = props.editorState.getCurrentInlineStyle();
return (
<div className="RichEditor-controls">
{INLINE_STYLES.map(type =>
<StyleButton
key={type.label}
active={currentStyle.has(type.style) }
label={type.label}
onToggle={props.onToggle}
style={type.style}
/>
) }
</div>
);
};
ReactDOM.render(
<RichEditorExample />,
document.getElementById('target')
);