1
0
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:
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 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>

View File

@@ -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}
/>
)

View File

@@ -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.
*/