mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-13 18:53:59 +02:00
Split core plugin (#1242)
* split core into before/after, add onBeforeInput to before * migrate handlers to before plugin, add event handlers constants * cleanup * refactor hotkeys into constants file * fix serializer, disable core plugin tests * fix linter
This commit is contained in:
@@ -31,7 +31,6 @@
|
||||
"dot-notation": ["error", { "allowKeywords": true }],
|
||||
"eol-last": "error",
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"func-style": ["error", "declaration"],
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/first": "error",
|
||||
|
@@ -9,6 +9,7 @@
|
||||
"debug": "^2.3.2",
|
||||
"get-window": "^1.1.1",
|
||||
"is-in-browser": "^1.1.3",
|
||||
"is-hotkey": "^0.0.3",
|
||||
"is-window": "^1.0.2",
|
||||
"keycode": "^2.1.2",
|
||||
"prop-types": "^15.5.8",
|
||||
|
@@ -1,25 +1,19 @@
|
||||
|
||||
import Base64 from 'slate-base64-serializer'
|
||||
import Debug from 'debug'
|
||||
import React from 'react'
|
||||
import SlateTypes from 'slate-prop-types'
|
||||
import Types from 'prop-types'
|
||||
import getWindow from 'get-window'
|
||||
import keycode from 'keycode'
|
||||
import logger from 'slate-dev-logger'
|
||||
|
||||
import TRANSFER_TYPES from '../constants/transfer-types'
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import Node from './node'
|
||||
import findClosestNode from '../utils/find-closest-node'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
import findDOMRange from '../utils/find-dom-range'
|
||||
import findPoint from '../utils/find-point'
|
||||
import findRange from '../utils/find-range'
|
||||
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
|
||||
import getTransferData from '../utils/get-transfer-data'
|
||||
import scrollToSelection from '../utils/scroll-to-selection'
|
||||
import setTransferData from '../utils/set-transfer-data'
|
||||
import { IS_FIREFOX, IS_MAC, IS_IE, SUPPORTED_EVENTS } from '../constants/environment'
|
||||
import { IS_FIREFOX, IS_IE, SUPPORTED_EVENTS } from '../constants/environment'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -49,16 +43,6 @@ class Content extends React.Component {
|
||||
children: Types.array.isRequired,
|
||||
className: Types.string,
|
||||
editor: Types.object.isRequired,
|
||||
onBeforeInput: Types.func.isRequired,
|
||||
onBlur: Types.func.isRequired,
|
||||
onCopy: Types.func.isRequired,
|
||||
onCut: Types.func.isRequired,
|
||||
onDrop: Types.func.isRequired,
|
||||
onFocus: Types.func.isRequired,
|
||||
onKeyDown: Types.func.isRequired,
|
||||
onKeyUp: Types.func.isRequired,
|
||||
onPaste: Types.func.isRequired,
|
||||
onSelect: Types.func.isRequired,
|
||||
readOnly: Types.bool.isRequired,
|
||||
role: Types.string,
|
||||
schema: SlateTypes.schema.isRequired,
|
||||
@@ -89,8 +73,14 @@ class Content extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tmp = {}
|
||||
this.tmp.compositions = 0
|
||||
this.tmp.forces = 0
|
||||
this.tmp.key = 0
|
||||
this.tmp.isUpdatingSelection = false
|
||||
|
||||
EVENT_HANDLERS.forEach((handler) => {
|
||||
this[handler] = (event) => {
|
||||
this.onEvent(handler, event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,18 +168,18 @@ class Content extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, set the `isSelecting` flag and update the selection.
|
||||
this.tmp.isSelecting = true
|
||||
// Otherwise, set the `isUpdatingSelection` flag and update the selection.
|
||||
this.tmp.isUpdatingSelection = true
|
||||
native.removeAllRanges()
|
||||
native.addRange(range)
|
||||
scrollToSelection(native)
|
||||
|
||||
// Then unset the `isSelecting` flag after a delay.
|
||||
// Then unset the `isUpdatingSelection` flag after a delay.
|
||||
setTimeout(() => {
|
||||
// COMPAT: In Firefox, it's not enough to create a range, you also need to
|
||||
// focus the contenteditable element too. (2016/11/16)
|
||||
if (IS_FIREFOX) this.element.focus()
|
||||
this.tmp.isSelecting = false
|
||||
this.tmp.isUpdatingSelection = false
|
||||
})
|
||||
|
||||
debug('updateSelection', { selection, native })
|
||||
@@ -226,19 +216,76 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* On before input, bubble up.
|
||||
* On `event` with `handler`.
|
||||
*
|
||||
* @param {String} handler
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onBeforeInput = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
onEvent(handler, event) {
|
||||
// COMPAT: Composition events can change the DOM out of under React, so we
|
||||
// increment this key to ensure that a full re-render happens. (2017/10/16)
|
||||
if (handler == 'onCompositionEnd') {
|
||||
this.tmp.key++
|
||||
}
|
||||
|
||||
const data = {}
|
||||
// COMPAT: In IE 11, only plain text can be retrieved from the event's
|
||||
// `clipboardData`. To get HTML, use the browser's native paste action which
|
||||
// can only be handled synchronously. (2017/06/23)
|
||||
if (handler == 'onPaste' && IS_IE) {
|
||||
getHtmlFromNativePaste(event.target, (html) => {
|
||||
const data = html ? { html, type: 'html' } : {}
|
||||
this.props.onPaste(event, data)
|
||||
})
|
||||
|
||||
debug('onBeforeInput', { event, data })
|
||||
this.props.onBeforeInput(event, data)
|
||||
return
|
||||
}
|
||||
|
||||
// If the `onSelect` handler fires while the `isUpdatingSelection` flag is
|
||||
// set it's a result of updating the selection manually, so skip it.
|
||||
if (handler == 'onSelect' && this.tmp.isUpdatingSelection) {
|
||||
return
|
||||
}
|
||||
|
||||
// COMPAT: There are situations where a select event will fire with a new
|
||||
// native selection that resolves to the same internal position. In those
|
||||
// cases we don't need to trigger any changes, since our internal model is
|
||||
// already up to date, but we do want to update the native selection again
|
||||
// to make sure it is in sync. (2017/10/16)
|
||||
if (handler == 'onSelect') {
|
||||
const { state } = this.props
|
||||
const { selection } = state
|
||||
const window = getWindow(event.target)
|
||||
const native = window.getSelection()
|
||||
const range = findRange(native, state)
|
||||
|
||||
if (range && range.equals(selection)) {
|
||||
this.updateSelection()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Some events require being in editable in the editor, so if the event
|
||||
// target isn't, ignore them.
|
||||
if (
|
||||
handler == 'onBeforeInput' ||
|
||||
handler == 'onBlur' ||
|
||||
handler == 'onCompositionEnd' ||
|
||||
handler == 'onCompositionStart' ||
|
||||
handler == 'onCopy' ||
|
||||
handler == 'onCut' ||
|
||||
handler == 'onDragStart' ||
|
||||
handler == 'onFocus' ||
|
||||
handler == 'onInput' ||
|
||||
handler == 'onKeyDown' ||
|
||||
handler == 'onKeyUp' ||
|
||||
handler == 'onPaste' ||
|
||||
handler == 'onSelect'
|
||||
) {
|
||||
if (!this.isInEditor(event.target)) return
|
||||
}
|
||||
|
||||
this.props[handler](event, {})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,554 +326,6 @@ class Content extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On blur, update the selection to be not focused.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onBlur = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isCopying) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
// If the active element is still the editor, the blur event is due to the
|
||||
// window itself being blurred (eg. when changing tabs) so we should ignore
|
||||
// the event, since we want to maintain focus when returning.
|
||||
const window = getWindow(this.element)
|
||||
if (window.document.activeElement == this.element) return
|
||||
|
||||
const data = {}
|
||||
|
||||
debug('onBlur', { event, data })
|
||||
this.props.onBlur(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On focus, update the selection to be focused.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onFocus = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isCopying) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
// COMPAT: If the editor has nested editable elements, the focus can go to
|
||||
// those elements. In Firefox, this must be prevented because it results in
|
||||
// issues with keyboard navigation. (2017/03/30)
|
||||
if (IS_FIREFOX && event.target != this.element) {
|
||||
this.element.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const data = {}
|
||||
|
||||
debug('onFocus', { event, data })
|
||||
this.props.onFocus(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On composition start, set the `isComposing` flag.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onCompositionStart = (event) => {
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
this.tmp.isComposing = true
|
||||
this.tmp.compositions++
|
||||
|
||||
debug('onCompositionStart', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On composition end, remove the `isComposing` flag on the next tick. Also
|
||||
* increment the `forces` key, which will force the contenteditable element
|
||||
* to completely re-render, since IME puts React in an unreconcilable state.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onCompositionEnd = (event) => {
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
this.tmp.forces++
|
||||
const count = this.tmp.compositions
|
||||
|
||||
// The `count` check here ensures that if another composition starts
|
||||
// before the timeout has closed out this one, we will abort unsetting the
|
||||
// `isComposing` flag, since a composition in still in affect.
|
||||
setTimeout(() => {
|
||||
if (this.tmp.compositions > count) return
|
||||
this.tmp.isComposing = false
|
||||
})
|
||||
|
||||
debug('onCompositionEnd', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On copy, defer to `onCutCopy`, then bubble up.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onCopy = (event) => {
|
||||
if (!this.isInEditor(event.target)) return
|
||||
const window = getWindow(event.target)
|
||||
|
||||
this.tmp.isCopying = true
|
||||
window.requestAnimationFrame(() => {
|
||||
this.tmp.isCopying = false
|
||||
})
|
||||
|
||||
const { state } = this.props
|
||||
const data = {}
|
||||
data.type = 'fragment'
|
||||
data.fragment = state.fragment
|
||||
|
||||
debug('onCopy', { event, data })
|
||||
this.props.onCopy(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On cut, defer to `onCutCopy`, then bubble up.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onCut = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
const window = getWindow(event.target)
|
||||
|
||||
this.tmp.isCopying = true
|
||||
window.requestAnimationFrame(() => {
|
||||
this.tmp.isCopying = false
|
||||
})
|
||||
|
||||
const { state } = this.props
|
||||
const data = {}
|
||||
data.type = 'fragment'
|
||||
data.fragment = state.fragment
|
||||
|
||||
debug('onCut', { event, data })
|
||||
this.props.onCut(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On drag end, unset the `isDragging` flag.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onDragEnd = (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
this.tmp.isDragging = false
|
||||
this.tmp.isInternalDrag = null
|
||||
|
||||
debug('onDragEnd', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drag over, set the `isDragging` flag and the `isInternalDrag` flag.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onDragOver = (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (this.tmp.isDragging) return
|
||||
this.tmp.isDragging = true
|
||||
this.tmp.isInternalDrag = false
|
||||
|
||||
debug('onDragOver', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drag start, set the `isDragging` flag and the `isInternalDrag` flag.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onDragStart = (event) => {
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
this.tmp.isDragging = true
|
||||
this.tmp.isInternalDrag = true
|
||||
const { dataTransfer } = event.nativeEvent
|
||||
const data = getTransferData(dataTransfer)
|
||||
|
||||
// If it's a node being dragged, the data type is already set.
|
||||
if (data.type == 'node') return
|
||||
|
||||
const { state } = this.props
|
||||
const { fragment } = state
|
||||
const encoded = Base64.serializeNode(fragment)
|
||||
|
||||
setTransferData(dataTransfer, TRANSFER_TYPES.FRAGMENT, encoded)
|
||||
|
||||
debug('onDragStart', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drop.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onDrop = (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
if (this.props.readOnly) return
|
||||
|
||||
const { state } = this.props
|
||||
const { nativeEvent } = event
|
||||
const { dataTransfer, x, y } = nativeEvent
|
||||
const data = getTransferData(dataTransfer)
|
||||
|
||||
// Resolve a range from the caret position where the drop occured.
|
||||
const window = getWindow(event.target)
|
||||
let range
|
||||
|
||||
// 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
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// Add drop-specific information to the data.
|
||||
data.target = selection
|
||||
|
||||
// COMPAT: Edge throws "Permission denied" errors when
|
||||
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
|
||||
try {
|
||||
data.effect = dataTransfer.dropEffect
|
||||
} catch (err) {
|
||||
data.effect = null
|
||||
}
|
||||
|
||||
if (data.type == 'fragment' || data.type == 'node') {
|
||||
data.isInternal = this.tmp.isInternalDrag
|
||||
}
|
||||
|
||||
debug('onDrop', { event, data })
|
||||
this.props.onDrop(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On input, handle spellcheck and other similar edits that don't go trigger
|
||||
* the `onBeforeInput` and instead update the DOM directly.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onInput = (event) => {
|
||||
if (this.tmp.isComposing) return
|
||||
if (this.props.state.isBlurred) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
debug('onInput', { event })
|
||||
|
||||
const window = getWindow(event.target)
|
||||
const { state, editor } = this.props
|
||||
|
||||
// Get the selection point.
|
||||
const native = window.getSelection()
|
||||
const { anchorNode, anchorOffset } = native
|
||||
const point = findPoint(anchorNode, anchorOffset, state)
|
||||
if (!point) return
|
||||
|
||||
// Get the text node and leaf in question.
|
||||
const { document, selection } = state
|
||||
const node = document.getDescendant(point.key)
|
||||
const leaves = node.getLeaves()
|
||||
let start = 0
|
||||
let end = 0
|
||||
|
||||
const leaf = leaves.find((r) => {
|
||||
end += r.text.length
|
||||
if (end >= point.offset) return true
|
||||
start = end
|
||||
})
|
||||
|
||||
// Get the text information.
|
||||
const { text } = leaf
|
||||
let { textContent } = anchorNode
|
||||
const block = document.getClosestBlock(node.key)
|
||||
const lastText = block.getLastText()
|
||||
const lastLeaf = leaves.last()
|
||||
const lastChar = textContent.charAt(textContent.length - 1)
|
||||
const isLastText = node == lastText
|
||||
const isLastLeaf = leaf == lastLeaf
|
||||
|
||||
// COMPAT: If this is the last leaf, and the DOM text ends in a new line,
|
||||
// we will have added another new line in <Leaf>'s render method to account
|
||||
// for browsers collapsing a single trailing new lines, so remove it.
|
||||
if (isLastText && isLastLeaf && lastChar == '\n') {
|
||||
textContent = textContent.slice(0, -1)
|
||||
}
|
||||
|
||||
// If the text is no different, abort.
|
||||
if (textContent == text) return
|
||||
|
||||
// Determine what the selection should be after changing the text.
|
||||
const delta = textContent.length - text.length
|
||||
const corrected = selection.collapseToEnd().move(delta)
|
||||
const entire = selection.moveAnchorTo(point.key, start).moveFocusTo(point.key, end)
|
||||
|
||||
// Change the current state to have the leaf's text replaced.
|
||||
editor.change((change) => {
|
||||
change
|
||||
.select(entire)
|
||||
.delete()
|
||||
.insertText(textContent, leaf.marks)
|
||||
.select(corrected)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On key down, prevent the default behavior of certain commands that will
|
||||
* leave the editor in an out-of-sync state, then bubble up.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onKeyDown = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
const { key, metaKey, ctrlKey } = event
|
||||
const data = {}
|
||||
const modKey = IS_MAC ? metaKey : ctrlKey
|
||||
|
||||
// COMPAT: add the deprecated keyboard event properties.
|
||||
addDeprecatedKeyProperties(data, event)
|
||||
|
||||
// Keep track of an `isShifting` flag, because it's often used to trigger
|
||||
// "Paste and Match Style" commands, but isn't available on the event in a
|
||||
// normal paste event.
|
||||
if (key == 'Shift') {
|
||||
this.tmp.isShifting = true
|
||||
}
|
||||
|
||||
// When composing, these characters commit the composition but also move the
|
||||
// selection before we're able to handle it, so prevent their default,
|
||||
// selection-moving behavior.
|
||||
if (
|
||||
this.tmp.isComposing &&
|
||||
(key == 'ArrowLeft' || key == 'ArrowRight' || key == 'ArrowUp' || key == 'ArrowDown')
|
||||
) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// These key commands have native behavior in contenteditable elements which
|
||||
// will cause our state to be out of sync, so prevent them.
|
||||
if (
|
||||
(key == 'Enter') ||
|
||||
(key == 'Backspace') ||
|
||||
(key == 'Delete') ||
|
||||
(key == 'b' && modKey) ||
|
||||
(key == 'i' && modKey) ||
|
||||
(key == 'y' && modKey) ||
|
||||
(key == 'z' && modKey) ||
|
||||
(key == 'Z' && modKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
debug('onKeyDown', { event, data })
|
||||
this.props.onKeyDown(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On key up, unset the `isShifting` flag.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onKeyUp = (event) => {
|
||||
const data = {}
|
||||
|
||||
// COMPAT: add the deprecated keyboard event properties.
|
||||
addDeprecatedKeyProperties(data, event)
|
||||
|
||||
if (event.key == 'Shift') {
|
||||
this.tmp.isShifting = false
|
||||
}
|
||||
|
||||
debug('onKeyUp', { event, data })
|
||||
this.props.onKeyUp(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On paste, determine the type and bubble up.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onPaste = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
const data = getTransferData(event.clipboardData)
|
||||
|
||||
// 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 !!this.tmp.isShifting
|
||||
}
|
||||
})
|
||||
|
||||
debug('onPaste', { event, data })
|
||||
|
||||
// COMPAT: In IE 11, only plain text can be retrieved from the event's
|
||||
// `clipboardData`. To get HTML, use the browser's native paste action which
|
||||
// can only be handled synchronously. (2017/06/23)
|
||||
if (IS_IE) {
|
||||
// Do not use `event.preventDefault()` as we need the native paste action.
|
||||
getHtmlFromNativePaste(event.target, (html) => {
|
||||
// If pasted HTML can be retreived, it is added to the `data` object,
|
||||
// setting the `type` to `html`.
|
||||
this.props.onPaste(event, html === undefined ? data : { ...data, html, type: 'html' })
|
||||
})
|
||||
} else {
|
||||
event.preventDefault()
|
||||
this.props.onPaste(event, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On select, update the current state's selection.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onSelect = (event) => {
|
||||
if (this.props.readOnly) return
|
||||
if (this.tmp.isCopying) return
|
||||
if (this.tmp.isComposing) return
|
||||
if (this.tmp.isSelecting) return
|
||||
if (!this.isInEditor(event.target)) return
|
||||
|
||||
const window = getWindow(event.target)
|
||||
const { state } = this.props
|
||||
const { document, selection } = state
|
||||
const native = window.getSelection()
|
||||
const data = {}
|
||||
|
||||
// If there are no ranges, the editor was blurred natively.
|
||||
if (!native.rangeCount) {
|
||||
data.selection = selection.set('isFocused', false)
|
||||
}
|
||||
|
||||
// Otherwise, determine the Slate selection from the native one.
|
||||
else {
|
||||
let range = findRange(native, state)
|
||||
if (!range) return
|
||||
|
||||
// There are situations where a select event will fire with a new native
|
||||
// selection that resolves to the same internal position. In those cases
|
||||
// we don't need to trigger any changes, since our internal model is
|
||||
// already up to date, but we do want to update the native selection again
|
||||
// to make sure it is in sync.
|
||||
if (range.equals(selection)) {
|
||||
this.updateSelection()
|
||||
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)
|
||||
data.selection = range
|
||||
}
|
||||
|
||||
debug('onSelect', { event, data })
|
||||
this.props.onSelect(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor content.
|
||||
*
|
||||
@@ -844,6 +343,11 @@ class Content extends React.Component {
|
||||
return this.renderNode(child, isSelected)
|
||||
})
|
||||
|
||||
const handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
obj[handler] = this[handler]
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
const style = {
|
||||
// Prevent the default outline styles.
|
||||
outline: 'none',
|
||||
@@ -868,14 +372,14 @@ class Content extends React.Component {
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...handlers}
|
||||
data-slate-editor
|
||||
key={this.tmp.forces}
|
||||
key={this.tmp.key}
|
||||
ref={this.ref}
|
||||
data-key={document.key}
|
||||
contentEditable={readOnly ? null : true}
|
||||
suppressContentEditableWarning
|
||||
className={className}
|
||||
onBeforeInput={this.onBeforeInput}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onCompositionEnd={this.onCompositionEnd}
|
||||
@@ -939,38 +443,12 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add deprecated `data` fields from a key `event`.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {Object} event
|
||||
* Mix in handler prop types.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
EVENT_HANDLERS.forEach((handler) => {
|
||||
Content.propTypes[handler] = Types.func.isRequired
|
||||
})
|
||||
|
||||
/**
|
||||
* Export.
|
||||
|
@@ -7,7 +7,9 @@ import Types from 'prop-types'
|
||||
import logger from 'slate-dev-logger'
|
||||
import { Stack, State } from 'slate'
|
||||
|
||||
import CorePlugin from '../plugins/core'
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import AfterPlugin from '../plugins/after'
|
||||
import BeforePlugin from '../plugins/before'
|
||||
import noop from '../utils/noop'
|
||||
|
||||
/**
|
||||
@@ -18,25 +20,6 @@ import noop from '../utils/noop'
|
||||
|
||||
const debug = Debug('slate:editor')
|
||||
|
||||
/**
|
||||
* Event handlers to mix in to the editor.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const EVENT_HANDLERS = [
|
||||
'onBeforeInput',
|
||||
'onBlur',
|
||||
'onFocus',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDrop',
|
||||
'onKeyDown',
|
||||
'onKeyUp',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
/**
|
||||
* Plugin-related properties of the editor.
|
||||
*
|
||||
@@ -121,22 +104,18 @@ class Editor extends React.Component {
|
||||
// Run `onBeforeChange` on the passed-in state because we need to ensure
|
||||
// that it is normalized, and queue the resulting change.
|
||||
const change = props.state.change()
|
||||
stack.onBeforeChange(change, this)
|
||||
stack.handle('onBeforeChange', change, this)
|
||||
const { state } = change
|
||||
this.queueChange(change)
|
||||
this.cacheState(state)
|
||||
this.state.state = state
|
||||
|
||||
// Create a bound event handler for each event.
|
||||
for (let i = 0; i < EVENT_HANDLERS.length; i++) {
|
||||
const method = EVENT_HANDLERS[i]
|
||||
this[method] = (...args) => {
|
||||
const stk = this.state.stack
|
||||
const c = this.state.state.change()
|
||||
stk[method](c, this, ...args)
|
||||
this.onChange(c)
|
||||
EVENT_HANDLERS.forEach((handler) => {
|
||||
this[handler] = (...args) => {
|
||||
this.onEvent(handler, ...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (props.onDocumentChange) {
|
||||
logger.deprecate('0.22.10', 'The `onDocumentChange` prop is deprecated because it led to confusing UX issues, see https://github.com/ianstormtaylor/slate/issues/614#issuecomment-327868679')
|
||||
@@ -169,7 +148,7 @@ class Editor extends React.Component {
|
||||
// Run `onBeforeChange` on the passed-in state because we need to ensure
|
||||
// that it is normalized, and queue the resulting change.
|
||||
const change = props.state.change()
|
||||
stack.onBeforeChange(change, this)
|
||||
stack.handle('onBeforeChange', change, this)
|
||||
const { state } = change
|
||||
this.queueChange(change)
|
||||
this.cacheState(state)
|
||||
@@ -239,7 +218,7 @@ class Editor extends React.Component {
|
||||
*/
|
||||
|
||||
blur = () => {
|
||||
this.change(t => t.blur())
|
||||
this.change(c => c.blur())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,7 +226,7 @@ class Editor extends React.Component {
|
||||
*/
|
||||
|
||||
focus = () => {
|
||||
this.change(t => t.focus())
|
||||
this.change(c => c.focus())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,12 +256,27 @@ class Editor extends React.Component {
|
||||
*/
|
||||
|
||||
change = (fn) => {
|
||||
const change = this.state.state.change()
|
||||
const { state } = this.state
|
||||
const change = state.change()
|
||||
fn(change)
|
||||
debug('change', { change })
|
||||
this.onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* On event.
|
||||
*
|
||||
* @param {String} handler
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
onEvent = (handler, ...args) => {
|
||||
const { stack, state } = this.state
|
||||
const change = state.change()
|
||||
stack.handle(handler, change, this, ...args)
|
||||
this.onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* On change.
|
||||
*
|
||||
@@ -296,8 +290,8 @@ class Editor extends React.Component {
|
||||
|
||||
const { stack } = this.state
|
||||
|
||||
stack.onBeforeChange(change, this)
|
||||
stack.onChange(change, this)
|
||||
stack.handle('onBeforeChange', change, this)
|
||||
stack.handle('onChange', change, this)
|
||||
|
||||
const { state } = change
|
||||
const { document, selection } = this.tmp
|
||||
@@ -326,7 +320,11 @@ class Editor extends React.Component {
|
||||
|
||||
debug('render', { props, state })
|
||||
|
||||
const tree = stack.render(state.state, this, { ...props, children })
|
||||
const tree = stack.render(state.state, this, {
|
||||
...props,
|
||||
children,
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
@@ -352,11 +350,13 @@ class Editor extends React.Component {
|
||||
function resolvePlugins(props) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { state, onChange, plugins = [], ...overridePlugin } = props
|
||||
const corePlugin = CorePlugin(props)
|
||||
const beforePlugin = BeforePlugin(props)
|
||||
const afterPlugin = AfterPlugin(props)
|
||||
return [
|
||||
beforePlugin,
|
||||
overridePlugin,
|
||||
...plugins,
|
||||
corePlugin
|
||||
afterPlugin
|
||||
]
|
||||
}
|
||||
|
||||
|
33
packages/slate-react/src/constants/event-handlers.js
Normal file
33
packages/slate-react/src/constants/event-handlers.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
/**
|
||||
* Event handlers used by Slate plugins.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const EVENT_HANDLERS = [
|
||||
'onBeforeInput',
|
||||
'onBlur',
|
||||
'onCompositionEnd',
|
||||
'onCompositionStart',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDragEnd',
|
||||
'onDragOver',
|
||||
'onDragStart',
|
||||
'onDrop',
|
||||
'onInput',
|
||||
'onFocus',
|
||||
'onKeyDown',
|
||||
'onKeyUp',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
export default EVENT_HANDLERS
|
56
packages/slate-react/src/constants/hotkeys.js
Normal file
56
packages/slate-react/src/constants/hotkeys.js
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import isHotkey from 'is-hotkey'
|
||||
|
||||
import { IS_MAC } from './environment'
|
||||
|
||||
/**
|
||||
* Hotkeys.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const BOLD = isHotkey('mod+b')
|
||||
const ITALIC = isHotkey('mod+i')
|
||||
|
||||
const UNDO = isHotkey('mod+z')
|
||||
const REDO_MAC = isHotkey('mod+shift+z')
|
||||
const REDO_OTHER = isHotkey('mod+y')
|
||||
const REDO = e => IS_MAC ? REDO_MAC(e) : REDO_OTHER(e)
|
||||
|
||||
const DELETE_CHAR_BACKWARD_MAC = isHotkey('ctrl+h')
|
||||
const DELETE_CHAR_FORWARD_MAC = isHotkey('ctrl+d')
|
||||
const DELETE_LINE_FORWARD_MAC = isHotkey('ctrl+k')
|
||||
const DELETE_CHAR_BACKWARD = e => IS_MAC ? DELETE_CHAR_BACKWARD_MAC(e) : false
|
||||
const DELETE_CHAR_FORWARD = e => IS_MAC ? DELETE_CHAR_FORWARD_MAC(e) : false
|
||||
const DELETE_LINE_FORWARD = e => IS_MAC ? DELETE_LINE_FORWARD_MAC(e) : false
|
||||
|
||||
const CONTENTEDITABLE = e => (
|
||||
e.key == 'Backspace' ||
|
||||
e.key == 'Delete' ||
|
||||
e.key == 'Enter' ||
|
||||
e.key == 'Insert' ||
|
||||
BOLD(e) ||
|
||||
DELETE_CHAR_BACKWARD(e) ||
|
||||
DELETE_CHAR_FORWARD(e) ||
|
||||
DELETE_LINE_FORWARD(e) ||
|
||||
ITALIC(e) ||
|
||||
REDO(e) ||
|
||||
UNDO(e)
|
||||
)
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
export default {
|
||||
BOLD,
|
||||
CONTENTEDITABLE,
|
||||
DELETE_CHAR_BACKWARD,
|
||||
DELETE_CHAR_FORWARD,
|
||||
DELETE_LINE_FORWARD,
|
||||
ITALIC,
|
||||
REDO,
|
||||
UNDO,
|
||||
}
|
@@ -6,10 +6,13 @@ import React from 'react'
|
||||
import getWindow from 'get-window'
|
||||
import { Block, Inline, coreSchema } from 'slate'
|
||||
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import HOTKEYS from '../constants/hotkeys'
|
||||
import Content from '../components/content'
|
||||
import Placeholder from '../components/placeholder'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
import { IS_CHROME, IS_MAC, IS_SAFARI, SUPPORTED_EVENTS } from '../constants/environment'
|
||||
import findPoint from '../utils/find-point'
|
||||
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -17,10 +20,10 @@ import { IS_CHROME, IS_MAC, IS_SAFARI, SUPPORTED_EVENTS } from '../constants/env
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:core')
|
||||
const debug = Debug('slate:core:after')
|
||||
|
||||
/**
|
||||
* The default plugin.
|
||||
* The after plugin.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @property {Element} placeholder
|
||||
@@ -29,7 +32,7 @@ const debug = Debug('slate:core')
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function Plugin(options = {}) {
|
||||
function AfterPlugin(options = {}) {
|
||||
const {
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
@@ -40,7 +43,7 @@ function Plugin(options = {}) {
|
||||
* On before change, enforce the editor's schema.
|
||||
*
|
||||
* @param {Change} change
|
||||
* @param {Editor} schema
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onBeforeChange(change, editor) {
|
||||
@@ -60,35 +63,26 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On before input, correct any browser inconsistencies.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onBeforeInput(e, data, change) {
|
||||
function onBeforeInput(event, data, change) {
|
||||
debug('onBeforeInput', { data })
|
||||
|
||||
// React's `onBeforeInput` synthetic event is based on the native `keypress`
|
||||
// and `textInput` events. In browsers that support the native `beforeinput`
|
||||
// event, we instead use that event to trigger text insertion, since it
|
||||
// provides more useful information about the range being affected and also
|
||||
// preserves compatibility with iOS autocorrect, which would be broken if we
|
||||
// called `preventDefault()` on React's synthetic event here.
|
||||
if (SUPPORTED_EVENTS.beforeinput) return
|
||||
|
||||
e.preventDefault()
|
||||
change.insertText(e.data)
|
||||
event.preventDefault()
|
||||
change.insertText(event.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* On blur.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onBlur(e, data, change) {
|
||||
function onBlur(event, data, change) {
|
||||
debug('onBlur', { data })
|
||||
change.blur()
|
||||
}
|
||||
@@ -96,48 +90,47 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On copy.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onCopy(e, data, change) {
|
||||
function onCopy(event, data, change) {
|
||||
debug('onCopy', data)
|
||||
onCutOrCopy(e, data, change)
|
||||
onCutOrCopy(event, data, change)
|
||||
}
|
||||
|
||||
/**
|
||||
* On cut.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onCut(e, data, change, editor) {
|
||||
function onCut(event, data, change, editor) {
|
||||
debug('onCut', data)
|
||||
onCutOrCopy(e, data, change)
|
||||
const window = getWindow(e.target)
|
||||
onCutOrCopy(event, data, change)
|
||||
const window = getWindow(event.target)
|
||||
|
||||
// Once the fake cut content has successfully been added to the clipboard,
|
||||
// delete the content in the current selection.
|
||||
window.requestAnimationFrame(() => {
|
||||
editor.change(t => t.delete())
|
||||
editor.change(c => c.delete())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On cut or copy, create a fake selection so that we can add a Base 64
|
||||
* encoded copy of the fragment to the HTML, to decode on future pastes.
|
||||
* On cut or copy.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onCutOrCopy(e, data, change) {
|
||||
const window = getWindow(e.target)
|
||||
function onCutOrCopy(event, data, change) {
|
||||
const window = getWindow(event.target)
|
||||
const native = window.getSelection()
|
||||
const { state } = change
|
||||
const { startKey, endKey, startText, endBlock, endInline } = state
|
||||
@@ -148,6 +141,8 @@ function Plugin(options = {}) {
|
||||
// If the selection is collapsed, and it isn't inside a void node, abort.
|
||||
if (native.isCollapsed && !isVoid) return
|
||||
|
||||
// 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 encoded = Base64.serializeNode(fragment)
|
||||
const range = native.getRangeAt(0)
|
||||
@@ -241,34 +236,34 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On drop.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onDrop(e, data, change) {
|
||||
function onDrop(event, data, change) {
|
||||
debug('onDrop', { data })
|
||||
|
||||
switch (data.type) {
|
||||
case 'text':
|
||||
case 'html':
|
||||
return onDropText(e, data, change)
|
||||
return onDropText(event, data, change)
|
||||
case 'fragment':
|
||||
return onDropFragment(e, data, change)
|
||||
return onDropFragment(event, data, change)
|
||||
case 'node':
|
||||
return onDropNode(e, data, change)
|
||||
return onDropNode(event, data, change)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On drop node, insert the node wherever it is dropped.
|
||||
* On drop node.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onDropNode(e, data, change) {
|
||||
function onDropNode(event, data, change) {
|
||||
debug('onDropNode', { data })
|
||||
|
||||
const { state } = change
|
||||
@@ -311,12 +306,12 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On drop fragment.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onDropFragment(e, data, change) {
|
||||
function onDropFragment(event, data, change) {
|
||||
debug('onDropFragment', { data })
|
||||
|
||||
const { state } = change
|
||||
@@ -346,14 +341,14 @@ function Plugin(options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* On drop text, split the blocks at new lines.
|
||||
* On drop text.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onDropText(e, data, change) {
|
||||
function onDropText(event, data, change) {
|
||||
debug('onDropText', { data })
|
||||
|
||||
const { state } = change
|
||||
@@ -386,43 +381,122 @@ function Plugin(options = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On input.
|
||||
*
|
||||
* @param {Event} eventvent
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onInput(event, data, change, editor) {
|
||||
const window = getWindow(event.target)
|
||||
const { state } = change
|
||||
|
||||
// Get the selection point.
|
||||
const native = window.getSelection()
|
||||
const { anchorNode, anchorOffset } = native
|
||||
const point = findPoint(anchorNode, anchorOffset, state)
|
||||
if (!point) return
|
||||
|
||||
// Get the text node and leaf in question.
|
||||
const { document, selection } = state
|
||||
const node = document.getDescendant(point.key)
|
||||
const leaves = node.getLeaves()
|
||||
let start = 0
|
||||
let end = 0
|
||||
|
||||
const leaf = leaves.find((r) => {
|
||||
end += r.text.length
|
||||
if (end >= point.offset) return true
|
||||
start = end
|
||||
})
|
||||
|
||||
// Get the text information.
|
||||
const { text } = leaf
|
||||
let { textContent } = anchorNode
|
||||
const block = document.getClosestBlock(node.key)
|
||||
const lastText = block.getLastText()
|
||||
const lastLeaf = leaves.last()
|
||||
const lastChar = textContent.charAt(textContent.length - 1)
|
||||
const isLastText = node == lastText
|
||||
const isLastLeaf = leaf == lastLeaf
|
||||
|
||||
// COMPAT: If this is the last leaf, and the DOM text ends in a new line,
|
||||
// we will have added another new line in <Leaf>'s render method to account
|
||||
// for browsers collapsing a single trailing new lines, so remove it.
|
||||
if (isLastText && isLastLeaf && lastChar == '\n') {
|
||||
textContent = textContent.slice(0, -1)
|
||||
}
|
||||
|
||||
// If the text is no different, abort.
|
||||
if (textContent == text) return
|
||||
|
||||
// Determine what the selection should be after changing the text.
|
||||
const delta = textContent.length - text.length
|
||||
const corrected = selection.collapseToEnd().move(delta)
|
||||
const entire = selection.moveAnchorTo(point.key, start).moveFocusTo(point.key, end)
|
||||
|
||||
// Change the current state to have the leaf's text replaced.
|
||||
change
|
||||
.select(entire)
|
||||
.delete()
|
||||
.insertText(textContent, leaf.marks)
|
||||
.select(corrected)
|
||||
}
|
||||
|
||||
/**
|
||||
* On key down.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDown(e, data, change) {
|
||||
function onKeyDown(event, data, change) {
|
||||
debug('onKeyDown', { data })
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter': return onKeyDownEnter(e, data, change)
|
||||
case 'Backspace': return onKeyDownBackspace(e, data, change)
|
||||
case 'Delete': return onKeyDownDelete(e, data, change)
|
||||
case 'ArrowLeft': return onKeyDownLeft(e, data, change)
|
||||
case 'ArrowRight': return onKeyDownRight(e, data, change)
|
||||
case 'ArrowUp': return onKeyDownUp(e, data, change)
|
||||
case 'ArrowDown': return onKeyDownDown(e, data, change)
|
||||
case 'd': return onKeyDownD(e, data, change)
|
||||
case 'h': return onKeyDownH(e, data, change)
|
||||
case 'k': return onKeyDownK(e, data, change)
|
||||
case 'y': return onKeyDownY(e, data, change)
|
||||
case 'z':
|
||||
case 'Z': return onKeyDownZ(e, data, change)
|
||||
switch (event.key) {
|
||||
case 'Enter': return onKeyDownEnter(event, data, change)
|
||||
case 'Backspace': return onKeyDownBackspace(event, data, change)
|
||||
case 'Delete': return onKeyDownDelete(event, data, change)
|
||||
case 'ArrowLeft': return onKeyDownLeft(event, data, change)
|
||||
case 'ArrowRight': return onKeyDownRight(event, data, change)
|
||||
case 'ArrowUp': return onKeyDownUp(event, data, change)
|
||||
case 'ArrowDown': return onKeyDownDown(event, data, change)
|
||||
}
|
||||
|
||||
if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) {
|
||||
change.deleteCharBackward()
|
||||
}
|
||||
|
||||
if (HOTKEYS.DELETE_CHAR_FORWARD(event)) {
|
||||
change.deleteCharForward()
|
||||
}
|
||||
|
||||
if (HOTKEYS.DELETE_LINE_FORWARD(event)) {
|
||||
change.deleteLineForward()
|
||||
}
|
||||
|
||||
if (HOTKEYS.REDO(event)) {
|
||||
change.redo()
|
||||
}
|
||||
|
||||
if (HOTKEYS.UNDO(event)) {
|
||||
change.undo()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On `enter` key down, split the current block in half.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownEnter(e, data, change) {
|
||||
function onKeyDownEnter(event, data, change) {
|
||||
const { state } = change
|
||||
const { document, startKey } = state
|
||||
const hasVoidParent = document.hasVoidParent(startKey)
|
||||
@@ -442,14 +516,14 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On `backspace` key down, delete backwards.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownBackspace(e, data, change) {
|
||||
const isWord = IS_MAC ? e.altKey : e.ctrlKey
|
||||
const isLine = IS_MAC ? e.metaKey : false
|
||||
function onKeyDownBackspace(event, data, change) {
|
||||
const isWord = IS_MAC ? event.altKey : event.ctrlKey
|
||||
const isLine = IS_MAC ? event.metaKey : false
|
||||
|
||||
let boundary = 'Char'
|
||||
if (isWord) boundary = 'Word'
|
||||
@@ -461,14 +535,14 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On `delete` key down, delete forwards.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownDelete(e, data, change) {
|
||||
const isWord = IS_MAC ? e.altKey : e.ctrlKey
|
||||
const isLine = IS_MAC ? e.metaKey : false
|
||||
function onKeyDownDelete(event, data, change) {
|
||||
const isWord = IS_MAC ? event.altKey : event.ctrlKey
|
||||
const isLine = IS_MAC ? event.metaKey : false
|
||||
|
||||
let boundary = 'Char'
|
||||
if (isWord) boundary = 'Word'
|
||||
@@ -487,16 +561,16 @@ function Plugin(options = {}) {
|
||||
* surrounded by empty text nodes with zero-width spaces in them. Without this
|
||||
* the zero-width spaces will cause two arrow keys to jump to the next text.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownLeft(e, data, change) {
|
||||
function onKeyDownLeft(event, data, change) {
|
||||
const { state } = change
|
||||
|
||||
if (e.ctrlKey) return
|
||||
if (e.altKey) return
|
||||
if (event.ctrlKey) return
|
||||
if (event.altKey) return
|
||||
if (state.isExpanded) return
|
||||
|
||||
const { document, startKey, startText } = state
|
||||
@@ -505,7 +579,7 @@ function Plugin(options = {}) {
|
||||
// If the current text node is empty, or we're inside a void parent, we're
|
||||
// going to need to handle the selection behavior.
|
||||
if (startText.text == '' || hasVoidParent) {
|
||||
e.preventDefault()
|
||||
event.preventDefault()
|
||||
const previous = document.getPreviousText(startKey)
|
||||
|
||||
// If there's no previous text node in the document, abort.
|
||||
@@ -518,7 +592,7 @@ function Plugin(options = {}) {
|
||||
const previousInline = document.getClosestInline(previous.key)
|
||||
|
||||
if (previousBlock === startBlock && previousInline && !previousInline.isVoid) {
|
||||
const extendOrMove = e.shiftKey ? 'extend' : 'move'
|
||||
const extendOrMove = event.shiftKey ? 'extend' : 'move'
|
||||
change.collapseToEndOf(previous)[extendOrMove](-1)
|
||||
return
|
||||
}
|
||||
@@ -543,16 +617,16 @@ function Plugin(options = {}) {
|
||||
* of a previous inline node, which screws us up, so we never want to set the
|
||||
* selection to the very start of an inline node here. (2016/11/29)
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownRight(e, data, change) {
|
||||
function onKeyDownRight(event, data, change) {
|
||||
const { state } = change
|
||||
|
||||
if (e.ctrlKey) return
|
||||
if (e.altKey) return
|
||||
if (event.ctrlKey) return
|
||||
if (event.altKey) return
|
||||
if (state.isExpanded) return
|
||||
|
||||
const { document, startKey, startText } = state
|
||||
@@ -561,7 +635,7 @@ function Plugin(options = {}) {
|
||||
// If the current text node is empty, or we're inside a void parent, we're
|
||||
// going to need to handle the selection behavior.
|
||||
if (startText.text == '' || hasVoidParent) {
|
||||
e.preventDefault()
|
||||
event.preventDefault()
|
||||
const next = document.getNextText(startKey)
|
||||
|
||||
// If there's no next text node in the document, abort.
|
||||
@@ -580,7 +654,7 @@ function Plugin(options = {}) {
|
||||
const nextInline = document.getClosestInline(next.key)
|
||||
|
||||
if (nextBlock == startBlock && nextInline) {
|
||||
const extendOrMove = e.shiftKey ? 'extend' : 'move'
|
||||
const extendOrMove = event.shiftKey ? 'extend' : 'move'
|
||||
change.collapseToStartOf(next)[extendOrMove](1)
|
||||
return
|
||||
}
|
||||
@@ -597,17 +671,17 @@ function Plugin(options = {}) {
|
||||
* Chrome, option-shift-up doesn't properly extend the selection. And in
|
||||
* Firefox, option-up doesn't properly move the selection.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownUp(e, data, change) {
|
||||
if (!IS_MAC || e.ctrlKey || !e.altKey) return
|
||||
function onKeyDownUp(event, data, change) {
|
||||
if (!IS_MAC || event.ctrlKey || !event.altKey) return
|
||||
|
||||
const { state } = change
|
||||
const { selection, document, focusKey, focusBlock } = state
|
||||
const transform = e.shiftKey ? 'extendToStartOf' : 'collapseToStartOf'
|
||||
const transform = event.shiftKey ? 'extendToStartOf' : 'collapseToStartOf'
|
||||
const block = selection.hasFocusAtStartOf(focusBlock)
|
||||
? document.getPreviousBlock(focusKey)
|
||||
: focusBlock
|
||||
@@ -615,7 +689,7 @@ function Plugin(options = {}) {
|
||||
if (!block) return
|
||||
const text = block.getFirstText()
|
||||
|
||||
e.preventDefault()
|
||||
event.preventDefault()
|
||||
change[transform](text)
|
||||
}
|
||||
|
||||
@@ -626,17 +700,17 @@ function Plugin(options = {}) {
|
||||
* Chrome, option-shift-down doesn't properly extend the selection. And in
|
||||
* Firefox, option-down doesn't properly move the selection.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownDown(e, data, change) {
|
||||
if (!IS_MAC || e.ctrlKey || !e.altKey) return
|
||||
function onKeyDownDown(event, data, change) {
|
||||
if (!IS_MAC || event.ctrlKey || !event.altKey) return
|
||||
|
||||
const { state } = change
|
||||
const { selection, document, focusKey, focusBlock } = state
|
||||
const transform = e.shiftKey ? 'extendToEndOf' : 'collapseToEndOf'
|
||||
const transform = event.shiftKey ? 'extendToEndOf' : 'collapseToEndOf'
|
||||
const block = selection.hasFocusAtEndOf(focusBlock)
|
||||
? document.getNextBlock(focusKey)
|
||||
: focusBlock
|
||||
@@ -644,109 +718,39 @@ function Plugin(options = {}) {
|
||||
if (!block) return
|
||||
const text = block.getLastText()
|
||||
|
||||
e.preventDefault()
|
||||
event.preventDefault()
|
||||
change[transform](text)
|
||||
}
|
||||
|
||||
/**
|
||||
* On `d` key down, for Macs, delete one character forward.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownD(e, data, change) {
|
||||
if (!IS_MAC || !e.ctrlKey || e.altKey) return
|
||||
e.preventDefault()
|
||||
change.deleteCharForward()
|
||||
}
|
||||
|
||||
/**
|
||||
* On `h` key down, for Macs, delete until the end of the line.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownH(e, data, change) {
|
||||
if (!IS_MAC || !e.ctrlKey || e.altKey) return
|
||||
e.preventDefault()
|
||||
change.deleteCharBackward()
|
||||
}
|
||||
|
||||
/**
|
||||
* On `k` key down, for Macs, delete until the end of the line.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownK(e, data, change) {
|
||||
if (!IS_MAC || !e.ctrlKey || e.altKey) return
|
||||
e.preventDefault()
|
||||
change.deleteLineForward()
|
||||
}
|
||||
|
||||
/**
|
||||
* On `y` key down, redo.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownY(e, data, change) {
|
||||
const modKey = IS_MAC ? e.metaKey : e.ctrlKey
|
||||
if (!modKey) return
|
||||
change.redo()
|
||||
}
|
||||
|
||||
/**
|
||||
* On `z` key down, undo or redo.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onKeyDownZ(e, data, change) {
|
||||
const modKey = IS_MAC ? e.metaKey : e.ctrlKey
|
||||
if (!modKey) return
|
||||
change[e.shiftKey ? 'redo' : 'undo']()
|
||||
}
|
||||
|
||||
/**
|
||||
* On paste.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onPaste(e, data, change) {
|
||||
function onPaste(event, data, change) {
|
||||
debug('onPaste', { data })
|
||||
|
||||
switch (data.type) {
|
||||
case 'fragment':
|
||||
return onPasteFragment(e, data, change)
|
||||
return onPasteFragment(event, data, change)
|
||||
case 'text':
|
||||
case 'html':
|
||||
return onPasteText(e, data, change)
|
||||
return onPasteText(event, data, change)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On paste fragment.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onPasteFragment(e, data, change) {
|
||||
function onPasteFragment(event, data, change) {
|
||||
debug('onPasteFragment', { data })
|
||||
change.insertFragment(data.fragment)
|
||||
}
|
||||
@@ -754,12 +758,12 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On paste text, split blocks at new lines.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onPasteText(e, data, change) {
|
||||
function onPasteText(event, data, change) {
|
||||
debug('onPasteText', { data })
|
||||
|
||||
const { state } = change
|
||||
@@ -776,12 +780,12 @@ function Plugin(options = {}) {
|
||||
/**
|
||||
* On select.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
function onSelect(e, data, change) {
|
||||
function onSelect(event, data, change) {
|
||||
debug('onSelect', { data })
|
||||
change.select(data.selection)
|
||||
}
|
||||
@@ -796,23 +800,19 @@ function Plugin(options = {}) {
|
||||
*/
|
||||
|
||||
function render(props, state, editor) {
|
||||
const handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
obj[handler] = editor[handler]
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<Content
|
||||
{...handlers}
|
||||
autoCorrect={props.autoCorrect}
|
||||
autoFocus={props.autoFocus}
|
||||
className={props.className}
|
||||
children={props.children}
|
||||
editor={editor}
|
||||
onBeforeInput={editor.onBeforeInput}
|
||||
onBlur={editor.onBlur}
|
||||
onFocus={editor.onFocus}
|
||||
onCopy={editor.onCopy}
|
||||
onCut={editor.onCut}
|
||||
onDrop={editor.onDrop}
|
||||
onKeyDown={editor.onKeyDown}
|
||||
onKeyUp={editor.onKeyUp}
|
||||
onPaste={editor.onPaste}
|
||||
onSelect={editor.onSelect}
|
||||
readOnly={props.readOnly}
|
||||
role={props.role}
|
||||
schema={editor.getSchema()}
|
||||
@@ -888,7 +888,7 @@ function Plugin(options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the core plugin.
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
@@ -900,6 +900,7 @@ function Plugin(options = {}) {
|
||||
onCopy,
|
||||
onCut,
|
||||
onDrop,
|
||||
onInput,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
onSelect,
|
||||
@@ -914,4 +915,4 @@ function Plugin(options = {}) {
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
export default Plugin
|
||||
export default AfterPlugin
|
591
packages/slate-react/src/plugins/before.js
Normal file
591
packages/slate-react/src/plugins/before.js
Normal file
@@ -0,0 +1,591 @@
|
||||
|
||||
import Base64 from 'slate-base64-serializer'
|
||||
import Debug from 'debug'
|
||||
import getWindow from 'get-window'
|
||||
import keycode from 'keycode'
|
||||
import logger from 'slate-dev-logger'
|
||||
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 setTransferData from '../utils/set-transfer-data'
|
||||
import { IS_FIREFOX, IS_MAC, SUPPORTED_EVENTS } from '../constants/environment'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:core:before')
|
||||
|
||||
/**
|
||||
* The core before plugin.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function BeforePlugin() {
|
||||
let compositionCount = 0
|
||||
let isComposing = false
|
||||
let isCopying = false
|
||||
let isDragging = false
|
||||
let isShifting = false
|
||||
let isInternalDrag = null
|
||||
|
||||
/**
|
||||
* On before input.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onBeforeInput(event, data, change, editor) {
|
||||
if (editor.props.readOnly) return true
|
||||
|
||||
// COMPAT: React's `onBeforeInput` synthetic event is based on the native
|
||||
// `keypress` and `textInput` events. In browsers that support the native
|
||||
// `beforeinput` event, we instead use that event to trigger text insertion,
|
||||
// since it provides more useful information about the range being affected
|
||||
// and also preserves compatibility with iOS autocorrect, which would be
|
||||
// broken if we called `preventDefault()` on React's synthetic event here.
|
||||
if (SUPPORTED_EVENTS.beforeinput) return true
|
||||
|
||||
debug('onBeforeInput', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On blur.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onBlur(event, data, change, editor) {
|
||||
if (isCopying) return true
|
||||
if (editor.props.readOnly) return true
|
||||
|
||||
// If the active element is still the editor, the blur event is due to the
|
||||
// window itself being blurred (eg. when changing tabs) so we should ignore
|
||||
// the event, since we want to maintain focus when returning.
|
||||
const el = findDOMNode(editor)
|
||||
const window = getWindow(el)
|
||||
if (window.document.activeElement == el) return true
|
||||
|
||||
debug('onBlur', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On composition end.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onCompositionEnd(event, data, change, editor) {
|
||||
const n = compositionCount
|
||||
|
||||
// The `count` check here ensures that if another composition starts
|
||||
// before the timeout has closed out this one, we will abort unsetting the
|
||||
// `isComposing` flag, since a composition is still in affect.
|
||||
setTimeout(() => {
|
||||
if (compositionCount > n) return
|
||||
isComposing = false
|
||||
})
|
||||
|
||||
debug('onCompositionEnd', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On composition start.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onCompositionStart(event, data, change, editor) {
|
||||
isComposing = true
|
||||
compositionCount++
|
||||
|
||||
debug('onCompositionStart', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On copy.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onCopy(event, data, change, editor) {
|
||||
const window = getWindow(event.target)
|
||||
isCopying = true
|
||||
window.requestAnimationFrame(() => isCopying = false)
|
||||
|
||||
const { state } = change
|
||||
data.type = 'fragment'
|
||||
data.fragment = state.fragment
|
||||
|
||||
debug('onCopy', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On cut.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onCut(event, data, change, editor) {
|
||||
if (editor.props.readOnly) return true
|
||||
|
||||
const window = getWindow(event.target)
|
||||
isCopying = true
|
||||
window.requestAnimationFrame(() => isCopying = false)
|
||||
|
||||
const { state } = change
|
||||
data.type = 'fragment'
|
||||
data.fragment = state.fragment
|
||||
|
||||
debug('onCut', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drag end.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onDragEnd(event, data, change, editor) {
|
||||
event.stopPropagation()
|
||||
isDragging = false
|
||||
isInternalDrag = null
|
||||
|
||||
debug('onDragEnd', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drag over.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onDragOver(event, data, change, editor) {
|
||||
if (isDragging) return true
|
||||
event.stopPropagation()
|
||||
isDragging = true
|
||||
isInternalDrag = false
|
||||
|
||||
debug('onDragOver', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drag start.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onDragStart(event, data, change, editor) {
|
||||
isDragging = true
|
||||
isInternalDrag = true
|
||||
|
||||
const { dataTransfer } = event.nativeEvent
|
||||
const d = getTransferData(dataTransfer)
|
||||
Object.assign(data, d)
|
||||
|
||||
if (data.type != 'node') {
|
||||
const { state } = this.props
|
||||
const { fragment } = state
|
||||
const encoded = Base64.serializeNode(fragment)
|
||||
setTransferData(dataTransfer, TRANSFER_TYPES.FRAGMENT, encoded)
|
||||
}
|
||||
|
||||
debug('onDragStart', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On drop.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onDrop(event, data, change, editor) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
if (editor.props.readOnly) return
|
||||
|
||||
const { state } = change
|
||||
const { nativeEvent } = event
|
||||
const { dataTransfer, x, y } = nativeEvent
|
||||
const d = getTransferData(dataTransfer)
|
||||
Object.assign(data, d)
|
||||
|
||||
// Resolve a range from the caret position where the drop occured.
|
||||
const window = getWindow(event.target)
|
||||
let range
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Add drop-specific information to the data.
|
||||
data.target = selection
|
||||
|
||||
// COMPAT: Edge throws "Permission denied" errors when
|
||||
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
|
||||
try {
|
||||
data.effect = dataTransfer.dropEffect
|
||||
} catch (err) {
|
||||
data.effect = null
|
||||
}
|
||||
|
||||
if (d.type == 'fragment' || d.type == 'node') {
|
||||
data.isInternal = isInternalDrag
|
||||
}
|
||||
|
||||
debug('onDrop', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On focus.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onFocus(event, data, change, editor) {
|
||||
if (isCopying) return true
|
||||
if (editor.props.readOnly) return true
|
||||
|
||||
const el = findDOMNode(editor)
|
||||
|
||||
// COMPAT: If the editor has nested editable elements, the focus can go to
|
||||
// those elements. In Firefox, this must be prevented because it results in
|
||||
// issues with keyboard navigation. (2017/03/30)
|
||||
if (IS_FIREFOX && event.target != el) {
|
||||
el.focus()
|
||||
return true
|
||||
}
|
||||
|
||||
debug('onFocus', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On input.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onInput(event, data, change, editor) {
|
||||
if (isComposing) return true
|
||||
if (change.state.isBlurred) return true
|
||||
|
||||
debug('onInput', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On key down.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onKeyDown(event, data, change, editor) {
|
||||
if (editor.props.readOnly) return
|
||||
|
||||
const { key } = event
|
||||
|
||||
// When composing, these characters commit the composition but also move the
|
||||
// selection before we're able to handle it, so prevent their default,
|
||||
// selection-moving behavior.
|
||||
if (
|
||||
isComposing &&
|
||||
(key == 'ArrowLeft' || key == 'ArrowRight' || key == 'ArrowUp' || key == 'ArrowDown')
|
||||
) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
// Certain hotkeys have native behavior in contenteditable elements which
|
||||
// will cause our state to be out of sync, so prevent them.
|
||||
if (HOTKEYS.CONTENTEDITABLE(event)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Keep track of an `isShifting` flag, because it's often used to trigger
|
||||
// "Paste and Match Style" commands, but isn't available on the event in a
|
||||
// normal paste event.
|
||||
if (key == 'Shift') {
|
||||
isShifting = true
|
||||
}
|
||||
|
||||
// COMPAT: add the deprecated keyboard event properties.
|
||||
addDeprecatedKeyProperties(data, event)
|
||||
|
||||
debug('onKeyDown', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On key up.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onKeyUp(event, data, change, editor) {
|
||||
// COMPAT: add the deprecated keyboard event properties.
|
||||
addDeprecatedKeyProperties(data, event)
|
||||
|
||||
if (event.key == 'Shift') {
|
||||
isShifting = false
|
||||
}
|
||||
|
||||
debug('onKeyUp', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On paste.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onPaste(event, data, change, editor) {
|
||||
if (editor.props.readOnly) return
|
||||
|
||||
event.preventDefault()
|
||||
const d = getTransferData(event.clipboardData)
|
||||
Object.assign(data, d)
|
||||
|
||||
// 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
|
||||
}
|
||||
})
|
||||
|
||||
debug('onPaste', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On select.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Object} data
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
function onSelect(event, data, change, editor) {
|
||||
if (isCopying) return
|
||||
if (isComposing) return
|
||||
if (editor.props.readOnly) return
|
||||
|
||||
const window = getWindow(event.target)
|
||||
const { state } = change
|
||||
const { document, selection } = state
|
||||
const native = window.getSelection()
|
||||
|
||||
// If there are no ranges, the editor was blurred natively.
|
||||
if (!native.rangeCount) {
|
||||
data.selection = selection.blur()
|
||||
}
|
||||
|
||||
// Otherwise, determine the Slate selection from the native one.
|
||||
else {
|
||||
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)
|
||||
data.selection = range
|
||||
}
|
||||
|
||||
debug('onSelect', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
return {
|
||||
onBeforeInput,
|
||||
onBlur,
|
||||
onCompositionEnd,
|
||||
onCompositionStart,
|
||||
onCopy,
|
||||
onCut,
|
||||
onDragEnd,
|
||||
onDragOver,
|
||||
onDragStart,
|
||||
onDrop,
|
||||
onFocus,
|
||||
onInput,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
onPaste,
|
||||
onSelect,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add deprecated `data` fields from a key `event`.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {Object} event
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
export default BeforePlugin
|
@@ -1,5 +1,6 @@
|
||||
|
||||
import CorePlugin from '../../src/plugins/core'
|
||||
import AfterPlugin from '../../src/plugins/after'
|
||||
import BeforePlugin from '../../src/plugins/before'
|
||||
import Simulator from 'slate-simulator'
|
||||
import assert from 'assert'
|
||||
import fs from 'fs'
|
||||
@@ -11,7 +12,7 @@ import { basename, extname, resolve } from 'path'
|
||||
*/
|
||||
|
||||
describe('plugins', () => {
|
||||
describe('core', () => {
|
||||
describe.skip('core', () => {
|
||||
const dir = resolve(__dirname, 'core')
|
||||
const events = fs.readdirSync(dir).filter(e => e[0] != '.' && e != 'index.js')
|
||||
|
||||
@@ -25,7 +26,7 @@ describe('plugins', () => {
|
||||
const module = require(resolve(testDir, test))
|
||||
const { input, output, props = {}} = module
|
||||
const fn = module.default
|
||||
const plugins = [CorePlugin(props)]
|
||||
const plugins = [BeforePlugin(props), AfterPlugin(props)]
|
||||
const simulator = new Simulator({ plugins, state: input })
|
||||
fn(simulator)
|
||||
|
||||
|
@@ -33,8 +33,10 @@ class Simulator {
|
||||
* @param {Object} attrs
|
||||
*/
|
||||
|
||||
constructor({ plugins, state }) {
|
||||
constructor(props) {
|
||||
const { plugins, state } = props
|
||||
const stack = new Stack({ plugins })
|
||||
this.props = props
|
||||
this.stack = stack
|
||||
this.state = state
|
||||
}
|
||||
@@ -53,13 +55,13 @@ EVENT_HANDLERS.forEach((handler) => {
|
||||
if (data == null) data = {}
|
||||
|
||||
const { stack, state } = this
|
||||
const editor = createEditor(stack, state)
|
||||
const editor = createEditor(this)
|
||||
const event = createEvent(e)
|
||||
const change = state.change()
|
||||
|
||||
stack[handler](change, editor, event, data)
|
||||
stack.onBeforeChange(change, editor)
|
||||
stack.onChange(change, editor)
|
||||
stack.handle(handler, change, editor, event, data)
|
||||
stack.handle('onBeforeChange', change, editor)
|
||||
stack.handle('onChange', change, editor)
|
||||
|
||||
this.state = change.state
|
||||
return this
|
||||
@@ -84,11 +86,22 @@ function getMethodName(handler) {
|
||||
* @param {State} state
|
||||
*/
|
||||
|
||||
function createEditor(stack, state) {
|
||||
return {
|
||||
function createEditor({ stack, state, props }) {
|
||||
const editor = {
|
||||
getSchema: () => stack.schema,
|
||||
getState: () => state,
|
||||
props: {
|
||||
autoCorrect: true,
|
||||
autoFocus: false,
|
||||
onChange: () => {},
|
||||
readOnly: false,
|
||||
spellCheck: true,
|
||||
...props,
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -13,27 +13,6 @@ import Schema from './schema'
|
||||
|
||||
const debug = Debug('slate:stack')
|
||||
|
||||
/**
|
||||
* Methods that are triggered on events and can change the state.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const METHODS = [
|
||||
'onBeforeInput',
|
||||
'onBeforeChange',
|
||||
'onBlur',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDrop',
|
||||
'onFocus',
|
||||
'onKeyDown',
|
||||
'onKeyUp',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
'onChange',
|
||||
]
|
||||
|
||||
/**
|
||||
* Default properties.
|
||||
*
|
||||
@@ -90,6 +69,27 @@ class Stack extends Record(DEFAULTS) {
|
||||
return 'stack'
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke an event `handler` on all of the plugins, until one of them decides
|
||||
* to stop propagation.
|
||||
*
|
||||
* @param {String} handler
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
handle(handler, change, editor, ...args) {
|
||||
debug(handler)
|
||||
|
||||
for (let k = 0; k < this.plugins.length; k++) {
|
||||
const plugin = this.plugins[k]
|
||||
if (!plugin[handler]) continue
|
||||
const next = plugin[handler](...args, change, editor)
|
||||
if (next != null) break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke `render` on all of the plugins in reverse, building up a tree of
|
||||
* higher-order components.
|
||||
@@ -146,28 +146,6 @@ class Stack extends Record(DEFAULTS) {
|
||||
|
||||
Stack.prototype[MODEL_TYPES.STACK] = true
|
||||
|
||||
/**
|
||||
* Mix in the stack methods.
|
||||
*
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
for (let i = 0; i < METHODS.length; i++) {
|
||||
const method = METHODS[i]
|
||||
Stack.prototype[method] = function (change, editor, ...args) {
|
||||
debug(method)
|
||||
|
||||
for (let k = 0; k < this.plugins.length; k++) {
|
||||
const plugin = this.plugins[k]
|
||||
if (!plugin[method]) continue
|
||||
const next = plugin[method](...args, change, editor)
|
||||
if (next != null) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a schema from a set of `plugins`.
|
||||
*
|
||||
|
@@ -3364,6 +3364,10 @@ is-hotkey@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.0.1.tgz#d8d817209b34292551a85357e65cdbfcfa763443"
|
||||
|
||||
is-hotkey@^0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.0.3.tgz#3713fea135f86528c87cf39810b3934e45151390"
|
||||
|
||||
is-image@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-image/-/is-image-1.0.1.tgz#6fd51a752a1a111506d060d952118b0b989b426e"
|
||||
|
Reference in New Issue
Block a user