Skip to content
Closed
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
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
},
"dependencies": {
"classnames": "^2.2.3",
"draft-js": "^0.4.0",
"draft-js-export-html": "^0.1.9",
"draft-js-export-markdown": "^0.1.3",
"draft-js": "^0.5.0",
"draft-js-export-html": "^0.1.13",
"draft-js-export-markdown": "^0.1.5",
"draft-js-import-html": "^0.1.5",
"draft-js-import-markdown": "^0.1.2",
"draft-js-tools": "^0.1.2",
"draft-js-utils": "^0.1.3",
"immutable": "^3.7.6"
},
"peerDependencies": {
Expand Down
39 changes: 37 additions & 2 deletions src/RichTextEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {CompositeDecorator, Editor, EditorState, Modifier, RichUtils} from 'draf
import getDefaultKeyBinding from 'draft-js/lib/getDefaultKeyBinding';
import changeBlockDepth from './lib/changeBlockDepth';
import changeBlockType from './lib/changeBlockType';
import getBlocksInSelection from './lib/getBlocksInSelection';
import insertBlockAfter from './lib/insertBlockAfter';
import isListItem from './lib/isListItem';
import isSoftNewlineEvent from 'draft-js/lib/isSoftNewlineEvent';
import EditorToolbar from './lib/EditorToolbar';
import EditorValue from './lib/EditorValue';
import LinkDecorator from './lib/LinkDecorator';
import ImageDecorator from './lib/ImageDecorator';
import cx from 'classnames';
import autobind from 'class-autobind';
import {EventEmitter} from 'events';
Expand All @@ -20,7 +22,7 @@ import './Draft.global.css';
// $FlowIssue - Flow doesn't understand CSS Modules
import styles from './RichTextEditor.css';

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

const MAX_LIST_DEPTH = 2;

Expand Down Expand Up @@ -219,9 +221,42 @@ export default class RichTextEditor extends Component<Props> {

_onChange(editorState: EditorState) {
let newValue = this.props.value.setEditorState(editorState);
let newEditorState = newValue.getEditorState();

this._handleInlineImageSelection(newEditorState);
this.props.onChange(newValue);
}

_handleInlineImageSelection(editorState: EditorState) {
let selection = editorState.getSelection();
let blocks = getBlocksInSelection(editorState);

const selectImage = (block, offset) => {
const imageKey = block.getEntityAt(offset);
Entity.mergeData(imageKey, {selected: true});
};

let isInMiddleBlock = (index) => index > 0 && index < blocks.size - 1;
let isWithinStartBlockSelection = (offset, index) => (
index === 0 && offset > selection.getStartOffset()
);
let isWithinEndBlockSelection = (offset, index) => (
index === blocks.size - 1 && offset < selection.getEndOffset()
);

blocks.toIndexedSeq().forEach((block, index) => {
ImageDecorator.strategy(
block,
(offset) => {
if (isWithinStartBlockSelection(offset, index) ||
isInMiddleBlock(index) ||
isWithinEndBlockSelection(offset, index)) {
selectImage(block, offset);
}
});
});
}

_focus() {
this.refs.editor.focus();
}
Expand All @@ -241,7 +276,7 @@ function getBlockStyle(block: ContentBlock): string {
}
}

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

function createEmptyValue(): EditorValue {
return EditorValue.createEmpty(decorator);
Expand Down
55 changes: 55 additions & 0 deletions src/lib/EditorToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default class EditorToolbar extends Component<Props> {
autobind(this);
this.state = {
showLinkInput: false,
showImageInput: false,
};
}

Expand All @@ -60,6 +61,7 @@ export default class EditorToolbar extends Component<Props> {
{this._renderInlineStyleButtons()}
{this._renderBlockTypeButtons()}
{this._renderLinkButtons()}
{this._renderImageButton()}
{this._renderBlockTypeDropdown()}
{this._renderUndoRedo()}
</div>
Expand Down Expand Up @@ -146,6 +148,27 @@ export default class EditorToolbar extends Component<Props> {
);
}

_renderImageButton(): React.Element {
const {editorState} = this.props;
let selection = editorState.getSelection();
let entity = this._getEntityAtCursor();
let hasSelection = !selection.isCollapsed();
let isCursorOnImage = (entity != null && entity.type === ENTITY_TYPE.IMAGE);
let shouldShowImageButton = hasSelection || isCursorOnImage;
return (
<ButtonGroup>
<PopoverIconButton
label="Image"
iconName="image"
isDisabled={!shouldShowImageButton}
showPopover={this.state.showImageInput}
onTogglePopover={this._toggleShowImageInput}
onSubmit={this._setImage}
/>
</ButtonGroup>
);
}

_renderUndoRedo(): React.Element {
let {editorState} = this.props;
let canUndo = editorState.getUndoStack().size !== 0;
Expand Down Expand Up @@ -200,6 +223,38 @@ export default class EditorToolbar extends Component<Props> {
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.setState({showImageInput: !isShowing});
}

_setImage(src: string) {
let {editorState} = this.props;
let selection = editorState.getSelection();
let entityKey = Entity.create(ENTITY_TYPE.IMAGE, 'IMMUTABLE', {src});
this.setState({showImageInput: false});
this.props.onChange(
RichUtils.toggleLink(editorState, selection, entityKey)
);
this._focusEditor();
}

_setLink(url: string) {
let {editorState} = this.props;
let selection = editorState.getSelection();
Expand Down
22 changes: 22 additions & 0 deletions src/lib/ImageDecorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* @flow */
import {Entity} from 'draft-js';
import {ENTITY_TYPE} from 'draft-js-tools';
import type {ContentBlock} from 'draft-js';
import ImageSpan from '../ui/ImageSpan';

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

function findImageEntities(contentBlock: ContentBlock, callback: EntityRangeCallback) {
contentBlock.findEntityRanges((character) => {
const entityKey = character.getEntity();
return (
entityKey != null &&
Entity.get(entityKey).getType() === ENTITY_TYPE.IMAGE
);
}, callback);
}

export default {
strategy: findImageEntities,
component: ImageSpan,
};
22 changes: 22 additions & 0 deletions src/lib/getBlocksInSelection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* @flow */
import {EditorState} from 'draft-js';
import {OrderedMap} from 'immutable';

export default function getBlocksInSelection(
editorState: EditorState,
): EditorState {
let contentState = editorState.getCurrentContent();
let blockMap = contentState.getBlockMap();
let selection = editorState.getSelection();
if (selection.isCollapsed()) {
return new OrderedMap();
}

let startKey = selection.getStartKey();
let endKey = selection.getEndKey();
if (startKey === endKey) {
return new OrderedMap({startKey: contentState.getBlockForKey(startKey)});
}
let blocksUntilEnd = blockMap.takeUntil((block, key) => key === endKey);
return blocksUntilEnd.skipUntil((block, key) => key === startKey);
}
6 changes: 6 additions & 0 deletions src/ui/IconButton.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions src/ui/ImageSpan.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.root {
color: transparent;
background-repeat: no-repeat;
display: inline-block;
text-align: right;
cursor: pointer;
}

.resize {
border: 1px dashed #78a300;
position: relative;
max-width: 100%;
display: inline-block;
line-height: 0;
top: -1px;
left: -1px,
}

.resizeHandle {
cursor: nwse-resize;
position: absolute;
z-index: 2;
line-height: 1;
bottom: -4px;
right: -5px;
border: 1px solid white;
background-color: #78a300;
width: 8px;
height: 8px;
}

.selected {
background-color: #B4D5FE;
}
104 changes: 104 additions & 0 deletions src/ui/ImageSpan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* @flow */

import React, {Component} from 'react';
import {Entity} from 'draft-js';
import cx from 'classnames';

// $FlowIssue - Flow doesn't understand CSS Modules
import styles from './ImageSpan.css';

type Props = {
children: React.Node;
entityKey: string;
className: ?string;
};

export default class ImageSpan extends Component {
constructor(props: Props) {
super(props);
const entity = Entity.get(this.props.entityKey);
const {width, height} = entity.getData();
this.state = {resizing: false, width, height};
}

_handleImageSpanClick = () => {
this.setState({resizing: !this.state.resizing});
}

_handleResize = (event, {size}) => {
const {width, height} = size;
this.setState({width, height});
Entity.mergeData(
this.props.entityKey,
{width, height}
);
}

componentDidMount() {
const {width, height} = this.state;
const entity = Entity.get(this.props.entityKey);
const image = new Image();
const {src} = entity.getData();
image.src = src;

const self = this;
image.onload = () => {
if (width == null || height == null) {
self.setState({width: image.width, height: image.height});
Entity.mergeData(
self.props.entityKey,
{
width: image.width,
height: image.height,
originalWidth: image.width,
originalHeight: image.height,
}
);
}
};
}

render() {
const {resizing, width, height} = this.state;
let {className} = this.props;
const entity = Entity.get(this.props.entityKey);
const {src, selected} = entity.getData();

const resizingStyles = resizing ? styles.resize : {};

className = cx(className, {
[styles.root]: true,
[styles.selected]: selected,
});
const imageStyle = {
//...resizingStyles,
backgroundImage: `url("${src}")`,
backgroundSize: `${width}px ${height}px`,
width,
height,
};

const imageSpan = (
<span
className={className}
style={imageStyle}
onClick={this._handleImageSpanClick}
>
{this.props.children}
</span>
);

// const view = resizing ?
// <Resizable width={width} height={height}
// minConstraints={[100, 100]} maxConstraints={[1000, 1000]}
// onResize={this._handleResize}
// lockAspectRatio
// styles={{handle: styles.resizeInlineImageHandle}}
// contentEditable={false}>
// {imageSpan}
// </Resizable> :
// imageSpan;

return imageSpan;
}
}