From 09e9ccf3e2d4bd630dda93d364690bfc1e8c4905 Mon Sep 17 00:00:00 2001 From: Greg Ziegan Date: Sun, 10 Apr 2016 01:51:59 -0700 Subject: [PATCH 1/4] should work when the micro-dependencies update --- src/RichTextEditor.js | 26 ++++- src/lib/ImageDecorator.js | 143 +++++++++++++++++++++++++++ src/lib/Resizable.js | 197 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 src/lib/ImageDecorator.js create mode 100644 src/lib/Resizable.js diff --git a/src/RichTextEditor.js b/src/RichTextEditor.js index b39c7102..12ef08fa 100644 --- a/src/RichTextEditor.js +++ b/src/RichTextEditor.js @@ -10,6 +10,7 @@ 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'; @@ -20,7 +21,7 @@ import './Draft.global.css'; // $FlowIssue - Flow doesn't understand CSS Modules import styles from './RichTextEditor.css'; -import type {ContentBlock} from 'draft-js'; +import type {ContentBlock, Entity} from 'draft-js'; const MAX_LIST_DEPTH = 2; @@ -219,9 +220,30 @@ export default class RichTextEditor extends Component { _onChange(editorState: EditorState) { let newValue = this.props.value.setEditorState(editorState); + let newEditorState = newValue.getEditorState(); + let selection = newEditorState.getSelection(); + let contentState = newEditorState.getCurrentContent(); + let startBlock = contentState.getBlockForKey(selection.getStartKey()); + let endBlock = contentState.getBlockForKey(selection.getEndKey()); + if (startBlock && startBlock === endBlock) { + this._checkForImageSelection(selection, startBlock); + } this.props.onChange(newValue); } + _checkForImageSelection(selection, block) { + ImageDecorator.strategy( + block, + (start, end) => { + if (start === selection.getStartOffset() && + end === selection.getEndOffset()) { + console.log('image selected') + this.setState({transparentSelection: true}); + } + } + ); + } + _focus() { this.refs.editor.focus(); } @@ -241,7 +263,7 @@ function getBlockStyle(block: ContentBlock): string { } } -const decorator = new CompositeDecorator([LinkDecorator]); +const decorator = new CompositeDecorator([LinkDecorator, ImageDecorator]); function createEmptyValue(): EditorValue { return EditorValue.createEmpty(decorator); diff --git a/src/lib/ImageDecorator.js b/src/lib/ImageDecorator.js new file mode 100644 index 00000000..658b1ef7 --- /dev/null +++ b/src/lib/ImageDecorator.js @@ -0,0 +1,143 @@ +/* @flow */ +import React, {Component} from 'react'; +import {Entity} from 'draft-js'; +import {ENTITY_TYPE} from 'draft-js-tools'; +import Resizable from './Resizable'; + +import type {ContentBlock} from 'draft-js'; + +type Props = { + children: React.Node, + entityKey: string, +}; + +type EntityRangeCallback = (start: number, end: number) => void; + +const styles = { + inlineImage: { + color: 'transparent', + backgroundRepeat: 'no-repeat', + display: 'inline-block', + }, + resizeInlineImage: { + border: '1px dashed #78a300', + position: 'relative', + maxWidth: '100%', + display: 'inline-block', + lineHeight: 0, + top: '-1px', + left: '-1px', + }, + resizeInlineImageHandle: { + cursor: 'nwse-resize', + position: 'absolute', + zIndex: 2, + lineHeight: 1, + bottom: '-4px', + right: '-5px', + border: '1px solid white', + backgroundColor: '#78a300', + width: '8px', + height: '8px', + }, +}; + +class ImageSpan extends Component { + constructor(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; + const entity = Entity.get(this.props.entityKey); + const {src} = entity.getData(); + + const resizingStyles = resizing ? styles.resizeInlineImage : {}; + + const imageStyle = { + ...styles.inlineImage, + ...resizingStyles, + backgroundImage: `url("${src}")`, + backgroundSize: `${width}px ${height}px`, + width, + height, + }; + + const imageSpan = ( + + {this.props.children} + + ); + + const view = resizing ? + + {imageSpan} + : + imageSpan; + + return view; + } +} + +function findImageEntities(contentBlock: ContentBlock, callback: EntityRangeCallback) { + contentBlock.findEntityRanges((character) => { + const entityKey = character.getEntity(); + return ( + entityKey != null && + Entity.get(entityKey).getType() === 'IMAGE' + ); + }, callback); +} + +export default { + strategy: findImageEntities, + component: ImageSpan, +}; diff --git a/src/lib/Resizable.js b/src/lib/Resizable.js new file mode 100644 index 00000000..90e5b011 --- /dev/null +++ b/src/lib/Resizable.js @@ -0,0 +1,197 @@ +// @flow +/* +Taken from https://github.com/STRML/react-resizable/ +Currently having an npm install issue +*/ +import {default as React, PropTypes} from 'react'; +import {DraggableCore} from 'react-draggable'; +import cloneElement from './cloneElement'; + +type Position = { + deltaX: number, + deltaY: number +}; +type State = { + resizing: boolean, + width: number, height: number, + slackW: number, slackH: number +}; +type DragCallbackData = { + node: HTMLElement, + position: Position +}; + +export default class Resizable extends React.Component { + + static propTypes = { + // + // Required Props + // + + // Require that one and only one child be present. + children: PropTypes.element.isRequired, + + // Initial w/h + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + + styles: PropTypes.object.isRequired, + + // + // Optional props + // + + // If you change this, be sure to update your css + handleSize: PropTypes.array, + + // If true, will only allow width/height to move in lockstep + lockAspectRatio: PropTypes.bool, + + // Min/max size + minConstraints: PropTypes.arrayOf(PropTypes.number), + maxConstraints: PropTypes.arrayOf(PropTypes.number), + + // Callbacks + onResizeStop: PropTypes.func, + onResizeStart: PropTypes.func, + onResize: PropTypes.func, + + // These will be passed wholesale to react-draggable's DraggableCore + draggableOpts: PropTypes.object + }; + + static defaultProps = { + handleSize: [20, 20], + lockAspectRatio: false, + minConstraints: [20, 20], + maxConstraints: [Infinity, Infinity] + }; + + state: State = { + resizing: false, + width: this.props.width, height: this.props.height, + slackW: 0, slackH: 0 + }; + + componentWillReceiveProps(nextProps: Object) { + // If parent changes height/width, set that in our state. + if (!this.state.resizing && + (nextProps.width !== this.props.width || nextProps.height !== this.props.height)) { + this.setState({ + width: nextProps.width, + height: nextProps.height + }); + } + } + + lockAspectRatio(width: number, height: number, aspectRatio: number): [number, number] { + height = width / aspectRatio; + width = height * aspectRatio; + return [width, height]; + } + + // If you do this, be careful of constraints + runConstraints(width: number, height: number): [number, number] { + let [min, max] = [this.props.minConstraints, this.props.maxConstraints]; + + if (this.props.lockAspectRatio) { + const ratio = this.state.width / this.state.height; + height = width / ratio; + width = height * ratio; + } + + if (!min && !max) return [width, height]; + + let [oldW, oldH] = [width, height]; + + // Add slack to the values used to calculate bound position. This will ensure that if + // we start removing slack, the element won't react to it right away until it's been + // completely removed. + let {slackW, slackH} = this.state; + width += slackW; + height += slackH; + + if (min) { + width = Math.max(min[0], width); + height = Math.max(min[1], height); + } + if (max) { + width = Math.min(max[0], width); + height = Math.min(max[1], height); + } + + // If the numbers changed, we must have introduced some slack. Record it for the next iteration. + slackW += (oldW - width); + slackH += (oldH - height); + if (slackW !== this.state.slackW || slackH !== this.state.slackH) { + this.setState({slackW, slackH}); + } + + return [width, height]; + } + + /** + * Wrapper around drag events to provide more useful data. + * + * @param {String} handlerName Handler name to wrap. + * @return {Function} Handler function. + */ + resizeHandler(handlerName: string): Function { + return (e, {node, position}: DragCallbackData) => { + const {deltaX, deltaY} = position; + let width = this.state.width + deltaX, height = this.state.height + deltaY; + + // Early return if no change + let widthChanged = width !== this.state.width, heightChanged = height !== this.state.height; + if (handlerName === 'onResize' && !widthChanged && !heightChanged) return; + + [width, height] = this.runConstraints(width, height); + + // Set the appropriate state for this handler. + let newState = {}; + if (handlerName === 'onResizeStart') { + newState.resizing = true; + } else if (handlerName === 'onResizeStop') { + newState.resizing = false; + } else { + // Early return if no change after constraints + if (width === this.state.width && height === this.state.height) return; + newState.width = width; + newState.height = height; + } + + this.setState(newState, () => { + this.props[handlerName] && this.props[handlerName](e, {node, size: {width, height}}); + }); + + }; + } + + render(): React.Element { + let {width, height, ...p} = this.props; + let className = p.className ? + `${p.className} react-resizable`: + 'react-resizable'; + + // What we're doing here is getting the child of this element, and cloning it with this element's props. + // We are then defining its children as: + // Its original children (resizable's child's children), and + // A draggable handle. + return cloneElement(p.children, { + ...p, + className, + children: [ + p.children.props.children, + + + + ] + }); + } +} From f8834ad5861f6dd4f6b75b4bc1f532d9e4678d70 Mon Sep 17 00:00:00 2001 From: Greg Ziegan Date: Sun, 10 Apr 2016 01:56:03 -0700 Subject: [PATCH 2/4] oh ya there was a toolbar button --- src/lib/EditorToolbar.js | 55 ++++++++++++++++++++++++++++++++++++++++ src/ui/IconButton.css | 6 +++++ 2 files changed, 61 insertions(+) diff --git a/src/lib/EditorToolbar.js b/src/lib/EditorToolbar.js index b88572ea..890115d6 100644 --- a/src/lib/EditorToolbar.js +++ b/src/lib/EditorToolbar.js @@ -41,6 +41,7 @@ export default class EditorToolbar extends Component { autobind(this); this.state = { showLinkInput: false, + showImageInput: false, }; } @@ -60,6 +61,7 @@ export default class EditorToolbar extends Component { {this._renderInlineStyleButtons()} {this._renderBlockTypeButtons()} {this._renderLinkButtons()} + {this._renderImageButton()} {this._renderBlockTypeDropdown()} {this._renderUndoRedo()} @@ -146,6 +148,27 @@ export default class EditorToolbar extends Component { ); } + _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 === 'IMAGE'); + let shouldShowImageButton = hasSelection || isCursorOnImage; + return ( + + + + ); + } + _renderUndoRedo(): React.Element { let {editorState} = this.props; let canUndo = editorState.getUndoStack().size !== 0; @@ -200,6 +223,38 @@ export default class EditorToolbar extends Component { 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('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(); diff --git a/src/ui/IconButton.css b/src/ui/IconButton.css index 09e5c1a7..061e7c11 100644 --- a/src/ui/IconButton.css +++ b/src/ui/IconButton.css @@ -74,6 +74,12 @@ background-size: 14px; } +.icon-image { + composes: icon; + background-image: url("data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4IiB2aWV3Qm94PSIwIDAgNTMzLjMzMyA1MzMuMzM0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MzMuMzMzIDUzMy4zMzQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8cGF0aCBkPSJNNDY2LjY2NywxMDBoLTQwMHYzMzMuMzMzaDQwMFYxMDB6IE01MzMuMzMzLDMzLjMzM0w1MzMuMzMzLDMzLjMzM1Y1MDBIMFYzMy4zMzNINTMzLjMzM3ogTTQzMy4zMzMsNDAwSDEwMHYtNjYuNjY3ICAgbDEwMC0xNjYuNjY3bDEzNi45NzksMTY2LjY2N2w5Ni4zNTQtNjYuNjY2VjMwMFY0MDB6IE0zMzMuMzMzLDE4My4zMzNjMCwyNy42MTQsMjIuMzg2LDUwLDUwLDUwczUwLTIyLjM4Niw1MC01MHMtMjIuMzg2LTUwLTUwLTUwICAgUzMzMy4zMzMsMTU1LjcxOSwzMzMuMzMzLDE4My4zMzN6IiBmaWxsPSIjMDAwMDAwIi8+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg=="); + background-size: 14px; +} + .icon-cancel { composes: icon; background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMjMuNzggMTkuMjhMMTYuNSAxMmw3LjI4LTcuMjhhLjc0OC43NDggMCAwIDAgMC0xLjA2TDIwLjM0LjIxOGEuNzUuNzUgMCAwIDAtMS4wNi0uMDAyTDEyIDcuNDk4IDQuNzE3LjIyYS43NDguNzQ4IDAgMCAwLTEuMDYgMEwuMjE3IDMuNjZhLjc1Ljc1IDAgMCAwIDAgMS4wNkw3LjQ5NyAxMmwtNy4yOCA3LjI4YS43NDguNzQ4IDAgMCAwIDAgMS4wNmwzLjQ0IDMuNDRhLjc1Ljc1IDAgMCAwIDEuMDYuMDAybDcuMjgtNy4yOCA3LjI4MiA3LjI4Yy4wNzguMDc4LjE3LjEzNS4yNjguMTcuMjY3LjEuNTguMDQ0Ljc5My0uMTdsMy40NC0zLjQ0YS43NS43NSAwIDAgMCAwLTEuMDZ6Ii8+PC9zdmc+"); From a1001e070ce700171da4822d2aef04817865d695 Mon Sep 17 00:00:00 2001 From: Greg Ziegan Date: Tue, 26 Apr 2016 21:24:29 -0700 Subject: [PATCH 3/4] make things work except resizer --- src/RichTextEditor.js | 49 +++++--- src/lib/ImageDecorator.js | 123 +------------------- src/lib/Resizable.js | 197 -------------------------------- src/lib/getBlocksInSelection.js | 22 ++++ src/ui/ImageSpan.css | 34 ++++++ src/ui/ImageSpan.js | 104 +++++++++++++++++ 6 files changed, 192 insertions(+), 337 deletions(-) delete mode 100644 src/lib/Resizable.js create mode 100644 src/lib/getBlocksInSelection.js create mode 100644 src/ui/ImageSpan.css create mode 100644 src/ui/ImageSpan.js diff --git a/src/RichTextEditor.js b/src/RichTextEditor.js index 12ef08fa..fae6f3d1 100644 --- a/src/RichTextEditor.js +++ b/src/RichTextEditor.js @@ -4,6 +4,7 @@ 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'; @@ -21,7 +22,7 @@ import './Draft.global.css'; // $FlowIssue - Flow doesn't understand CSS Modules import styles from './RichTextEditor.css'; -import type {ContentBlock, Entity} from 'draft-js'; +import {ContentBlock, Entity} from 'draft-js'; const MAX_LIST_DEPTH = 2; @@ -221,27 +222,39 @@ export default class RichTextEditor extends Component { _onChange(editorState: EditorState) { let newValue = this.props.value.setEditorState(editorState); let newEditorState = newValue.getEditorState(); - let selection = newEditorState.getSelection(); - let contentState = newEditorState.getCurrentContent(); - let startBlock = contentState.getBlockForKey(selection.getStartKey()); - let endBlock = contentState.getBlockForKey(selection.getEndKey()); - if (startBlock && startBlock === endBlock) { - this._checkForImageSelection(selection, startBlock); - } + + this._handleInlineImageSelection(newEditorState); this.props.onChange(newValue); } - _checkForImageSelection(selection, block) { - ImageDecorator.strategy( - block, - (start, end) => { - if (start === selection.getStartOffset() && - end === selection.getEndOffset()) { - console.log('image selected') - this.setState({transparentSelection: true}); - } - } + _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() { diff --git a/src/lib/ImageDecorator.js b/src/lib/ImageDecorator.js index 658b1ef7..7796f234 100644 --- a/src/lib/ImageDecorator.js +++ b/src/lib/ImageDecorator.js @@ -1,132 +1,11 @@ /* @flow */ -import React, {Component} from 'react'; import {Entity} from 'draft-js'; import {ENTITY_TYPE} from 'draft-js-tools'; -import Resizable from './Resizable'; - import type {ContentBlock} from 'draft-js'; - -type Props = { - children: React.Node, - entityKey: string, -}; +import ImageSpan from '../ui/ImageSpan'; type EntityRangeCallback = (start: number, end: number) => void; -const styles = { - inlineImage: { - color: 'transparent', - backgroundRepeat: 'no-repeat', - display: 'inline-block', - }, - resizeInlineImage: { - border: '1px dashed #78a300', - position: 'relative', - maxWidth: '100%', - display: 'inline-block', - lineHeight: 0, - top: '-1px', - left: '-1px', - }, - resizeInlineImageHandle: { - cursor: 'nwse-resize', - position: 'absolute', - zIndex: 2, - lineHeight: 1, - bottom: '-4px', - right: '-5px', - border: '1px solid white', - backgroundColor: '#78a300', - width: '8px', - height: '8px', - }, -}; - -class ImageSpan extends Component { - constructor(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; - const entity = Entity.get(this.props.entityKey); - const {src} = entity.getData(); - - const resizingStyles = resizing ? styles.resizeInlineImage : {}; - - const imageStyle = { - ...styles.inlineImage, - ...resizingStyles, - backgroundImage: `url("${src}")`, - backgroundSize: `${width}px ${height}px`, - width, - height, - }; - - const imageSpan = ( - - {this.props.children} - - ); - - const view = resizing ? - - {imageSpan} - : - imageSpan; - - return view; - } -} - function findImageEntities(contentBlock: ContentBlock, callback: EntityRangeCallback) { contentBlock.findEntityRanges((character) => { const entityKey = character.getEntity(); diff --git a/src/lib/Resizable.js b/src/lib/Resizable.js deleted file mode 100644 index 90e5b011..00000000 --- a/src/lib/Resizable.js +++ /dev/null @@ -1,197 +0,0 @@ -// @flow -/* -Taken from https://github.com/STRML/react-resizable/ -Currently having an npm install issue -*/ -import {default as React, PropTypes} from 'react'; -import {DraggableCore} from 'react-draggable'; -import cloneElement from './cloneElement'; - -type Position = { - deltaX: number, - deltaY: number -}; -type State = { - resizing: boolean, - width: number, height: number, - slackW: number, slackH: number -}; -type DragCallbackData = { - node: HTMLElement, - position: Position -}; - -export default class Resizable extends React.Component { - - static propTypes = { - // - // Required Props - // - - // Require that one and only one child be present. - children: PropTypes.element.isRequired, - - // Initial w/h - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - - styles: PropTypes.object.isRequired, - - // - // Optional props - // - - // If you change this, be sure to update your css - handleSize: PropTypes.array, - - // If true, will only allow width/height to move in lockstep - lockAspectRatio: PropTypes.bool, - - // Min/max size - minConstraints: PropTypes.arrayOf(PropTypes.number), - maxConstraints: PropTypes.arrayOf(PropTypes.number), - - // Callbacks - onResizeStop: PropTypes.func, - onResizeStart: PropTypes.func, - onResize: PropTypes.func, - - // These will be passed wholesale to react-draggable's DraggableCore - draggableOpts: PropTypes.object - }; - - static defaultProps = { - handleSize: [20, 20], - lockAspectRatio: false, - minConstraints: [20, 20], - maxConstraints: [Infinity, Infinity] - }; - - state: State = { - resizing: false, - width: this.props.width, height: this.props.height, - slackW: 0, slackH: 0 - }; - - componentWillReceiveProps(nextProps: Object) { - // If parent changes height/width, set that in our state. - if (!this.state.resizing && - (nextProps.width !== this.props.width || nextProps.height !== this.props.height)) { - this.setState({ - width: nextProps.width, - height: nextProps.height - }); - } - } - - lockAspectRatio(width: number, height: number, aspectRatio: number): [number, number] { - height = width / aspectRatio; - width = height * aspectRatio; - return [width, height]; - } - - // If you do this, be careful of constraints - runConstraints(width: number, height: number): [number, number] { - let [min, max] = [this.props.minConstraints, this.props.maxConstraints]; - - if (this.props.lockAspectRatio) { - const ratio = this.state.width / this.state.height; - height = width / ratio; - width = height * ratio; - } - - if (!min && !max) return [width, height]; - - let [oldW, oldH] = [width, height]; - - // Add slack to the values used to calculate bound position. This will ensure that if - // we start removing slack, the element won't react to it right away until it's been - // completely removed. - let {slackW, slackH} = this.state; - width += slackW; - height += slackH; - - if (min) { - width = Math.max(min[0], width); - height = Math.max(min[1], height); - } - if (max) { - width = Math.min(max[0], width); - height = Math.min(max[1], height); - } - - // If the numbers changed, we must have introduced some slack. Record it for the next iteration. - slackW += (oldW - width); - slackH += (oldH - height); - if (slackW !== this.state.slackW || slackH !== this.state.slackH) { - this.setState({slackW, slackH}); - } - - return [width, height]; - } - - /** - * Wrapper around drag events to provide more useful data. - * - * @param {String} handlerName Handler name to wrap. - * @return {Function} Handler function. - */ - resizeHandler(handlerName: string): Function { - return (e, {node, position}: DragCallbackData) => { - const {deltaX, deltaY} = position; - let width = this.state.width + deltaX, height = this.state.height + deltaY; - - // Early return if no change - let widthChanged = width !== this.state.width, heightChanged = height !== this.state.height; - if (handlerName === 'onResize' && !widthChanged && !heightChanged) return; - - [width, height] = this.runConstraints(width, height); - - // Set the appropriate state for this handler. - let newState = {}; - if (handlerName === 'onResizeStart') { - newState.resizing = true; - } else if (handlerName === 'onResizeStop') { - newState.resizing = false; - } else { - // Early return if no change after constraints - if (width === this.state.width && height === this.state.height) return; - newState.width = width; - newState.height = height; - } - - this.setState(newState, () => { - this.props[handlerName] && this.props[handlerName](e, {node, size: {width, height}}); - }); - - }; - } - - render(): React.Element { - let {width, height, ...p} = this.props; - let className = p.className ? - `${p.className} react-resizable`: - 'react-resizable'; - - // What we're doing here is getting the child of this element, and cloning it with this element's props. - // We are then defining its children as: - // Its original children (resizable's child's children), and - // A draggable handle. - return cloneElement(p.children, { - ...p, - className, - children: [ - p.children.props.children, - - - - ] - }); - } -} diff --git a/src/lib/getBlocksInSelection.js b/src/lib/getBlocksInSelection.js new file mode 100644 index 00000000..9b5503fd --- /dev/null +++ b/src/lib/getBlocksInSelection.js @@ -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); +} diff --git a/src/ui/ImageSpan.css b/src/ui/ImageSpan.css new file mode 100644 index 00000000..d5a5d1f0 --- /dev/null +++ b/src/ui/ImageSpan.css @@ -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; +} diff --git a/src/ui/ImageSpan.js b/src/ui/ImageSpan.js new file mode 100644 index 00000000..721e27c4 --- /dev/null +++ b/src/ui/ImageSpan.js @@ -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 = ( + + {this.props.children} + + ); + + // const view = resizing ? + // + // {imageSpan} + // : + // imageSpan; + + return imageSpan; + } +} From 7c9d51983722d1ed442f360692f5f98fcd8206b3 Mon Sep 17 00:00:00 2001 From: Greg Ziegan Date: Tue, 26 Apr 2016 21:32:39 -0700 Subject: [PATCH 4/4] update draft-js-utils* deps; use ENTITY_TYPE.IMAGE --- package.json | 8 ++++---- src/lib/EditorToolbar.js | 4 ++-- src/lib/ImageDecorator.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1bd5bcc4..93041dec 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/lib/EditorToolbar.js b/src/lib/EditorToolbar.js index 890115d6..427b7e59 100644 --- a/src/lib/EditorToolbar.js +++ b/src/lib/EditorToolbar.js @@ -153,7 +153,7 @@ export default class EditorToolbar extends Component { let selection = editorState.getSelection(); let entity = this._getEntityAtCursor(); let hasSelection = !selection.isCollapsed(); - let isCursorOnImage = (entity != null && entity.type === 'IMAGE'); + let isCursorOnImage = (entity != null && entity.type === ENTITY_TYPE.IMAGE); let shouldShowImageButton = hasSelection || isCursorOnImage; return ( @@ -247,7 +247,7 @@ export default class EditorToolbar extends Component { _setImage(src: string) { let {editorState} = this.props; let selection = editorState.getSelection(); - let entityKey = Entity.create('IMAGE', 'IMMUTABLE', {src}); + let entityKey = Entity.create(ENTITY_TYPE.IMAGE, 'IMMUTABLE', {src}); this.setState({showImageInput: false}); this.props.onChange( RichUtils.toggleLink(editorState, selection, entityKey) diff --git a/src/lib/ImageDecorator.js b/src/lib/ImageDecorator.js index 7796f234..981ceb54 100644 --- a/src/lib/ImageDecorator.js +++ b/src/lib/ImageDecorator.js @@ -11,7 +11,7 @@ function findImageEntities(contentBlock: ContentBlock, callback: EntityRangeCall const entityKey = character.getEntity(); return ( entityKey != null && - Entity.get(entityKey).getType() === 'IMAGE' + Entity.get(entityKey).getType() === ENTITY_TYPE.IMAGE ); }, callback); }