1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-21 14:41:23 +02:00

add drag and drop support

This commit is contained in:
Ian Storm Taylor
2016-07-22 16:58:24 -07:00
parent 384af9ea3a
commit ebb1625e29
3 changed files with 182 additions and 41 deletions

View File

@@ -1,5 +1,6 @@
import Key from '../utils/key' import Key from '../utils/key'
import Selection from '../models/selection'
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'
@@ -120,7 +121,6 @@ class Content extends React.Component {
onBlur = (e) => { onBlur = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (this.tmp.isCopying) return if (this.tmp.isCopying) return
if (this.tmp.isComposing) return
let { state } = this.props let { state } = this.props
state = state state = state
@@ -180,7 +180,6 @@ class Content extends React.Component {
*/ */
onCopy = (e) => { onCopy = (e) => {
if (this.tmp.isComposing) return
this.onCutCopy(e) this.onCutCopy(e)
} }
@@ -192,7 +191,6 @@ class Content extends React.Component {
onCut = (e) => { onCut = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (this.tmp.isComposing) return
this.onCutCopy(e) this.onCutCopy(e)
// Once the cut has successfully executed, delete the current selection. // Once the cut has successfully executed, delete the current selection.
@@ -260,6 +258,121 @@ class Content extends React.Component {
}) })
} }
/**
* On drag end, unset the `isDragging` flag.
*
* @param {Event} e
*/
onDragEnd = (e) => {
this.tmp.isDragging = false
}
/**
* On drag over, set the `isDragging` flag and the `isInternalDrag` flag.
*
* @param {Event} e
*/
onDragOver = (e) => {
if (this.tmp.isDragging) return
this.tmp.isDragging = true
this.tmp.isInternalDrag = false
}
/**
* On drag start, set the `isDragging` flag and the `isInternalDrag` flag.
*
* @param {Event} e
*/
onDragStart = (e) => {
this.tmp.isDragging = true
this.tmp.isInternalDrag = true
}
/**
* On drop.
*
* @param {Event} e
*/
onDrop = (e) => {
if (this.props.readOnly) return
e.preventDefault()
const { state } = this.props
const { selection } = state
const data = e.nativeEvent.dataTransfer
const drop = {}
// Resolve the point where the drop occured.
const { x, y } = e.nativeEvent
const range = window.document.caretRangeFromPoint(x, y)
const startNode = range.startContainer
const startOffset = range.startOffset
const point = OffsetKey.findPoint(startNode, startOffset, state)
let target = Selection.create({
anchorKey: point.key,
anchorOffset: point.offset,
focusKey: point.key,
focusOffset: point.offset,
isFocused: true
})
// If the drag is internal, handle it now. And it the target is after the
// selection, it needs to account for the selection's content being deleted.
if (this.tmp.isInternalDrag) {
if (
selection.endKey == target.endKey &&
selection.endOffset < target.endOffset
) {
const width = selection.startKey == selection.endKey
? selection.endOffset - selection.startOffset
: selection.endOffset
target = target.moveBackward(width)
}
const fragment = state.fragment
const next = state
.transform()
.delete()
.moveTo(target)
.insertFragment(fragment)
.apply()
this.onChange(next)
return
}
// COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types)
// Handle files.
if (data.files.length) {
drop.type = 'files'
drop.files = data.files
}
// Handle HTML.
else if (includes(types, 'text/html')) {
drop.type = 'html'
drop.text = data.getData('text/plain')
drop.html = data.getData('text/html')
}
// Handle plain text.
else {
drop.type = 'text'
drop.text = data.getData('text/plain')
}
drop.data = data
drop.target = target
this.props.onDrop(e, drop)
}
/** /**
* On key down, prevent the default behavior of certain commands that will * On key down, prevent the default behavior of certain commands that will
* leave the editor in an out-of-sync state, then bubble up. * leave the editor in an out-of-sync state, then bubble up.
@@ -302,8 +415,8 @@ class Content extends React.Component {
onPaste = (e) => { onPaste = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (this.tmp.isComposing) return
e.preventDefault() e.preventDefault()
const data = e.clipboardData const data = e.clipboardData
const paste = {} const paste = {}
@@ -311,7 +424,7 @@ class Content extends React.Component {
const types = Array.from(data.types) const types = Array.from(data.types)
// Handle files. // Handle files.
if (data.files.length != 0) { if (data.files.length) {
paste.type = 'files' paste.type = 'files'
paste.files = data.files paste.files = data.files
} }
@@ -434,20 +547,24 @@ class Content extends React.Component {
return ( return (
<div <div
key={`slate-content-${this.forces}`} key={`slate-content-${this.forces}`}
className={className}
contentEditable={!readOnly} contentEditable={!readOnly}
suppressContentEditableWarning suppressContentEditableWarning
style={style} className={className}
onBeforeInput={this.onBeforeInput} onBeforeInput={this.onBeforeInput}
onBlur={this.onBlur} onBlur={this.onBlur}
onCompositionEnd={this.onCompositionEnd} onCompositionEnd={this.onCompositionEnd}
onCompositionStart={this.onCompositionStart} onCompositionStart={this.onCompositionStart}
onCopy={this.onCopy} onCopy={this.onCopy}
onCut={this.onCut} onCut={this.onCut}
onDragEnd={this.onDragEnd}
onDragOver={this.onDragOver}
onDragStart={this.onDragStart}
onDrop={this.onDrop}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onKeyUp={noop}
onPaste={this.onPaste} onPaste={this.onPaste}
onSelect={this.onSelect} onSelect={this.onSelect}
onKeyUp={noop} style={style}
> >
{children} {children}
</div> </div>

View File

@@ -25,6 +25,7 @@ class Editor extends React.Component {
onBeforeInput: React.PropTypes.func, onBeforeInput: React.PropTypes.func,
onChange: React.PropTypes.func.isRequired, onChange: React.PropTypes.func.isRequired,
onDocumentChange: React.PropTypes.func, onDocumentChange: React.PropTypes.func,
onDrop: React.PropTypes.func,
onKeyDown: React.PropTypes.func, onKeyDown: React.PropTypes.func,
onPaste: React.PropTypes.func, onPaste: React.PropTypes.func,
onSelectionChange: React.PropTypes.func, onSelectionChange: React.PropTypes.func,
@@ -170,6 +171,16 @@ class Editor extends React.Component {
this.onEvent('onBeforeInput', ...args) this.onEvent('onBeforeInput', ...args)
} }
/**
* On drop.
*
* @param {Mixed} ...args
*/
onDrop = (...args) => {
this.onEvent('onDrop', ...args)
}
/** /**
* On key down. * On key down.
* *
@@ -201,14 +212,15 @@ class Editor extends React.Component {
<Content <Content
className={this.props.className} className={this.props.className}
editor={this} editor={this}
state={this.state.state} onBeforeInput={this.onBeforeInput}
onChange={this.onChange} onChange={this.onChange}
onDrop={this.onDrop}
onKeyDown={this.onKeyDown}
onPaste={this.onPaste}
readOnly={this.props.readOnly} readOnly={this.props.readOnly}
renderMark={this.renderMark} renderMark={this.renderMark}
renderNode={this.renderNode} renderNode={this.renderNode}
onPaste={this.onPaste} state={this.state.state}
onBeforeInput={this.onBeforeInput}
onKeyDown={this.onKeyDown}
style={this.props.style} style={this.props.style}
/> />
) )

View File

@@ -110,43 +110,26 @@ function Plugin(options = {}) {
/** /**
* The core `onBeforeInput` handler. * The core `onBeforeInput` handler.
* *
*
*
*
*
* Otherwise, we can allow the default, native text insertion, avoiding a
* re-render for improved performance.
*
* @param {Event} e * @param {Event} e
* @param {State} state * @param {State} state
* @param {Editor} editor * @param {Editor} editor
* @return {State or Null} newState * @return {State or Null}
*/ */
onBeforeInput(e, state, editor) { onBeforeInput(e, state, editor) {
const transform = state.transform().insertText(e.data) const transform = state.transform().insertText(e.data)
const synthetic = transform.apply() const synthetic = transform.apply()
const resolved = editor.resolveState(synthetic) const resolved = editor.resolveState(synthetic)
let isNative = true
// If the current selection is expanded, we have to re-render. // We do not have to re-render if the current selection is collapsed, the
if (state.isExpanded) { // current node is not empty, and the new state has the same decorations
isNative = false // as the current one.
} const isNative = (
state.isCollapsed &&
state.startText.text != '' &&
resolved.equals(synthetic)
)
// If the current node was empty, we have to re-render so that any empty
// placeholder logic will be updated.
if (state.startText.text == '') {
isNative = false
}
// If the next state resolves a new list of decorations for any of its
// text nodes, we have to re-render.
else if (!resolved.equals(synthetic)) {
isNative = false
}
// Update the state with the proper `isNative`.
state = isNative state = isNative
? transform.apply({ isNative }) ? transform.apply({ isNative })
: synthetic : synthetic
@@ -155,13 +138,43 @@ function Plugin(options = {}) {
return state return state
}, },
/**
* The core `onDrop` handler.
*
* @param {Event} e
* @param {Object} drop
* @param {State} state
* @param {Editor} editor
* @return {State or Null}
*/
onDrop(e, drop, state, editor) {
switch (drop.type) {
case 'text':
case 'html': {
let transform = state
.transform()
.moveTo(drop.target)
drop.text
.split('\n')
.forEach((line, i) => {
if (i > 0) transform = transform.splitBlock()
transform = transform.insertText(line)
})
return transform.apply()
}
}
},
/** /**
* The core `onKeyDown` handler. * The core `onKeyDown` handler.
* *
* @param {Event} e * @param {Event} e
* @param {State} state * @param {State} state
* @param {Editor} editor * @param {Editor} editor
* @return {State or Null} newState * @return {State or Null}
*/ */
onKeyDown(e, state, editor) { onKeyDown(e, state, editor) {
@@ -267,7 +280,7 @@ function Plugin(options = {}) {
* @param {Object} paste * @param {Object} paste
* @param {State} state * @param {State} state
* @param {Editor} editor * @param {Editor} editor
* @return {State or Null} newState * @return {State or Null}
*/ */
onPaste(e, paste, state, editor) { onPaste(e, paste, state, editor) {
@@ -301,7 +314,6 @@ function Plugin(options = {}) {
} }
} }
/** /**
* Export. * Export.
*/ */