mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-11 17:53:59 +02:00
add node component, cleanup draggable/void interactions
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { Editor, Raw, wrap } from '../..'
|
import { Editor, Raw } from '../..'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
import initialState from './state.json'
|
import initialState from './state.json'
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { Editor, Raw, wrap } from '../..'
|
import { Editor, Raw, Void } from '../..'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import initialState from './state.json'
|
import initialState from './state.json'
|
||||||
@@ -13,11 +13,15 @@ import isUrl from 'is-url'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const NODES = {
|
const NODES = {
|
||||||
image: wrap()((props) => {
|
image: (props) => {
|
||||||
const { node, state } = props
|
const { node, state } = props
|
||||||
const src = node.data.get('src')
|
const src = node.data.get('src')
|
||||||
return <img draggable src={src} />
|
return (
|
||||||
})
|
<Void {...props} className="image-block">
|
||||||
|
<img src={src} {...props.attributes} />
|
||||||
|
</Void>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +87,7 @@ class Images extends React.Component {
|
|||||||
renderNode={this.renderNode}
|
renderNode={this.renderNode}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onDocumentChange={this.onDocumentChange}
|
onDocumentChange={this.onDocumentChange}
|
||||||
|
onDrop={this.onDrop}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,6 +158,25 @@ class Images extends React.Component {
|
|||||||
this.onChange(state)
|
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.
|
* On paste, if the pasted content is an image URL, insert it.
|
||||||
*
|
*
|
||||||
|
@@ -112,7 +112,19 @@ class App extends React.Component {
|
|||||||
const router = (
|
const router = (
|
||||||
<Router history={hashHistory}>
|
<Router history={hashHistory}>
|
||||||
<Route path="/" component={App}>
|
<Route path="/" component={App}>
|
||||||
|
<IndexRedirect to="rich-text" />
|
||||||
<Route path="auto-markdown" component={AutoMarkdown} />
|
<Route path="auto-markdown" component={AutoMarkdown} />
|
||||||
|
<Route path="code-highlighting" component={CodeHighlighting} />
|
||||||
|
<Route path="hovering-menu" component={HoveringMenu} />
|
||||||
|
<Route path="images" component={Images} />
|
||||||
|
<Route path="links" component={Links} />
|
||||||
|
<Route path="paste-html" component={PasteHtml} />
|
||||||
|
<Route path="plain-text" component={PlainText} />
|
||||||
|
<Route path="read-only" component={ReadOnly} />
|
||||||
|
<Route path="rich-text" component={RichText} />
|
||||||
|
<Route path="tables" component={Tables} />
|
||||||
|
<Route path="dev-performance-plain" component={DevPerformancePlain} />
|
||||||
|
<Route path="dev-performance-rich" component={DevPerformanceRich} />
|
||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
import Base64 from '../serializers/base-64'
|
import Base64 from '../serializers/base-64'
|
||||||
import Key from '../utils/key'
|
import Key from '../utils/key'
|
||||||
|
import Node from './node'
|
||||||
import OffsetKey from '../utils/offset-key'
|
import OffsetKey from '../utils/offset-key'
|
||||||
import Raw from '../serializers/raw'
|
import Raw from '../serializers/raw'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Selection from '../models/selection'
|
import Selection from '../models/selection'
|
||||||
import Text from './text'
|
|
||||||
import TYPES from '../utils/types'
|
import TYPES from '../utils/types'
|
||||||
import includes from 'lodash/includes'
|
import includes from 'lodash/includes'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
@@ -341,7 +341,16 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
// Resolve the point where the drop occured.
|
// Resolve the point where the drop occured.
|
||||||
const { x, y } = e.nativeEvent
|
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 startNode = range.startContainer
|
||||||
const startOffset = range.startOffset
|
const startOffset = range.startOffset
|
||||||
const point = OffsetKey.findPoint(startNode, startOffset, state)
|
const point = OffsetKey.findPoint(startNode, startOffset, state)
|
||||||
@@ -359,7 +368,7 @@ class Content extends React.Component {
|
|||||||
// Handle Slate fragments.
|
// Handle Slate fragments.
|
||||||
if (includes(types, TYPES.FRAGMENT)) {
|
if (includes(types, TYPES.FRAGMENT)) {
|
||||||
const encoded = data.getData(TYPES.FRAGMENT)
|
const encoded = data.getData(TYPES.FRAGMENT)
|
||||||
const fragment = Base64.deserializeDocument(encoded)
|
const fragment = Base64.deserializeNode(encoded)
|
||||||
drop.type = 'fragment'
|
drop.type = 'fragment'
|
||||||
drop.fragment = fragment
|
drop.fragment = fragment
|
||||||
drop.isInternal = this.tmp.isInternalDrag
|
drop.isInternal = this.tmp.isInternalDrag
|
||||||
@@ -395,6 +404,7 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
drop.data = data
|
drop.data = data
|
||||||
drop.target = target
|
drop.target = target
|
||||||
|
drop.effect = data.dropEffect
|
||||||
this.props.onDrop(e, drop)
|
this.props.onDrop(e, drop)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +528,7 @@ class Content extends React.Component {
|
|||||||
const regexp = /data-fragment="([^\s]+)"/
|
const regexp = /data-fragment="([^\s]+)"/
|
||||||
const matches = regexp.exec(paste.html)
|
const matches = regexp.exec(paste.html)
|
||||||
const [ full, encoded ] = matches
|
const [ full, encoded ] = matches
|
||||||
const fragment = Base64.deserializeDocument(encoded)
|
const fragment = Base64.deserializeNode(encoded)
|
||||||
let { state } = this.props
|
let { state } = this.props
|
||||||
|
|
||||||
state = state
|
state = state
|
||||||
@@ -653,62 +663,15 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode = (node) => {
|
renderNode = (node) => {
|
||||||
switch (node.kind) {
|
const { editor, renderMark, renderNode, state } = this.props
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Node
|
||||||
attributes={attributes}
|
|
||||||
key={node.key}
|
key={node.key}
|
||||||
editor={editor}
|
|
||||||
node={node}
|
node={node}
|
||||||
state={state}
|
state={state}
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Component>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a text `node`.
|
|
||||||
*
|
|
||||||
* @param {Node} node
|
|
||||||
* @return {Element} element
|
|
||||||
*/
|
|
||||||
|
|
||||||
renderText = (node) => {
|
|
||||||
const { editor, renderMark, state } = this.props
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
key={node.key}
|
|
||||||
editor={editor}
|
editor={editor}
|
||||||
node={node}
|
renderNode={renderNode}
|
||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
state={state}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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 (
|
|
||||||
<Tag
|
|
||||||
draggable
|
|
||||||
onDragStart={this.onDragStart}
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default Draggable
|
|
139
lib/components/node.js
Normal file
139
lib/components/node.js
Normal file
@@ -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 (
|
||||||
|
<Node
|
||||||
|
key={node.key}
|
||||||
|
node={node}
|
||||||
|
state={state}
|
||||||
|
editor={editor}
|
||||||
|
renderNode={renderNode}
|
||||||
|
renderMark={renderMark}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Component
|
||||||
|
attributes={attributes}
|
||||||
|
key={node.key}
|
||||||
|
editor={editor}
|
||||||
|
node={node}
|
||||||
|
state={state}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a text node.
|
||||||
|
*
|
||||||
|
* @return {Element} element
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderText = () => {
|
||||||
|
const { node, editor, renderMark, state } = this.props
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={node.key}
|
||||||
|
editor={editor}
|
||||||
|
node={node}
|
||||||
|
renderMark={renderMark}
|
||||||
|
state={state}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default Node
|
@@ -28,8 +28,7 @@ class Void extends React.Component {
|
|||||||
shouldComponentUpdate = (props) => {
|
shouldComponentUpdate = (props) => {
|
||||||
return (
|
return (
|
||||||
props.node != this.props.node ||
|
props.node != this.props.node ||
|
||||||
props.state.selection.hasEdgeIn(props.node) ||
|
props.state.selection.hasEdgeIn(props.node)
|
||||||
this.props.state.selection.hasEdgeIn(this.props.node)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +58,10 @@ class Void extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSpacer = () => {
|
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 = {
|
const style = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '0px',
|
top: '0px',
|
||||||
right: '0px',
|
left: '-9999px',
|
||||||
bottom: '0px',
|
|
||||||
left: '0px',
|
|
||||||
textIndent: '-9999px'
|
textIndent: '-9999px'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
* Components.
|
* Components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Draggable from './components/draggable'
|
|
||||||
import Editor from './components/editor'
|
import Editor from './components/editor'
|
||||||
import Placeholder from './components/placeholder'
|
import Placeholder from './components/placeholder'
|
||||||
import Void from './components/void'
|
import Void from './components/void'
|
||||||
@@ -27,7 +26,6 @@ import Text from './models/text'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Html from './serializers/html'
|
import Html from './serializers/html'
|
||||||
import Json from './serializers/json'
|
|
||||||
import Plain from './serializers/plain'
|
import Plain from './serializers/plain'
|
||||||
import Raw from './serializers/raw'
|
import Raw from './serializers/raw'
|
||||||
|
|
||||||
@@ -52,11 +50,9 @@ export {
|
|||||||
Character,
|
Character,
|
||||||
Data,
|
Data,
|
||||||
Document,
|
Document,
|
||||||
Draggable,
|
|
||||||
Editor,
|
Editor,
|
||||||
Html,
|
Html,
|
||||||
Inline,
|
Inline,
|
||||||
Json,
|
|
||||||
Mark,
|
Mark,
|
||||||
Placeholder,
|
Placeholder,
|
||||||
Plain,
|
Plain,
|
||||||
@@ -73,11 +69,9 @@ export default {
|
|||||||
Character,
|
Character,
|
||||||
Data,
|
Data,
|
||||||
Document,
|
Document,
|
||||||
Draggable,
|
|
||||||
Editor,
|
Editor,
|
||||||
Html,
|
Html,
|
||||||
Inline,
|
Inline,
|
||||||
Json,
|
|
||||||
Mark,
|
Mark,
|
||||||
Placeholder,
|
Placeholder,
|
||||||
Plain,
|
Plain,
|
||||||
|
@@ -176,18 +176,6 @@ function Plugin(options = {}) {
|
|||||||
.apply()
|
.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 'text':
|
||||||
case 'html': {
|
case 'html': {
|
||||||
const { text, target } = drop
|
const { text, target } = drop
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div contenteditable="true">
|
<div contenteditable="true">
|
||||||
<div contenteditable="false">
|
<div contenteditable="false">
|
||||||
<div contenteditable="true" style="position:relative;">
|
<div contenteditable="true" style="position:relative;">
|
||||||
<span style="position:absolute;top:0px;right:0px;bottom:0px;left:0px;text-indent:-9999px;">
|
<span style="position:absolute;top:0px;left:-9999px;text-indent:-9999px;">
|
||||||
<span>
|
<span>
|
||||||
<br>
|
<br>
|
||||||
</span>
|
</span>
|
||||||
|
Reference in New Issue
Block a user