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

deprecate data, add getEventRange and getEventTransfer helpers (#1243)

This commit is contained in:
Ian Storm Taylor
2017-10-16 18:50:29 -07:00
committed by GitHub
parent 617fba2ac0
commit b462c2ce19
9 changed files with 347 additions and 308 deletions

View File

@@ -1,6 +1,8 @@
import Editor from './components/editor'
import Placeholder from './components/placeholder'
import getEventRange from './utils/get-event-range'
import getEventTransfer from './utils/get-event-transfer'
import findDOMNode from './utils/find-dom-node'
import findDOMRange from './utils/find-dom-range'
import findNode from './utils/find-node'
@@ -15,6 +17,8 @@ import findRange from './utils/find-range'
export {
Editor,
Placeholder,
getEventRange,
getEventTransfer,
findDOMNode,
findDOMRange,
findNode,
@@ -24,6 +28,8 @@ export {
export default {
Editor,
Placeholder,
getEventRange,
getEventTransfer,
findDOMNode,
findDOMRange,
findNode,

View File

@@ -12,6 +12,9 @@ import Content from '../components/content'
import Placeholder from '../components/placeholder'
import findDOMNode from '../utils/find-dom-node'
import findPoint from '../utils/find-point'
import findRange from '../utils/find-range'
import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer'
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
/**
@@ -39,6 +42,8 @@ function AfterPlugin(options = {}) {
placeholderStyle,
} = options
let isDraggingInternally = null
/**
* On before change, enforce the editor's schema.
*
@@ -143,7 +148,7 @@ function AfterPlugin(options = {}) {
// Create a fake selection so that we can add a Base64-encoded copy of the
// fragment to the HTML, to decode on future pastes.
const { fragment } = data
const { fragment } = state
const encoded = Base64.serializeNode(fragment)
const range = native.getRangeAt(0)
let contents = range.cloneContents()
@@ -233,6 +238,51 @@ function AfterPlugin(options = {}) {
})
}
/**
* On drag end.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragEnd(event, data, change, editor) {
debug('onDragEnd', { event })
isDraggingInternally = null
}
/**
* On drag over.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragOver(event, data, change, editor) {
debug('onDragOver', { event })
isDraggingInternally = false
}
/**
* On drag start.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragStart(event, data, change, editor) {
debug('onDragStart', { event })
isDraggingInternally = true
}
/**
* On drop.
*
@@ -241,39 +291,23 @@ function AfterPlugin(options = {}) {
* @param {Change} change
*/
function onDrop(event, data, change) {
debug('onDrop', { data })
switch (data.type) {
case 'text':
case 'html':
return onDropText(event, data, change)
case 'fragment':
return onDropFragment(event, data, change)
case 'node':
return onDropNode(event, data, change)
}
}
/**
* On drop node.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
*/
function onDropNode(event, data, change) {
debug('onDropNode', { data })
function onDrop(event, data, change, editor) {
debug('onDrop', { event })
const { state } = change
const { selection } = state
let { node, target, isInternal } = data
let target = getEventRange(event, state)
if (!target) return
const transfer = getEventTransfer(event)
const { type, fragment, node, text } = transfer
change.focus()
// If the drag is internal and the target is after the selection, it
// needs to account for the selection's content being deleted.
if (
isInternal &&
isDraggingInternally &&
selection.endKey == target.endKey &&
selection.endOffset < target.endOffset
) {
@@ -282,103 +316,47 @@ function AfterPlugin(options = {}) {
: 0 - selection.endOffset)
}
if (isInternal) {
if (isDraggingInternally) {
change.delete()
}
if (Block.isBlock(node)) {
change
.select(target)
.focus()
.insertBlock(node)
.removeNodeByKey(node.key)
}
change.select(target)
if (Inline.isInline(node)) {
change
.select(target)
.focus()
.insertInline(node)
.removeNodeByKey(node.key)
}
}
if (type == 'text' || type == 'html') {
const { anchorKey } = target
let hasVoidParent = document.hasVoidParent(anchorKey)
/**
* On drop fragment.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
*/
if (hasVoidParent) {
let n = document.getNode(anchorKey)
function onDropFragment(event, data, change) {
debug('onDropFragment', { data })
while (hasVoidParent) {
n = document.getNextText(n.key)
if (!n) break
hasVoidParent = document.hasVoidParent(n.key)
}
const { state } = change
const { selection } = state
let { fragment, target, isInternal } = data
// If the drag is internal and the target is after the selection, it
// needs to account for the selection's content being deleted.
if (
isInternal &&
selection.endKey == target.endKey &&
selection.endOffset < target.endOffset
) {
target = target.move(selection.startKey == selection.endKey
? 0 - selection.endOffset + selection.startOffset
: 0 - selection.endOffset)
}
if (isInternal) {
change.delete()
}
change
.select(target)
.focus()
.insertFragment(fragment)
}
/**
* On drop text.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
*/
function onDropText(event, data, change) {
debug('onDropText', { data })
const { state } = change
const { document } = state
const { text, target } = data
const { anchorKey } = target
change.select(target).focus()
let hasVoidParent = document.hasVoidParent(anchorKey)
// Insert text into nearest text node
if (hasVoidParent) {
let node = document.getNode(anchorKey)
while (hasVoidParent) {
node = document.getNextText(node.key)
if (!node) break
hasVoidParent = document.hasVoidParent(node.key)
if (n) change.collapseToStartOf(n)
}
if (node) change.collapseToStartOf(node)
text
.split('\n')
.forEach((line, i) => {
if (i > 0) change.splitBlock()
change.insertText(line)
})
}
text
.split('\n')
.forEach((line, i) => {
if (i > 0) change.splitBlock()
change.insertText(line)
})
if (type == 'fragment') {
change.insertFragment(fragment)
}
if (type == 'node' && Block.isBlock(node)) {
change.insertBlock(node).removeNodeByKey(node.key)
}
if (type == 'node' && Inline.isInline(node)) {
change.insertInline(node).removeNodeByKey(node.key)
}
}
/**
@@ -733,48 +711,23 @@ function AfterPlugin(options = {}) {
function onPaste(event, data, change) {
debug('onPaste', { data })
switch (data.type) {
case 'fragment':
return onPasteFragment(event, data, change)
case 'text':
case 'html':
return onPasteText(event, data, change)
const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer
if (type == 'fragment') {
change.insertFragment(fragment)
}
}
/**
* On paste fragment.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
*/
if (type == 'text' || type == 'html') {
const { state } = change
const { document, selection, startBlock } = state
if (startBlock.isVoid) return
function onPasteFragment(event, data, change) {
debug('onPasteFragment', { data })
change.insertFragment(data.fragment)
}
/**
* On paste text, split blocks at new lines.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
*/
function onPasteText(event, data, change) {
debug('onPasteText', { data })
const { state } = change
const { document, selection, startBlock } = state
if (startBlock.isVoid) return
const { text } = data
const defaultBlock = startBlock
const defaultMarks = document.getMarksAtRange(selection.collapseToStart())
const fragment = Plain.deserialize(text, { defaultBlock, defaultMarks }).document
change.insertFragment(fragment)
const defaultBlock = startBlock
const defaultMarks = document.getMarksAtRange(selection.collapseToStart())
const frag = Plain.deserialize(text, { defaultBlock, defaultMarks }).document
change.insertFragment(frag)
}
}
/**
@@ -787,7 +740,73 @@ function AfterPlugin(options = {}) {
function onSelect(event, data, change) {
debug('onSelect', { data })
change.select(data.selection)
const window = getWindow(event.target)
const { state } = change
const { document } = state
const native = window.getSelection()
// If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) {
change.blur()
return
}
// Otherwise, determine the Slate selection from the native one.
let range = findRange(native, state)
if (!range) return
const { anchorKey, anchorOffset, focusKey, focusOffset } = range
const anchorText = document.getNode(anchorKey)
const focusText = document.getNode(focusKey)
const anchorInline = document.getClosestInline(anchorKey)
const focusInline = document.getClosestInline(focusKey)
const focusBlock = document.getClosestBlock(focusKey)
const anchorBlock = document.getClosestBlock(anchorKey)
// COMPAT: If the anchor point is at the start of a non-void, and the
// focus point is inside a void node with an offset that isn't `0`, set
// the focus offset to `0`. This is due to void nodes <span>'s being
// positioned off screen, resulting in the offset always being greater
// than `0`. Since we can't know what it really should be, and since an
// offset of `0` is less destructive because it creates a hanging
// selection, go with `0`. (2017/09/07)
if (
anchorBlock &&
!anchorBlock.isVoid &&
anchorOffset == 0 &&
focusBlock &&
focusBlock.isVoid &&
focusOffset != 0
) {
range = range.set('focusOffset', 0)
}
// COMPAT: If the selection is at the end of a non-void inline node, and
// there is a node after it, put it in the node after instead. This
// standardizes the behavior, since it's indistinguishable to the user.
if (
anchorInline &&
!anchorInline.isVoid &&
anchorOffset == anchorText.text.length
) {
const block = document.getClosestBlock(anchorKey)
const next = block.getNextText(anchorKey)
if (next) range = range.moveAnchorTo(next.key, 0)
}
if (
focusInline &&
!focusInline.isVoid &&
focusOffset == focusText.text.length
) {
const block = document.getClosestBlock(focusKey)
const next = block.getNextText(focusKey)
if (next) range = range.moveFocusTo(next.key, 0)
}
range = range.normalize(document)
change.select(range)
}
/**
@@ -899,6 +918,9 @@ function AfterPlugin(options = {}) {
onBlur,
onCopy,
onCut,
onDragEnd,
onDragOver,
onDragStart,
onDrop,
onInput,
onKeyDown,

View File

@@ -9,7 +9,8 @@ import { findDOMNode } from 'react-dom'
import HOTKEYS from '../constants/hotkeys'
import TRANSFER_TYPES from '../constants/transfer-types'
import findRange from '../utils/find-range'
import getTransferData from '../utils/get-transfer-data'
import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer'
import setTransferData from '../utils/set-transfer-data'
import { IS_FIREFOX, IS_MAC, SUPPORTED_EVENTS } from '../constants/environment'
@@ -135,8 +136,8 @@ function BeforePlugin() {
window.requestAnimationFrame(() => isCopying = false)
const { state } = change
data.type = 'fragment'
data.fragment = state.fragment
defineDeprecatedData(data, 'type', 'fragment')
defineDeprecatedData(data, 'fragment', state.fragment)
debug('onCopy', { event })
}
@@ -158,8 +159,8 @@ function BeforePlugin() {
window.requestAnimationFrame(() => isCopying = false)
const { state } = change
data.type = 'fragment'
data.fragment = state.fragment
defineDeprecatedData(data, 'type', 'fragment')
defineDeprecatedData(data, 'fragment', state.fragment)
debug('onCut', { event })
}
@@ -212,12 +213,16 @@ function BeforePlugin() {
isDragging = true
isInternalDrag = true
const { dataTransfer } = event.nativeEvent
const d = getTransferData(dataTransfer)
Object.assign(data, d)
const d = getEventTransfer(event)
const { nativeEvent } = event
const { dataTransfer } = nativeEvent
if (data.type != 'node') {
const { state } = this.props
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
if (d.type != 'node') {
const { state } = change
const { fragment } = state
const encoded = Base64.serializeNode(fragment)
setTransferData(dataTransfer, TRANSFER_TYPES.FRAGMENT, encoded)
@@ -243,60 +248,29 @@ function BeforePlugin() {
const { state } = change
const { nativeEvent } = event
const { dataTransfer, x, y } = nativeEvent
const d = getTransferData(dataTransfer)
Object.assign(data, d)
const { dataTransfer } = nativeEvent
const d = getEventTransfer(event)
// Resolve a range from the caret position where the drop occured.
const window = getWindow(event.target)
let range
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (window.document.caretRangeFromPoint) {
range = window.document.caretRangeFromPoint(x, y)
} else {
const position = window.document.caretPositionFromPoint(x, y)
range = window.document.createRange()
range.setStart(position.offsetNode, position.offset)
range.setEnd(position.offsetNode, position.offset)
}
// Resolve a Slate range from the DOM range.
let selection = findRange(range, state)
if (!selection) return true
const { document } = state
const node = document.getNode(selection.anchorKey)
const parent = document.getParent(node.key)
const el = findDOMNode(parent)
// If the drop target is inside a void node, move it into either the next or
// previous node, depending on which side the `x` and `y` coordinates are
// closest to.
if (parent.isVoid) {
const rect = el.getBoundingClientRect()
const isPrevious = parent.kind == 'inline'
? x - rect.left < rect.left + rect.width - x
: y - rect.top < rect.top + rect.height - y
selection = isPrevious
? selection.moveToEndOf(document.getPreviousText(node.key))
: selection.moveToStartOf(document.getNextText(node.key))
}
const range = getEventRange(event, state)
if (!range) return true
// Add drop-specific information to the data.
data.target = selection
defineDeprecatedData(data, 'target', range)
// COMPAT: Edge throws "Permission denied" errors when
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
try {
data.effect = dataTransfer.dropEffect
defineDeprecatedData(data, 'effect', dataTransfer.dropEffect)
} catch (err) {
data.effect = null
defineDeprecatedData(data, 'effect', null)
}
if (d.type == 'fragment' || d.type == 'node') {
data.isInternal = isInternalDrag
defineDeprecatedData(data, 'isInternal', isInternalDrag)
}
debug('onDrop', { event })
@@ -421,19 +395,14 @@ function BeforePlugin() {
if (editor.props.readOnly) return
event.preventDefault()
const d = getTransferData(event.clipboardData)
Object.assign(data, d)
const d = getEventTransfer(event)
// COMPAT: Attach the `isShift` flag, so that people can use it to trigger
// "Paste and Match Style" logic.
Object.defineProperty(data, 'isShift', {
enumerable: true,
get() {
logger.deprecate('0.28.0', 'The `data.isShift` property of paste events has been deprecated. If you need this functionality, you\'ll need to keep track of that state with `onKeyDown` and `onKeyUp` events instead')
return isShifting
}
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
defineDeprecatedData(data, 'isShift', isShifting)
debug('onPaste', { event })
}
@@ -458,7 +427,7 @@ function BeforePlugin() {
// If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) {
data.selection = selection.blur()
defineDeprecatedData(data, 'selection', selection.blur())
}
// Otherwise, determine the Slate selection from the native one.
@@ -516,7 +485,7 @@ function BeforePlugin() {
}
range = range.normalize(document)
data.selection = range
defineDeprecatedData(data, 'selection', range)
}
debug('onSelect', { event })
@@ -549,37 +518,33 @@ function BeforePlugin() {
}
/**
* Add deprecated `data` fields from a key `event`.
*
* @param {Object} data
* @param {Object} event
* Deprecated.
*/
function defineDeprecatedData(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
get() {
logger.deprecate('slate-react@0.5.0', `Accessing the \`data.${key}\` property is deprecated, please use the native \`event\` properties instead, or one of the newly exposed helper utilities.`)
return value
}
})
}
function addDeprecatedKeyProperties(data, event) {
const { altKey, ctrlKey, metaKey, shiftKey, which } = event
const name = keycode(which)
function define(key, value) {
Object.defineProperty(data, key, {
enumerable: true,
get() {
logger.deprecate('0.28.0', `The \`data.${key}\` property of keyboard events is deprecated, please use the native \`event\` properties instead.`)
return value
}
})
}
define('code', which)
define('key', name)
define('isAlt', altKey)
define('isCmd', IS_MAC ? metaKey && !altKey : false)
define('isCtrl', ctrlKey && !altKey)
define('isLine', IS_MAC ? metaKey : false)
define('isMeta', metaKey)
define('isMod', IS_MAC ? metaKey && !altKey : ctrlKey && !altKey)
define('isModAlt', IS_MAC ? metaKey && altKey : ctrlKey && altKey)
define('isShift', shiftKey)
define('isWord', IS_MAC ? altKey : ctrlKey)
defineDeprecatedData(data, 'code', which)
defineDeprecatedData(data, 'key', name)
defineDeprecatedData(data, 'isAlt', altKey)
defineDeprecatedData(data, 'isCmd', IS_MAC ? metaKey && !altKey : false)
defineDeprecatedData(data, 'isCtrl', ctrlKey && !altKey)
defineDeprecatedData(data, 'isLine', IS_MAC ? metaKey : false)
defineDeprecatedData(data, 'isMeta', metaKey)
defineDeprecatedData(data, 'isMod', IS_MAC ? metaKey && !altKey : ctrlKey && !altKey)
defineDeprecatedData(data, 'isModAlt', IS_MAC ? metaKey && altKey : ctrlKey && altKey)
defineDeprecatedData(data, 'isShift', shiftKey)
defineDeprecatedData(data, 'isWord', IS_MAC ? altKey : ctrlKey)
}
/**

View File

@@ -0,0 +1,69 @@
import getWindow from 'get-window'
import findDOMNode from './find-dom-node'
import findRange from './find-range'
/**
* Get the target range from a DOM `event`.
*
* @param {Event} event
* @param {State} state
* @return {Range}
*/
function getEventRange(event, state) {
if (event.nativeEvent) {
event = event.nativeEvent
}
const { x, y } = event
if (x == null || y == null) return null
// Resolve a range from the caret position where the drop occured.
const window = getWindow(event.target)
let r
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (window.document.caretRangeFromPoint) {
r = window.document.caretRangeFromPoint(x, y)
} else {
const position = window.document.caretPositionFromPoint(x, y)
r = window.document.createRange()
r.setStart(position.offsetNode, position.offset)
r.setEnd(position.offsetNode, position.offset)
}
// Resolve a Slate range from the DOM range.
let range = findRange(r, state)
if (!range) return null
const { document } = state
const node = document.getNode(range.anchorKey)
const parent = document.getParent(node.key)
const el = findDOMNode(parent)
// If the drop target is inside a void node, move it into either the next or
// previous node, depending on which side the `x` and `y` coordinates are
// closest to.
if (parent.isVoid) {
const rect = el.getBoundingClientRect()
const isPrevious = parent.kind == 'inline'
? x - rect.left < rect.left + rect.width - x
: y - rect.top < rect.top + rect.height - y
range = isPrevious
? range.moveToEndOf(document.getPreviousText(node.key))
: range.moveToStartOf(document.getNextText(node.key))
}
return range
}
/**
* Export.
*
* @type {Function}
*/
export default getEventRange

View File

@@ -12,13 +12,18 @@ import TRANSFER_TYPES from '../constants/transfer-types'
const FRAGMENT_MATCHER = / data-slate-fragment="([^\s"]+)"/
/**
* Get the data and type from a native data `transfer`.
* Get the transfer data from an `event`.
*
* @param {DataTransfer} transfer
* @param {Event} event
* @return {Object}
*/
function getTransferData(transfer) {
function getEventTransfer(event) {
if (event.nativeEvent) {
event = event.nativeEvent
}
const transfer = event.dataTransfer || event.clipboardData
let fragment = getType(transfer, TRANSFER_TYPES.FRAGMENT)
let node = getType(transfer, TRANSFER_TYPES.NODE)
const html = getType(transfer, 'text/html')
@@ -148,4 +153,4 @@ function getType(transfer, type) {
* @type {Function}
*/
export default getTransferData
export default getEventTransfer