Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-rte",
"version": "0.16.5",
"version": "0.16.7",
"description": "React Rich Text Editor",
"main": "dist/react-rte.js",
"files": [
Expand All @@ -22,7 +22,7 @@
"babel-runtime": "^6.23.0",
"class-autobind": "^0.1.4",
"classnames": "^2.2.5",
"draft-js": ">=0.10.0",
"draft-js": "^0.11.7",
"draft-js-export-html": ">=0.6.0",
"draft-js-export-markdown": ">=0.3.0",
"draft-js-import-html": ">=0.4.0",
Expand Down
13 changes: 9 additions & 4 deletions src/EditorDemo.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/* @flow */
import React, {Component} from 'react';
import RichTextEditor, {createEmptyValue} from './RichTextEditor';
import RichTextEditor, {getColorStyles} from './RichTextEditor';
import {convertToRaw} from 'draft-js';
import autobind from 'class-autobind';

import {getTextAlignBlockMetadata, getTextAlignClassName, getTextAlignStyles} from './lib/blockStyleFunctions';
import {
getColorEntity,
getTextAlignBlockMetadata,
getTextAlignClassName,
getTextAlignStyles,
} from './lib/blockStyleFunctions';
import ButtonGroup from './ui/ButtonGroup';
import Dropdown from './ui/Dropdown';
import IconButton from './ui/IconButton';
Expand All @@ -26,7 +31,7 @@ export default class EditorDemo extends Component {
super(...arguments);
autobind(this);
this.state = {
value: createEmptyValue(),
value: RichTextEditor.createValueFromString("<p><span style='color: #0000ff'>Hello</span> <span style='color: #ff0000'>world</span></p>", 'html', {customInlineFn: getColorEntity}),
format: 'html',
readOnly: false,
};
Expand Down Expand Up @@ -113,7 +118,7 @@ export default class EditorDemo extends Component {
<textarea
className="source"
placeholder="Editor Source"
value={value.toString(format, {blockStyleFn: getTextAlignStyles})}
value={value.toString(format, {blockStyleFn: getTextAlignStyles, entityStyleFn: getColorStyles})}
onChange={this._onChangeSource}
/>
</div>
Expand Down
13 changes: 7 additions & 6 deletions src/RichTextEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React, {Component} from 'react';
import {CompositeDecorator, Editor, EditorState, Modifier, RichUtils, Entity} from 'draft-js';
import getDefaultKeyBinding from 'draft-js/lib/getDefaultKeyBinding';
import {getTextAlignBlockMetadata, getTextAlignClassName, getTextAlignStyles} from './lib/blockStyleFunctions';
import {getTextAlignBlockMetadata, getTextAlignClassName, getTextAlignStyles, getColorStyles} from './lib/blockStyleFunctions';
import changeBlockDepth from './lib/changeBlockDepth';
import changeBlockType from './lib/changeBlockType';
import getBlocksInSelection from './lib/getBlocksInSelection';
Expand All @@ -13,6 +13,8 @@ import EditorToolbar from './lib/EditorToolbar';
import EditorValue from './lib/EditorValue';
import LinkDecorator from './lib/LinkDecorator';
import ImageDecorator from './lib/ImageDecorator';
import ColorDecorator from './lib/ColorDecorator';
import BackgroundColorDecorator from './lib/BackgroundColorDecorator';
import composite from './lib/composite';
import cx from 'classnames';
import autobind from 'class-autobind';
Expand Down Expand Up @@ -189,10 +191,8 @@ export default class RichTextEditor extends Component {
if (this._handleReturnEmptyListItem()) {
return true;
}
if (this._handleReturnSpecialBlock()) {
return true;
}
return false;
return this._handleReturnSpecialBlock();

}

// `shift + return` should insert a soft newline.
Expand Down Expand Up @@ -359,7 +359,7 @@ function defaultBlockStyleFn(block: ContentBlock): string {
}
}

const decorator = new CompositeDecorator([LinkDecorator, ImageDecorator]);
const decorator = new CompositeDecorator([LinkDecorator, ImageDecorator, ColorDecorator, BackgroundColorDecorator]);

function createEmptyValue(): EditorValue {
return EditorValue.createEmpty(decorator);
Expand Down Expand Up @@ -388,6 +388,7 @@ export {
getTextAlignBlockMetadata,
getTextAlignClassName,
getTextAlignStyles,
getColorStyles,
ButtonGroup,
Button,
Dropdown,
Expand Down
35 changes: 35 additions & 0 deletions src/lib/BackgroundColorDecorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* @flow */
import React from 'react';

import type {ContentBlock, ContentState} from 'draft-js';

type Props = {
children: ReactNode,
entityKey: string,
contentState: ContentState,
};

type EntityRangeCallback = (start: number, end: number) => void;

function BackgroundColoredText(props: Props) {
const {backgroundColor} = props.contentState.getEntity(props.entityKey).getData();
return (
<span style={{backgroundColor}}>{props.children}</span>
);
}

function findBackgroundColoredTextEntities(contentBlock: ContentBlock, callback: EntityRangeCallback, contentState: ?ContentState) {
contentBlock.findEntityRanges((character) => {
const entityKey = character.getEntity();
if (entityKey != null) {
let entity = contentState ? contentState.getEntity(entityKey) : null;
return entity != null && entity.getType() === 'backgroundColor';
}
return false;
}, callback);
}

export default {
strategy: findBackgroundColoredTextEntities,
component: BackgroundColoredText,
};
35 changes: 35 additions & 0 deletions src/lib/ColorDecorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* @flow */
import React from 'react';

import type {ContentBlock, ContentState} from 'draft-js';

type Props = {
children: ReactNode,
entityKey: string,
contentState: ContentState,
};

type EntityRangeCallback = (start: number, end: number) => void;

function ColoredText(props: Props) {
const {color} = props.contentState.getEntity(props.entityKey).getData();
return (
<span style={{color}}>{props.children}</span>
);
}

function findColoredTextEntities(contentBlock: ContentBlock, callback: EntityRangeCallback, contentState: ?ContentState) {
contentBlock.findEntityRanges((character) => {
const entityKey = character.getEntity();
if (entityKey != null) {
let entity = contentState ? contentState.getEntity(entityKey) : null;
return entity != null && entity.getType() === 'color';
}
return false;
}, callback);
}

export default {
strategy: findColoredTextEntities,
component: ColoredText,
};
103 changes: 85 additions & 18 deletions src/lib/EditorToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ type Props = {
customControls: Array<CustomControl>;
rootStyle?: Object;
isOnBottom?: boolean;
customStyleMap?: Object;
};

type State = {
showLinkInput: boolean;
showImageInput: boolean;
showBackgroundColorInput: boolean;
showTextColorInput: boolean;
customControlState: {[key: string]: string};
};

Expand All @@ -52,6 +55,8 @@ export default class EditorToolbar extends Component {
this.state = {
showLinkInput: false,
showImageInput: false,
showTextColorInput: false,
showBackgroundColorInput: false,
customControlState: {},
};
}
Expand Down Expand Up @@ -96,6 +101,9 @@ export default class EditorToolbar extends Component {
case 'HISTORY_BUTTONS': {
return this._renderUndoRedo(groupName, toolbarConfig);
}
case 'COLOR_BUTTONS': {
return this._renderBlockColorButtons(groupName, toolbarConfig);
}
}
});
return (
Expand Down Expand Up @@ -219,6 +227,39 @@ export default class EditorToolbar extends Component {
);
}

_renderBlockColorButtons(name: string, toolbarConfig: ToolbarConfig) {
let {editorState} = this.props;
let content = editorState.getCurrentContent();
let selection = editorState.getSelection();
let blockKey = selection.getStartKey();
let block = content.getBlockForKey(blockKey);
let textColor = block.getData().get('textColor');

let entity = this._getEntityAtCursor();
let isCursorOnLink = (entity != null && entity.type === ENTITY_TYPE.LINK);
let defaultTextColorValue = (entity && isCursorOnLink) ? entity.getData().url : '';

const config = toolbarConfig.BLOCK_COLOR_BUTTONS || {};
const textColorConfig = config.textColor || {};
const textColorLabel = textColorConfig.label || 'Text color';

return (
<ButtonGroup key={name}>
<PopoverIconButton
{...toolbarConfig.extraProps}
label={textColorLabel}
iconName="textColor"
isActive={textColor === 'TEXT_COLOR'}
showPopover={this.state.showTextColorInput}
onTogglePopover={this._toggleShowColorInput('showTextColorInput')}
defaultValue={defaultTextColorValue}
onSubmit={this._setColor}
inputtype="color"
/>
</ButtonGroup>
);
}

_renderLinkButtons(name: string, toolbarConfig: ToolbarConfig) {
let {editorState} = this.props;
let selection = editorState.getSelection();
Expand Down Expand Up @@ -320,9 +361,16 @@ export default class EditorToolbar extends Component {
}
}

_toggleShowLinkInput(event: ?Object) {
let isShowing = this.state.showLinkInput;
// If this is a hide request, decide if we should focus the editor.
_toggleShowColorInput(inputType: 'showTextColorInput' | 'showBackgroundColorInput') {
return (event: ?Object) => {
let isShowing = this.state[inputType];
// If this is a hide request, decide if we should focus the editor.
this._handlePopOverShow(isShowing, event);
this.setState({[inputType]: !isShowing});
};
}

_handlePopOverShow(isShowing, event) {
if (isShowing) {
let shouldFocusEditor = true;
if (event && event.type === 'click') {
Expand All @@ -338,27 +386,19 @@ export default class EditorToolbar extends Component {
this.props.focusEditor();
}
}
}

_toggleShowLinkInput(event: ?Object) {
let isShowing = this.state.showLinkInput;
// If this is a hide request, decide if we should focus the editor.
this._handlePopOverShow(isShowing, event);
this.setState({showLinkInput: !isShowing});
}

_toggleShowImageInput(event: ?Object) {
let isShowing = this.state.showImageInput;
// If this is a hide request, decide if we should focus the editor.
if (isShowing) {
let shouldFocusEditor = true;
if (event && event.type === 'click') {
// TODO: Use a better way to get the editor root node.
let editorRoot = ReactDOM.findDOMNode(this).parentNode;
let {activeElement} = document;
let wasClickAway = (activeElement == null || activeElement === document.body);
if (!wasClickAway && !editorRoot.contains(activeElement)) {
shouldFocusEditor = false;
}
}
if (shouldFocusEditor) {
this.props.focusEditor();
}
}
this._handlePopOverShow(isShowing, event);
this.setState({showImageInput: !isShowing});
}

Expand All @@ -376,6 +416,33 @@ export default class EditorToolbar extends Component {
this._focusEditor();
}

_setColor(toggledColor: string) {
this._setColorWithType(toggledColor, 'color', 'showTextColorInput');
}

_setBackgroundColor(toggledColor: string) {
this._setColorWithType(toggledColor, 'backgroundColor', 'showBackgroundColorInput');
}

_setColorWithType(toggledColor: string, type: string, inputKey: string) {
let {editorState} = this.props;
let contentState = editorState.getCurrentContent();
let selection = editorState.getSelection();

let origSelection = selection;

this.setState({[inputKey]: false});
contentState = contentState.createEntity(type, 'MUTABLE', {[type]: toggledColor});
let entityKey = contentState.getLastCreatedEntityKey();

editorState = EditorState.push(editorState, contentState);
editorState = RichUtils.toggleLink(editorState, selection, entityKey);
editorState = EditorState.acceptSelection(editorState, origSelection);

this.props.onChange(editorState);
this._focusEditor();
}

_setLink(url: string, checkOptions: {[key: string]: boolean}) {
let {editorState} = this.props;
let contentState = editorState.getCurrentContent();
Expand Down
11 changes: 9 additions & 2 deletions src/lib/EditorToolbarConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type CustomControlList = Array<CustomControl>;

export type StyleConfigList = Array<StyleConfig>;

export type GroupName = 'INLINE_STYLE_BUTTONS' | 'BLOCK_ALIGNMENT_BUTTONS' | 'BLOCK_TYPE_BUTTONS' | 'LINK_BUTTONS' | 'BLOCK_TYPE_DROPDOWN' | 'HISTORY_BUTTONS' | 'IMAGE_BUTTON';
export type GroupName = 'INLINE_STYLE_BUTTONS' | 'BLOCK_ALIGNMENT_BUTTONS' | 'BLOCK_TYPE_BUTTONS' | 'LINK_BUTTONS' | 'BLOCK_TYPE_DROPDOWN' | 'HISTORY_BUTTONS' | 'IMAGE_BUTTON' | 'COLOR_BUTTONS';

export type ToolbarConfig = {
display: Array<GroupName>;
Expand All @@ -24,6 +24,7 @@ export type ToolbarConfig = {
BLOCK_ALIGNMENT_BUTTONS: StyleConfigList;
BLOCK_TYPE_DROPDOWN: StyleConfigList;
BLOCK_TYPE_BUTTONS: StyleConfigList;
BLOCK_COLOR_BUTTONS: StyleConfigList;
};

export const INLINE_STYLE_BUTTONS: StyleConfigList = [
Expand Down Expand Up @@ -54,12 +55,18 @@ export const BLOCK_TYPE_BUTTONS: StyleConfigList = [
{label: 'Blockquote', style: 'blockquote'},
];

export const BLOCK_COLOR_BUTTONS: StyleConfigList = [
{label: 'Text color', style: 'TEXT_COLOR'},
{label: 'Background color', style: 'BACKGROUND_COLOR'},
];

let EditorToolbarConfig: ToolbarConfig = {
display: ['INLINE_STYLE_BUTTONS', 'BLOCK_ALIGNMENT_BUTTONS', 'BLOCK_TYPE_BUTTONS', 'LINK_BUTTONS', 'IMAGE_BUTTON', 'BLOCK_TYPE_DROPDOWN', 'HISTORY_BUTTONS'],
display: ['INLINE_STYLE_BUTTONS', 'BLOCK_ALIGNMENT_BUTTONS', 'COLOR_BUTTONS', 'BLOCK_TYPE_BUTTONS', 'LINK_BUTTONS', 'IMAGE_BUTTON', 'BLOCK_TYPE_DROPDOWN', 'HISTORY_BUTTONS'],
INLINE_STYLE_BUTTONS,
BLOCK_ALIGNMENT_BUTTONS,
BLOCK_TYPE_DROPDOWN,
BLOCK_TYPE_BUTTONS,
BLOCK_COLOR_BUTTONS,
};

export default EditorToolbarConfig;
Loading