diff --git a/examples/auto-markdown/index.js b/examples/auto-markdown/index.js index afca9c65b..d50dae896 100644 --- a/examples/auto-markdown/index.js +++ b/examples/auto-markdown/index.js @@ -1,5 +1,5 @@ -import { Editor, Raw, wrap } from '../..' +import { Editor, Raw } from '../..' import React from 'react' import keycode from 'keycode' import initialState from './state.json' diff --git a/examples/images/index.js b/examples/images/index.js index 0045032b0..e11372364 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -1,5 +1,5 @@ -import { Editor, Raw, wrap } from '../..' +import { Editor, Raw, Void } from '../..' import React from 'react' import ReactDOM from 'react-dom' import initialState from './state.json' @@ -13,11 +13,15 @@ import isUrl from 'is-url' */ const NODES = { - image: wrap()((props) => { + image: (props) => { const { node, state } = props const src = node.data.get('src') - return - }) + return ( + + + + ) + } } /** @@ -83,6 +87,7 @@ class Images extends React.Component { renderNode={this.renderNode} onChange={this.onChange} onDocumentChange={this.onDocumentChange} + onDrop={this.onDrop} onPaste={this.onPaste} /> @@ -153,6 +158,25 @@ class Images extends React.Component { this.onChange(state) } + /** + * On drop, insert the image wherever it is dropped. + * + * @param {Event} e + * @param {Object} drop + * @param {State} state + * @return {State} + */ + + onDrop = (e, drop, state) => { + if (drop.type != 'node') return + return state + .transform() + .removeNodeByKey(drop.node.key) + .moveTo(drop.target) + .insertBlock(drop.node) + .apply() + } + /** * On paste, if the pasted content is an image URL, insert it. * diff --git a/examples/index.js b/examples/index.js index d08b70a74..b8144bb57 100644 --- a/examples/index.js +++ b/examples/index.js @@ -112,7 +112,19 @@ class App extends React.Component { const router = ( + + + + + + + + + + + + ) diff --git a/lib/components/content.js b/lib/components/content.js index 4db502178..8c6a21bb8 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -1,11 +1,11 @@ import Base64 from '../serializers/base-64' import Key from '../utils/key' +import Node from './node' import OffsetKey from '../utils/offset-key' import Raw from '../serializers/raw' import React from 'react' import Selection from '../models/selection' -import Text from './text' import TYPES from '../utils/types' import includes from 'lodash/includes' import keycode from 'keycode' @@ -341,7 +341,16 @@ class Content extends React.Component { // Resolve the point where the drop occured. const { x, y } = e.nativeEvent - const range = window.document.caretRangeFromPoint(x, y) + let range + + // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25) + if (window.document.caretRangeFromPoint) { + range = window.document.caretRangeFromPoint(x, y) + } else { + range = window.document.createRange() + range.setStart(e.nativeEvent.rangeParent, e.nativeEvent.rangeOffset) + } + const startNode = range.startContainer const startOffset = range.startOffset const point = OffsetKey.findPoint(startNode, startOffset, state) @@ -359,7 +368,7 @@ class Content extends React.Component { // Handle Slate fragments. if (includes(types, TYPES.FRAGMENT)) { const encoded = data.getData(TYPES.FRAGMENT) - const fragment = Base64.deserializeDocument(encoded) + const fragment = Base64.deserializeNode(encoded) drop.type = 'fragment' drop.fragment = fragment drop.isInternal = this.tmp.isInternalDrag @@ -395,6 +404,7 @@ class Content extends React.Component { drop.data = data drop.target = target + drop.effect = data.dropEffect this.props.onDrop(e, drop) } @@ -518,7 +528,7 @@ class Content extends React.Component { const regexp = /data-fragment="([^\s]+)"/ const matches = regexp.exec(paste.html) const [ full, encoded ] = matches - const fragment = Base64.deserializeDocument(encoded) + const fragment = Base64.deserializeNode(encoded) let { state } = this.props state = state @@ -653,62 +663,15 @@ class Content extends React.Component { */ renderNode = (node) => { - switch (node.kind) { - case 'block': - case 'inline': - return this.renderElement(node) - case 'text': - return this.renderText(node) - } - } - - /** - * Render an element `node`. - * - * @param {Node} node - * @return {Element} element - */ - - renderElement = (node) => { - const { editor, renderNode, state } = this.props - const Component = renderNode(node) - const children = node.nodes - .map(child => this.renderNode(child)) - .toArray() - - const attributes = { - 'data-key': node.key - } - + const { editor, renderMark, renderNode, state } = this.props return ( - - {children} - - ) - } - - /** - * Render a text `node`. - * - * @param {Node} node - * @return {Element} element - */ - - renderText = (node) => { - const { editor, renderMark, state } = this.props - return ( - ) } diff --git a/lib/components/draggable.js b/lib/components/draggable.js deleted file mode 100644 index 836d4ef62..000000000 --- a/lib/components/draggable.js +++ /dev/null @@ -1,63 +0,0 @@ - -import Base64 from '../serializers/base-64' -import React from 'react' -import TYPES from '../utils/types' - -/** - * Draggable. - * - * @type {Component} - */ - -class Draggable extends React.Component { - - static propTypes = { - children: React.PropTypes.any.isRequired, - className: React.PropTypes.string, - editor: React.PropTypes.object.isRequired, - node: React.PropTypes.object.isRequired, - state: React.PropTypes.object.isRequired, - style: React.PropTypes.object - }; - - static defaultProps = { - style: {} - } - - shouldComponentUpdate = (props) => { - return ( - props.node != this.props.node || - props.state.selection.hasEdgeIn(props.node) || - this.props.state.selection.hasEdgeIn(this.props.node) - ) - } - - onDragStart = (e) => { - const { node } = this.props - const encoded = Base64.serializeNode(node) - const data = e.nativeEvent.dataTransfer - data.setData(TYPES.NODE, encoded) - } - - render = () => { - const { children, node, className, style } = this.props - const Tag = node.kind == 'block' ? 'div' : 'span' - return ( - - {children} - - ) - } - -} - -/** - * Export. - */ - -export default Draggable diff --git a/lib/components/node.js b/lib/components/node.js new file mode 100644 index 000000000..6b05e9740 --- /dev/null +++ b/lib/components/node.js @@ -0,0 +1,139 @@ + +import Base64 from '../serializers/base-64' +import React from 'react' +import TYPES from '../utils/types' +import Text from './text' + +/** + * Node. + * + * @type {Component} + */ + +class Node extends React.Component { + + static propTypes = { + editor: React.PropTypes.object.isRequired, + node: React.PropTypes.object.isRequired, + renderMark: React.PropTypes.func.isRequired, + renderNode: React.PropTypes.func.isRequired, + state: React.PropTypes.object.isRequired + }; + + static defaultProps = { + style: {} + } + + shouldComponentUpdate = (props) => { + return ( + props.node != this.props.node || + props.state.selection.hasEdgeIn(props.node) + ) + } + + /** + * On drag start, add a serialized representation of the node to the data. + * + * @param {Event} e + */ + + onDragStart = (e) => { + const { node } = this.props + const encoded = Base64.serializeNode(node) + const data = e.nativeEvent.dataTransfer + data.setData(TYPES.NODE, encoded) + } + + /** + * Render. + * + * @return {Element} element + */ + + render = () => { + const { node } = this.props + return node.kind == 'text' + ? this.renderText() + : this.renderElement() + } + + /** + * Render a `node`. + * + * @param {Node} node + * @return {Element} element + */ + + renderNode = (node) => { + const { editor, renderMark, renderNode, state } = this.props + return ( + + ) + } + + /** + * Render an element `node`. + * + * @return {Element} element + */ + + renderElement = () => { + const { editor, node, renderNode, state } = this.props + const Component = renderNode(node) + const children = node.nodes + .map(child => this.renderNode(child)) + .toArray() + + // Attributes that the developer has to mix into the element in their custom + // renderer component. + const attributes = { + 'data-key': node.key, + 'onDragStart': this.onDragStart + } + + return ( + + {children} + + ) + } + + /** + * Render a text node. + * + * @return {Element} element + */ + + renderText = () => { + const { node, editor, renderMark, state } = this.props + return ( + + ) + } + +} + +/** + * Export. + */ + +export default Node diff --git a/lib/components/void.js b/lib/components/void.js index f595e00b2..06c6a6b7e 100644 --- a/lib/components/void.js +++ b/lib/components/void.js @@ -28,8 +28,7 @@ class Void extends React.Component { shouldComponentUpdate = (props) => { return ( props.node != this.props.node || - props.state.selection.hasEdgeIn(props.node) || - this.props.state.selection.hasEdgeIn(this.props.node) + props.state.selection.hasEdgeIn(props.node) ) } @@ -59,15 +58,10 @@ class Void extends React.Component { } renderSpacer = () => { - // Styles that will cause the spacer to be overlaid exactly on top of the - // void content, so it capture clicks and emulates the same scrolling - // behavior, but with a negative text indent to hide the cursor. const style = { position: 'absolute', top: '0px', - right: '0px', - bottom: '0px', - left: '0px', + left: '-9999px', textIndent: '-9999px' } diff --git a/lib/index.js b/lib/index.js index 51488e4b9..e17c7a970 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,7 +3,6 @@ * Components. */ -import Draggable from './components/draggable' import Editor from './components/editor' import Placeholder from './components/placeholder' import Void from './components/void' @@ -27,7 +26,6 @@ import Text from './models/text' */ import Html from './serializers/html' -import Json from './serializers/json' import Plain from './serializers/plain' import Raw from './serializers/raw' @@ -52,11 +50,9 @@ export { Character, Data, Document, - Draggable, Editor, Html, Inline, - Json, Mark, Placeholder, Plain, @@ -73,11 +69,9 @@ export default { Character, Data, Document, - Draggable, Editor, Html, Inline, - Json, Mark, Placeholder, Plain, diff --git a/lib/plugins/core.js b/lib/plugins/core.js index 7a0de5d41..a7abe9a87 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -176,18 +176,6 @@ function Plugin(options = {}) { .apply() } - case 'node': { - const { node, target, isInternal } = drop - let transform = state.transform() - - if (isInternal) transform = transform.removeNodeByKey(node.key) - - return transform - .moveTo(target) - [node.kind == 'block' ? 'insertBlock' : 'insertInline'](node) - .apply() - } - case 'text': case 'html': { const { text, target } = drop diff --git a/test/rendering/fixtures/custom-block-void/output.html b/test/rendering/fixtures/custom-block-void/output.html index f31b16fd0..5a81ebf40 100644 --- a/test/rendering/fixtures/custom-block-void/output.html +++ b/test/rendering/fixtures/custom-block-void/output.html @@ -2,7 +2,7 @@
- +