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:
@@ -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>
|
||||||
|
@@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user