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

fix plugins stack ordering and defaulting

This commit is contained in:
Ian Storm Taylor
2018-10-09 18:43:47 -07:00
parent 7304c9b343
commit 3528bb7366
47 changed files with 943 additions and 721 deletions

View File

@@ -8,8 +8,6 @@ import warning from 'tiny-warning'
import { Editor as Controller } from 'slate'
import EVENT_HANDLERS from '../constants/event-handlers'
import BrowserPlugin from '../plugins/browser'
import PropsPlugin from '../plugins/props'
import ReactPlugin from '../plugins/react'
/**
@@ -143,7 +141,7 @@ class Editor extends React.Component {
render() {
debug('render', this)
const props = { ...this.props }
const props = { ...this.props, editor: this }
// Re-resolve the controller if needed based on memoized props.
const { commands, plugins, queries, schema } = props
@@ -155,7 +153,7 @@ class Editor extends React.Component {
this.controller.setValue(value, options)
// Render the editor's children with the controller.
const children = this.controller.run('renderEditor', props, this)
const children = this.controller.run('renderEditor', props)
return children
}
@@ -182,12 +180,8 @@ class Editor extends React.Component {
'A Slate <Editor> component is re-resolving the `plugins`, `schema`, `commands` or `queries` on each update, which leads to poor performance. This is often due to passing in a new references for these props with each render by declaring them inline in your render function. Do not do this! Declare them outside your render function, or memoize them instead.'
)
const { props, onControllerChange } = this
const reactPlugin = ReactPlugin()
const browserPlugin = BrowserPlugin()
const propsPlugin = PropsPlugin(props)
const allPlugins = [reactPlugin, browserPlugin, propsPlugin, ...plugins]
const attrs = { onChange: onControllerChange, plugins: allPlugins }
const react = ReactPlugin(this.props)
const attrs = { onChange: this.onControllerChange, plugins: [react] }
this.controller = new Controller(attrs, { editor: this, normalize: false })
}

View File

@@ -166,7 +166,7 @@ class Node extends React.Component {
readOnly,
}
let placeholder = editor.run('renderPlaceholder', props, editor)
let placeholder = editor.run('renderPlaceholder', props)
if (placeholder) {
placeholder = React.cloneElement(placeholder, {
@@ -176,15 +176,11 @@ class Node extends React.Component {
children = [placeholder, ...children]
}
const element = editor.run(
'renderNode',
{
...props,
attributes,
children,
},
editor
)
const element = editor.run('renderNode', {
...props,
attributes,
children,
})
return editor.query('isVoid', node) ? (
<Void {...this.props}>{element}</Void>

View File

@@ -2,14 +2,8 @@ import Base64 from 'slate-base64-serializer'
import Debug from 'debug'
import Hotkeys from 'slate-hotkeys'
import Plain from 'slate-plain-serializer'
import ReactDOM from 'react-dom'
import getWindow from 'get-window'
import {
IS_FIREFOX,
IS_IE,
IS_IOS,
HAS_INPUT_EVENTS_LEVEL_2,
} from 'slate-dev-environment'
import { IS_IOS } from 'slate-dev-environment'
import cloneFragment from '../utils/clone-fragment'
import findDOMNode from '../utils/find-dom-node'
@@ -26,20 +20,16 @@ import setEventTransfer from '../utils/set-event-transfer'
* @type {Function}
*/
const debug = Debug('slate:browser')
const debug = Debug('slate:after')
/**
* A plugin that adds the browser-specific logic to the editor.
* A plugin that adds the "after" browser-specific logic to the editor.
*
* @param {Object} options
* @return {Object}
*/
function BrowserPlugin() {
let activeElement = null
let compositionCount = 0
let isComposing = false
let isCopying = false
let isDragging = false
function AfterPlugin(options = {}) {
let isDraggingInternally = null
/**
@@ -48,24 +38,11 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onBeforeInput(event, change, next) {
const { editor, value } = change
const isSynthetic = !!event.nativeEvent
if (editor.readOnly) return true
// COMPAT: If the browser supports Input Events Level 2, we will have
// attached a custom handler for the real `beforeinput` events, instead of
// allowing React's synthetic polyfill, so we need to ignore synthetics.
if (isSynthetic && HAS_INPUT_EVENTS_LEVEL_2) return true
debug('onBeforeInput', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
// If the event is synthetic, it's React's polyfill of `beforeinput` that
// isn't a true `beforeinput` event with meaningful information. It only
@@ -73,13 +50,15 @@ function BrowserPlugin() {
if (isSynthetic) {
event.preventDefault()
change.insertText(event.data)
return
return next()
}
// Otherwise, we can use the information in the `beforeinput` event to
// figure out the exact change that will occur, and prevent it.
const [targetRange] = event.getTargetRanges()
if (!targetRange) return
if (!targetRange) return next()
debug('onBeforeInput', { event })
event.preventDefault()
@@ -93,29 +72,29 @@ function BrowserPlugin() {
case 'deleteContentBackward':
case 'deleteContentForward': {
change.deleteAtRange(range)
return
break
}
case 'deleteWordBackward': {
change.deleteWordBackwardAtRange(range)
return
break
}
case 'deleteWordForward': {
change.deleteWordForwardAtRange(range)
return
break
}
case 'deleteSoftLineBackward':
case 'deleteHardLineBackward': {
change.deleteLineBackwardAtRange(range)
return
break
}
case 'deleteSoftLineForward':
case 'deleteHardLineForward': {
change.deleteLineForwardAtRange(range)
return
break
}
case 'insertLineBreak':
@@ -131,7 +110,7 @@ function BrowserPlugin() {
change.splitBlockAtRange(range)
}
return
break
}
case 'insertFromYank':
@@ -146,7 +125,7 @@ function BrowserPlugin() {
? event.dataTransfer.getData('text/plain')
: event.data
if (text == null) return
if (text == null) break
change.insertTextAtRange(range, text, selection.marks)
@@ -156,9 +135,11 @@ function BrowserPlugin() {
change.select({ marks: null })
}
return
break
}
}
next()
}
/**
@@ -167,88 +148,12 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onBlur(event, change, next) {
const { editor } = change
if (isCopying) return true
if (editor.readOnly) return true
const { relatedTarget, target } = event
const window = getWindow(target)
// COMPAT: If the current `activeElement` is still the previous one, this is
// due to the window being blurred when the tab itself becomes unfocused, so
// we want to abort early to allow to editor to stay focused when the tab
// becomes focused again.
if (activeElement === window.document.activeElement) return true
// COMPAT: The `relatedTarget` can be null when the new focus target is not
// a "focusable" element (eg. a `<div>` without `tabindex` set).
if (relatedTarget) {
const el = ReactDOM.findDOMNode(editor)
// COMPAT: The event should be ignored if the focus is returning to the
// editor from an embedded editable element (eg. an <input> element inside
// a void node).
if (relatedTarget === el) return true
// COMPAT: The event should be ignored if the focus is moving from the
// editor to inside a void node's spacer element.
if (relatedTarget.hasAttribute('data-slate-spacer')) return true
// COMPAT: The event should be ignored if the focus is moving to a non-
// editable section of an element that isn't a void node (eg. a list item
// of the check list example).
const node = findNode(relatedTarget, editor)
if (el.contains(relatedTarget) && node && !change.isVoid(node))
return true
}
debug('onBlur', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
change.blur()
return true
}
/**
* On composition end.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onCompositionEnd(event, change, next) {
const { editor } = change
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.
window.requestAnimationFrame(() => {
if (compositionCount > n) return
isComposing = false
// HACK: we need to re-render the editor here so that it will update its
// placeholder in case one is currently rendered. This should be handled
// differently ideally, in a less invasive way?
// (apply force re-render if isComposing changes)
if (editor.state.isComposing) {
editor.setState({ isComposing: false })
}
})
debug('onCompositionEnd', { event })
// Delegate to the plugins stack.
return next()
next()
}
/**
@@ -257,23 +162,18 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onClick(event, change, next) {
debug('onClick', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change
if (editor.readOnly) return true
if (editor.readOnly) return next()
const { value } = editor
const { document } = value
const node = findNode(event.target, editor)
if (!node) return true
if (!node) return next()
debug('onClick', { event })
const ancestors = document.getAncestors(node.key)
const isVoid =
@@ -287,35 +187,7 @@ function BrowserPlugin() {
change.focus().moveToEndOfNode(node)
}
return true
}
/**
* On composition start.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onCompositionStart(event, change, next) {
isComposing = true
compositionCount++
const { editor } = change
// HACK: we need to re-render the editor here so that it will update its
// placeholder in case one is currently rendered. This should be handled
// differently ideally, in a less invasive way?
// (apply force re-render if isComposing changes)
if (!editor.state.isComposing) {
editor.setState({ isComposing: true })
}
debug('onCompositionStart', { event })
// Delegate to the plugins stack.
return next()
next()
}
/**
@@ -324,23 +196,13 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onCopy(event, change, next) {
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => (isCopying = false))
debug('onCopy', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change
cloneFragment(event, editor)
return true
next()
}
/**
@@ -349,22 +211,11 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onCut(event, change, next) {
const { editor } = change
if (editor.readOnly) return true
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => (isCopying = false))
debug('onCut', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change
// Once the fake cut content has successfully been added to the clipboard,
// delete the content in the current selection.
@@ -385,6 +236,8 @@ function BrowserPlugin() {
editor.change(c => c.delete())
}
})
next()
}
/**
@@ -393,96 +246,12 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDragEnd(event, change, next) {
debug('onDragEnd', { event })
isDragging = false
isDraggingInternally = null
return next()
}
/**
* On drag enter.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDragEnter(event, change, next) {
debug('onDragEnter', { event })
return next()
}
/**
* On drag exit.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDragExit(event, change, next) {
debug('onDragExit', { event })
return next()
}
/**
* On drag leave.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDragLeave(event, change, next) {
debug('onDragLeave', { event })
return next()
}
/**
* On drag over.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDragOver(event, change, next) {
debug('onDragOver', { event })
// If the target is inside a void node, and only in this case,
// call `preventDefault` to signal that drops are allowed.
// When the target is editable, dropping is already allowed by
// default, and calling `preventDefault` hides the cursor.
const { editor } = change
const node = findNode(event.target, editor)
if (change.isVoid(node)) event.preventDefault()
// COMPAT: IE won't call onDrop on contentEditables unless the
// default dragOver is prevented:
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/913982/
// (2018/07/11)
if (IS_IE) event.preventDefault()
// If a drag is already in progress, don't do this again.
if (!isDragging) {
isDragging = true
// COMPAT: IE will raise an `unspecified error` if dropEffect is
// set. (2018/07/11)
if (!IS_IE) {
event.nativeEvent.dataTransfer.dropEffect = 'move'
}
}
return next()
next()
}
/**
@@ -491,19 +260,13 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDragStart(event, change, next) {
debug('onDragStart', { event })
isDragging = true
isDraggingInternally = true
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change
const { value } = editor
const { document } = value
@@ -523,6 +286,7 @@ function BrowserPlugin() {
const fragment = change.value.fragment
const encoded = Base64.serializeNode(fragment)
setEventTransfer(event, 'fragment', encoded)
next()
}
/**
@@ -531,26 +295,16 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onDrop(event, change, next) {
const { editor, value } = change
if (editor.readOnly) return true
debug('onDrop', { event })
// Prevent default so the DOM's value isn't corrupted.
event.preventDefault()
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { document, selection } = value
const window = getWindow(event.target)
let target = getEventRange(event, editor)
if (!target) return true
if (!target) return next()
debug('onDrop', { event })
const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer
@@ -611,49 +365,18 @@ function BrowserPlugin() {
// DOM node, since that will make it go back to normal.
const focusNode = document.getNode(target.focus.key)
const el = findDOMNode(focusNode, window)
if (!el) return true
el.dispatchEvent(
new MouseEvent('mouseup', {
view: window,
bubbles: true,
cancelable: true,
})
)
return true
}
/**
* On focus.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onFocus(event, change, next) {
const { editor } = change
if (isCopying) return true
if (editor.readOnly) return true
const el = ReactDOM.findDOMNode(editor)
// Save the new `activeElement`.
const window = getWindow(event.target)
activeElement = window.document.activeElement
// 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
if (el) {
el.dispatchEvent(
new MouseEvent('mouseup', {
view: window,
bubbles: true,
cancelable: true,
})
)
}
debug('onFocus', { event })
return next()
next()
}
/**
@@ -662,19 +385,9 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onInput(event, change, next) {
if (isComposing) return true
if (change.value.selection.isBlurred) return true
debug('onInput', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const window = getWindow(event.target)
const { editor, value } = change
@@ -682,7 +395,7 @@ function BrowserPlugin() {
const native = window.getSelection()
const { anchorNode } = native
const point = findPoint(anchorNode, 0, editor)
if (!point) return
if (!point) return next()
// Get the text node and leaf in question.
const { document, selection } = value
@@ -716,7 +429,9 @@ function BrowserPlugin() {
}
// If the text is no different, abort.
if (textContent == text) return
if (textContent == text) return next()
debug('onInput', { event })
// Determine what the selection should be after changing the text.
const delta = textContent.length - text.length
@@ -729,6 +444,7 @@ function BrowserPlugin() {
// Change the current value to have the leaf's text replaced.
change.insertTextAtRange(entire, textContent, leaf.marks).select(corrected)
next()
}
/**
@@ -737,48 +453,12 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onKeyDown(event, change, next) {
const { editor, value } = change
if (editor.readOnly) return true
// When composing, we need to prevent all hotkeys from executing while
// typing. However, certain characters also move the selection before
// we're able to handle it, so prevent their default behavior.
if (isComposing) {
if (Hotkeys.isCompose(event)) event.preventDefault()
return true
}
debug('onKeyDown', { event })
// Certain hotkeys have native editing behaviors in `contenteditable`
// elements which will change the DOM and cause our value to be out of sync,
// so they need to always be prevented.
if (
!IS_IOS &&
(Hotkeys.isBold(event) ||
Hotkeys.isDeleteBackward(event) ||
Hotkeys.isDeleteForward(event) ||
Hotkeys.isDeleteLineBackward(event) ||
Hotkeys.isDeleteLineForward(event) ||
Hotkeys.isDeleteWordBackward(event) ||
Hotkeys.isDeleteWordForward(event) ||
Hotkeys.isItalic(event) ||
Hotkeys.isRedo(event) ||
Hotkeys.isSplitBlock(event) ||
Hotkeys.isTransposeCharacter(event) ||
Hotkeys.isUndo(event))
) {
event.preventDefault()
}
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor, value } = change
const { document, selection } = value
const hasVoidParent = document.hasVoidParent(selection.start.path, editor)
@@ -893,7 +573,7 @@ function BrowserPlugin() {
}
}
return true
next()
}
/**
@@ -902,22 +582,12 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onPaste(event, change, next) {
const { editor, value } = change
if (editor.readOnly) return true
debug('onPaste', { event })
// Prevent defaults so the DOM state isn't corrupted.
event.preventDefault()
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { value } = change
const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer
@@ -926,9 +596,9 @@ function BrowserPlugin() {
}
if (type == 'text' || type == 'html') {
if (!text) return true
if (!text) return next()
const { document, selection, startBlock } = value
if (change.isVoid(startBlock)) return true
if (change.isVoid(startBlock)) return next()
const defaultBlock = startBlock
const defaultMarks = document.getInsertMarksAtRange(selection)
@@ -937,7 +607,7 @@ function BrowserPlugin() {
change.insertFragment(frag)
}
return true
next()
}
/**
@@ -946,38 +616,25 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onSelect(event, change, next) {
if (isCopying) return true
if (isComposing) return true
const { editor, value } = change
if (editor.readOnly) return true
debug('onSelect', { event })
// Save the new `activeElement`.
const window = getWindow(event.target)
activeElement = window.document.activeElement
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor, value } = change
const { document } = value
const native = window.getSelection()
// If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) {
change.blur()
return true
return
}
// Otherwise, determine the Slate selection from the native one.
let range = findRange(native, editor)
if (!range) return true
if (!range) return
const { anchor, focus } = range
const anchorText = document.getNode(anchor.key)
@@ -1036,7 +693,7 @@ function BrowserPlugin() {
selection = selection.set('marks', value.selection.marks)
change.select(selection)
return true
next()
}
/**
@@ -1049,18 +706,11 @@ function BrowserPlugin() {
onBeforeInput,
onBlur,
onClick,
onCompositionEnd,
onCompositionStart,
onCopy,
onCut,
onDragEnd,
onDragEnter,
onDragExit,
onDragLeave,
onDragOver,
onDragStart,
onDrop,
onFocus,
onInput,
onKeyDown,
onPaste,
@@ -1074,4 +724,4 @@ function BrowserPlugin() {
* @type {Object}
*/
export default BrowserPlugin
export default AfterPlugin

View File

@@ -0,0 +1,505 @@
import Debug from 'debug'
import Hotkeys from 'slate-hotkeys'
import ReactDOM from 'react-dom'
import getWindow from 'get-window'
import {
IS_FIREFOX,
IS_IE,
IS_IOS,
HAS_INPUT_EVENTS_LEVEL_2,
} from 'slate-dev-environment'
import findNode from '../utils/find-node'
/**
* Debug.
*
* @type {Function}
*/
const debug = Debug('slate:before')
/**
* A plugin that adds the "before" browser-specific logic to the editor.
*
* @return {Object}
*/
function BeforePlugin() {
let activeElement = null
let compositionCount = 0
let isComposing = false
let isCopying = false
let isDragging = false
/**
* On before input.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onBeforeInput(event, change, next) {
const { editor } = change
const isSynthetic = !!event.nativeEvent
if (editor.readOnly) return
// COMPAT: If the browser supports Input Events Level 2, we will have
// attached a custom handler for the real `beforeinput` events, instead of
// allowing React's synthetic polyfill, so we need to ignore synthetics.
if (isSynthetic && HAS_INPUT_EVENTS_LEVEL_2) return
debug('onBeforeInput', { event })
next()
}
/**
* On blur.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onBlur(event, change, next) {
const { editor } = change
if (isCopying) return
if (editor.readOnly) return
const { relatedTarget, target } = event
const window = getWindow(target)
// COMPAT: If the current `activeElement` is still the previous one, this is
// due to the window being blurred when the tab itself becomes unfocused, so
// we want to abort early to allow to editor to stay focused when the tab
// becomes focused again.
if (activeElement === window.document.activeElement) return
// COMPAT: The `relatedTarget` can be null when the new focus target is not
// a "focusable" element (eg. a `<div>` without `tabindex` set).
if (relatedTarget) {
const el = ReactDOM.findDOMNode(editor)
// COMPAT: The event should be ignored if the focus is returning to the
// editor from an embedded editable element (eg. an <input> element inside
// a void node).
if (relatedTarget === el) return
// COMPAT: The event should be ignored if the focus is moving from the
// editor to inside a void node's spacer element.
if (relatedTarget.hasAttribute('data-slate-spacer')) return
// COMPAT: The event should be ignored if the focus is moving to a non-
// editable section of an element that isn't a void node (eg. a list item
// of the check list example).
const node = findNode(relatedTarget, editor)
if (el.contains(relatedTarget) && node && !change.isVoid(node)) return
}
debug('onBlur', { event })
next()
}
/**
* On composition end.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onCompositionEnd(event, change, next) {
const { editor } = change
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.
window.requestAnimationFrame(() => {
if (compositionCount > n) return
isComposing = false
// HACK: we need to re-render the editor here so that it will update its
// placeholder in case one is currently rendered. This should be handled
// differently ideally, in a less invasive way?
// (apply force re-render if isComposing changes)
if (editor.state.isComposing) {
editor.setState({ isComposing: false })
}
})
debug('onCompositionEnd', { event })
next()
}
/**
* On click.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onClick(event, change, next) {
debug('onClick', { event })
next()
}
/**
* On composition start.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onCompositionStart(event, change, next) {
isComposing = true
compositionCount++
const { editor } = change
// HACK: we need to re-render the editor here so that it will update its
// placeholder in case one is currently rendered. This should be handled
// differently ideally, in a less invasive way?
// (apply force re-render if isComposing changes)
if (!editor.state.isComposing) {
editor.setState({ isComposing: true })
}
debug('onCompositionStart', { event })
next()
}
/**
* On copy.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onCopy(event, change, next) {
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => (isCopying = false))
debug('onCopy', { event })
next()
}
/**
* On cut.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onCut(event, change, next) {
const { editor } = change
if (editor.readOnly) return
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => (isCopying = false))
debug('onCut', { event })
next()
}
/**
* On drag end.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDragEnd(event, change, next) {
isDragging = false
debug('onDragEnd', { event })
next()
}
/**
* On drag enter.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDragEnter(event, change, next) {
debug('onDragEnter', { event })
next()
}
/**
* On drag exit.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDragExit(event, change, next) {
debug('onDragExit', { event })
next()
}
/**
* On drag leave.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDragLeave(event, change, next) {
debug('onDragLeave', { event })
next()
}
/**
* On drag over.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDragOver(event, change, next) {
// If the target is inside a void node, and only in this case,
// call `preventDefault` to signal that drops are allowed.
// When the target is editable, dropping is already allowed by
// default, and calling `preventDefault` hides the cursor.
const { editor } = change
const node = findNode(event.target, editor)
if (change.isVoid(node)) event.preventDefault()
// COMPAT: IE won't call onDrop on contentEditables unless the
// default dragOver is prevented:
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/913982/
// (2018/07/11)
if (IS_IE) {
event.preventDefault()
}
// If a drag is already in progress, don't do this again.
if (!isDragging) {
isDragging = true
// COMPAT: IE will raise an `unspecified error` if dropEffect is
// set. (2018/07/11)
if (!IS_IE) {
event.nativeEvent.dataTransfer.dropEffect = 'move'
}
}
debug('onDragOver', { event })
next()
}
/**
* On drag start.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDragStart(event, change, next) {
isDragging = true
debug('onDragStart', { event })
next()
}
/**
* On drop.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onDrop(event, change, next) {
const { editor } = change
if (editor.readOnly) return
// Prevent default so the DOM's value isn't corrupted.
event.preventDefault()
debug('onDrop', { event })
next()
}
/**
* On focus.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onFocus(event, change, next) {
const { editor } = change
if (isCopying) return
if (editor.readOnly) return
const el = ReactDOM.findDOMNode(editor)
// Save the new `activeElement`.
const window = getWindow(event.target)
activeElement = window.document.activeElement
// 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
}
debug('onFocus', { event })
next()
}
/**
* On input.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onInput(event, change, next) {
if (isComposing) return
if (change.value.selection.isBlurred) return
debug('onInput', { event })
next()
}
/**
* On key down.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onKeyDown(event, change, next) {
const { editor } = change
if (editor.readOnly) return
// When composing, we need to prevent all hotkeys from executing while
// typing. However, certain characters also move the selection before
// we're able to handle it, so prevent their default behavior.
if (isComposing) {
if (Hotkeys.isCompose(event)) event.preventDefault()
return
}
// Certain hotkeys have native editing behaviors in `contenteditable`
// elements which will change the DOM and cause our value to be out of sync,
// so they need to always be prevented.
if (
!IS_IOS &&
(Hotkeys.isBold(event) ||
Hotkeys.isDeleteBackward(event) ||
Hotkeys.isDeleteForward(event) ||
Hotkeys.isDeleteLineBackward(event) ||
Hotkeys.isDeleteLineForward(event) ||
Hotkeys.isDeleteWordBackward(event) ||
Hotkeys.isDeleteWordForward(event) ||
Hotkeys.isItalic(event) ||
Hotkeys.isRedo(event) ||
Hotkeys.isSplitBlock(event) ||
Hotkeys.isTransposeCharacter(event) ||
Hotkeys.isUndo(event))
) {
event.preventDefault()
}
debug('onKeyDown', { event })
next()
}
/**
* On paste.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onPaste(event, change, next) {
const { editor } = change
if (editor.readOnly) return
// Prevent defaults so the DOM state isn't corrupted.
event.preventDefault()
debug('onPaste', { event })
next()
}
/**
* On select.
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
function onSelect(event, change, next) {
if (isCopying) return
if (isComposing) return
const { editor } = change
if (editor.readOnly) return
// Save the new `activeElement`.
const window = getWindow(event.target)
activeElement = window.document.activeElement
debug('onSelect', { event })
next()
}
/**
* Return the plugin.
*
* @type {Object}
*/
return {
onBeforeInput,
onBlur,
onClick,
onCompositionEnd,
onCompositionStart,
onCopy,
onCut,
onDragEnd,
onDragEnter,
onDragExit,
onDragLeave,
onDragOver,
onDragStart,
onDrop,
onFocus,
onInput,
onKeyDown,
onPaste,
onSelect,
}
}
/**
* Export.
*
* @type {Object}
*/
export default BeforePlugin

View File

@@ -0,0 +1,24 @@
import AfterPlugin from './after'
import BeforePlugin from './before'
/**
* A plugin that adds the browser-specific logic to the editor.
*
* @param {Object} options
* @return {Object}
*/
function DOMPlugin(options = {}) {
const { plugins = [] } = options
const beforePlugin = BeforePlugin()
const afterPlugin = AfterPlugin()
return [beforePlugin, ...plugins, afterPlugin]
}
/**
* Export.
*
* @type {Object}
*/
export default DOMPlugin

View File

@@ -1,46 +0,0 @@
import EVENT_HANDLERS from '../constants/event-handlers'
/**
* Props that can be defined by plugins.
*
* @type {Array}
*/
const PROPS = [
...EVENT_HANDLERS,
'commands',
'decorateNode',
'queries',
'renderEditor',
'renderMark',
'renderNode',
'renderPlaceholder',
'schema',
]
/**
* A plugin that is defined from the props on the `<Editor>` component.
*
* @param {Object} props
* @return {Object}
*/
function PropsPlugin(props) {
const plugin = {}
for (const prop of PROPS) {
if (prop in props) {
plugin[prop] = props[prop]
}
}
return plugin
}
/**
* Export.
*
* @type {Object}
*/
export default PropsPlugin

View File

@@ -1,26 +1,49 @@
import React from 'react'
import { Text } from 'slate'
import DOMPlugin from './dom'
import Content from '../components/content'
import EVENT_HANDLERS from '../constants/event-handlers'
/**
* Props that can be defined by plugins.
*
* @type {Array}
*/
const PROPS = [
...EVENT_HANDLERS,
'commands',
'decorateNode',
'queries',
'renderEditor',
'renderMark',
'renderNode',
'renderPlaceholder',
'schema',
]
/**
* A plugin that adds the React-specific rendering logic to the editor.
*
* @param {Object} options
* @return {Object}
*/
function ReactPlugin() {
function ReactPlugin(options = {}) {
const { plugins = [] } = options
/**
* Render editor.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Object}
*/
function renderEditor(props, editor, next) {
const children = (
function renderEditor(props, next) {
const { editor } = props
return (
<Content
onEvent={editor.event}
autoCorrect={props.autoCorrect}
@@ -34,27 +57,22 @@ function ReactPlugin() {
tagName={props.tagName}
/>
)
const ret = next({ ...props, children }, editor)
return ret !== undefined ? ret : children
}
/**
* Render node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
function renderNode(props, editor, next) {
const ret = next()
if (ret !== undefined) return ret
function renderNode(props, next) {
const { attributes, children, node } = props
if (node.object != 'block' && node.object != 'inline') return null
const Tag = node.object == 'block' ? 'div' : 'span'
const { object } = node
if (object != 'block' && object != 'inline') return null
const Tag = object == 'block' ? 'div' : 'span'
const style = { position: 'relative' }
return (
<Tag {...attributes} style={style}>
@@ -67,16 +85,12 @@ function ReactPlugin() {
* Render placeholder.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
function renderPlaceholder(props, editor, next) {
const ret = next()
if (ret !== undefined) return ret
const { node } = props
function renderPlaceholder(props, next) {
const { editor, node } = props
if (!editor.props.placeholder) return null
if (editor.state.isComposing) return null
if (node.object != 'block') return null
@@ -101,16 +115,22 @@ function ReactPlugin() {
}
/**
* Return the plugin.
* Return the plugins.
*
* @type {Object}
* @type {Array}
*/
return {
renderEditor,
renderNode,
renderPlaceholder,
}
const editorPlugin = PROPS.reduce((memo, prop) => {
if (prop in options) memo[prop] = options[prop]
return memo
}, {})
const domPlugin = DOMPlugin({
plugins: [editorPlugin, ...plugins],
})
const defaultsPlugin = { renderEditor, renderNode, renderPlaceholder }
return [domPlugin, defaultsPlugin]
}
/**

View File

@@ -11,10 +11,12 @@ function Image(props) {
})
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'image':
return Image(props)
default:
return next()
}
}

View File

@@ -11,10 +11,12 @@ function Image(props) {
})
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'image':
return Image(props)
default:
return next()
}
}

View File

@@ -11,10 +11,12 @@ function Code(props) {
)
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'code':
return Code(props)
default:
return next()
}
}

View File

@@ -11,10 +11,12 @@ function Image(props) {
})
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'image':
return Image(props)
default:
return next()
}
}

View File

@@ -10,10 +10,12 @@ function Image(props) {
})
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'image':
return Image(props)
default:
return next()
}
}

View File

@@ -12,10 +12,12 @@ function Code(props) {
}
export const props = {
renderNode(p) {
renderNode(p, editor, next) {
switch (p.node.type) {
case 'code':
return Code(p)
default:
return next()
}
},
}

View File

@@ -26,10 +26,12 @@ function Bold(props) {
return React.createElement('strong', { ...props.attributes }, props.children)
}
function renderMark(props) {
function renderMark(props, next) {
switch (props.mark.type) {
case 'bold':
return Bold(props)
default:
return next()
}
}

View File

@@ -11,10 +11,12 @@ function Link(props) {
)
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'link':
return Link(props)
default:
return next()
}
}

View File

@@ -7,10 +7,12 @@ function Emoji(props) {
return React.createElement('img', props.attributes)
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'emoji':
return Emoji(props)
default:
return next()
}
}

View File

@@ -11,10 +11,12 @@ function Link(props) {
)
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'link':
return Link(props)
default:
return next()
}
}

View File

@@ -7,10 +7,12 @@ function Bold(props) {
return React.createElement('strong', { ...props.attributes }, props.children)
}
function renderMark(props) {
function renderMark(props, next) {
switch (props.mark.type) {
case 'bold':
return Bold(props)
default:
return next()
}
}

View File

@@ -10,10 +10,12 @@ function Image(props) {
})
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'image':
return Image(props)
default:
return next()
}
}

View File

@@ -7,10 +7,12 @@ function Emoji(props) {
return React.createElement('img', props.attributes)
}
function renderNode(props) {
function renderNode(props, next) {
switch (props.node.type) {
case 'emoji':
return Emoji(props)
default:
return next()
}
}