mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-18 13:11:17 +02:00
add drag and drop support
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
|
||||
import Key from '../utils/key'
|
||||
import Selection from '../models/selection'
|
||||
import OffsetKey from '../utils/offset-key'
|
||||
import Raw from '../serializers/raw'
|
||||
import React from 'react'
|
||||
@@ -120,7 +121,6 @@ class Content extends React.Component {
|
||||
onBlur = (e) => {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isCopying) return
|
||||
if (this.tmp.isComposing) return
|
||||
let { state } = this.props
|
||||
|
||||
state = state
|
||||
@@ -180,7 +180,6 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
onCopy = (e) => {
|
||||
if (this.tmp.isComposing) return
|
||||
this.onCutCopy(e)
|
||||
}
|
||||
|
||||
@@ -192,7 +191,6 @@ class Content extends React.Component {
|
||||
|
||||
onCut = (e) => {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isComposing) return
|
||||
this.onCutCopy(e)
|
||||
|
||||
// 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
|
||||
* leave the editor in an out-of-sync state, then bubble up.
|
||||
@@ -302,8 +415,8 @@ class Content extends React.Component {
|
||||
|
||||
onPaste = (e) => {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isComposing) return
|
||||
e.preventDefault()
|
||||
|
||||
const data = e.clipboardData
|
||||
const paste = {}
|
||||
|
||||
@@ -311,7 +424,7 @@ class Content extends React.Component {
|
||||
const types = Array.from(data.types)
|
||||
|
||||
// Handle files.
|
||||
if (data.files.length != 0) {
|
||||
if (data.files.length) {
|
||||
paste.type = 'files'
|
||||
paste.files = data.files
|
||||
}
|
||||
@@ -434,20 +547,24 @@ class Content extends React.Component {
|
||||
return (
|
||||
<div
|
||||
key={`slate-content-${this.forces}`}
|
||||
className={className}
|
||||
contentEditable={!readOnly}
|
||||
suppressContentEditableWarning
|
||||
style={style}
|
||||
className={className}
|
||||
onBeforeInput={this.onBeforeInput}
|
||||
onBlur={this.onBlur}
|
||||
onCompositionEnd={this.onCompositionEnd}
|
||||
onCompositionStart={this.onCompositionStart}
|
||||
onCopy={this.onCopy}
|
||||
onCut={this.onCut}
|
||||
onDragEnd={this.onDragEnd}
|
||||
onDragOver={this.onDragOver}
|
||||
onDragStart={this.onDragStart}
|
||||
onDrop={this.onDrop}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={noop}
|
||||
onPaste={this.onPaste}
|
||||
onSelect={this.onSelect}
|
||||
onKeyUp={noop}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@@ -25,6 +25,7 @@ class Editor extends React.Component {
|
||||
onBeforeInput: React.PropTypes.func,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onDocumentChange: React.PropTypes.func,
|
||||
onDrop: React.PropTypes.func,
|
||||
onKeyDown: React.PropTypes.func,
|
||||
onPaste: React.PropTypes.func,
|
||||
onSelectionChange: React.PropTypes.func,
|
||||
@@ -170,6 +171,16 @@ class Editor extends React.Component {
|
||||
this.onEvent('onBeforeInput', ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* On drop.
|
||||
*
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
onDrop = (...args) => {
|
||||
this.onEvent('onDrop', ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* On key down.
|
||||
*
|
||||
@@ -201,14 +212,15 @@ class Editor extends React.Component {
|
||||
<Content
|
||||
className={this.props.className}
|
||||
editor={this}
|
||||
state={this.state.state}
|
||||
onBeforeInput={this.onBeforeInput}
|
||||
onChange={this.onChange}
|
||||
onDrop={this.onDrop}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPaste={this.onPaste}
|
||||
readOnly={this.props.readOnly}
|
||||
renderMark={this.renderMark}
|
||||
renderNode={this.renderNode}
|
||||
onPaste={this.onPaste}
|
||||
onBeforeInput={this.onBeforeInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
state={this.state.state}
|
||||
style={this.props.style}
|
||||
/>
|
||||
)
|
||||
|
@@ -110,43 +110,26 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* The core `onBeforeInput` handler.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Otherwise, we can allow the default, native text insertion, avoiding a
|
||||
* re-render for improved performance.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {State} state
|
||||
* @param {Editor} editor
|
||||
* @return {State or Null} newState
|
||||
* @return {State or Null}
|
||||
*/
|
||||
|
||||
onBeforeInput(e, state, editor) {
|
||||
const transform = state.transform().insertText(e.data)
|
||||
const synthetic = transform.apply()
|
||||
const resolved = editor.resolveState(synthetic)
|
||||
let isNative = true
|
||||
|
||||
// If the current selection is expanded, we have to re-render.
|
||||
if (state.isExpanded) {
|
||||
isNative = false
|
||||
}
|
||||
// We do not have to re-render if the current selection is collapsed, the
|
||||
// current node is not empty, and the new state has the same decorations
|
||||
// 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
|
||||
? transform.apply({ isNative })
|
||||
: synthetic
|
||||
@@ -155,13 +138,43 @@ function Plugin(options = {}) {
|
||||
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.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {State} state
|
||||
* @param {Editor} editor
|
||||
* @return {State or Null} newState
|
||||
* @return {State or Null}
|
||||
*/
|
||||
|
||||
onKeyDown(e, state, editor) {
|
||||
@@ -267,7 +280,7 @@ function Plugin(options = {}) {
|
||||
* @param {Object} paste
|
||||
* @param {State} state
|
||||
* @param {Editor} editor
|
||||
* @return {State or Null} newState
|
||||
* @return {State or Null}
|
||||
*/
|
||||
|
||||
onPaste(e, paste, state, editor) {
|
||||
@@ -301,7 +314,6 @@ function Plugin(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
Reference in New Issue
Block a user