1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-01 04:50:27 +02:00

add node component, cleanup draggable/void interactions

This commit is contained in:
Ian Storm Taylor
2016-07-25 16:46:17 -07:00
parent b5bbac02d0
commit 569e940fd1
10 changed files with 200 additions and 149 deletions

View File

@@ -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'

View File

@@ -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 <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}
onChange={this.onChange}
onDocumentChange={this.onDocumentChange}
onDrop={this.onDrop}
onPaste={this.onPaste}
/>
</div>
@@ -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.
*

View File

@@ -112,7 +112,19 @@ class App extends React.Component {
const router = (
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRedirect to="rich-text" />
<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>
</Router>
)

View File

@@ -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 (
<Component
attributes={attributes}
<Node
key={node.key}
editor={editor}
node={node}
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}
node={node}
renderNode={renderNode}
renderMark={renderMark}
state={state}
/>
)
}

View File

@@ -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
View 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

View File

@@ -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'
}

View File

@@ -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,

View File

@@ -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

View File

@@ -2,7 +2,7 @@
<div contenteditable="true">
<div contenteditable="false">
<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>
<br>
</span>