` 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
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
diff --git a/packages/slate-react/src/plugins/before.js b/packages/slate-react/src/plugins/before.js
new file mode 100644
index 000000000..8dfd7fbc5
--- /dev/null
+++ b/packages/slate-react/src/plugins/before.js
@@ -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 `
` 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 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
diff --git a/packages/slate-react/src/plugins/dom.js b/packages/slate-react/src/plugins/dom.js
new file mode 100644
index 000000000..c1a476193
--- /dev/null
+++ b/packages/slate-react/src/plugins/dom.js
@@ -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
diff --git a/packages/slate-react/src/plugins/props.js b/packages/slate-react/src/plugins/props.js
deleted file mode 100644
index ad57dde93..000000000
--- a/packages/slate-react/src/plugins/props.js
+++ /dev/null
@@ -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 `` 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
diff --git a/packages/slate-react/src/plugins/react.js b/packages/slate-react/src/plugins/react.js
index 440fc78d7..5ba82be06 100644
--- a/packages/slate-react/src/plugins/react.js
+++ b/packages/slate-react/src/plugins/react.js
@@ -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 (
)
-
- 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 (
@@ -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]
}
/**
diff --git a/packages/slate-react/test/rendering/fixtures/custom-block-blurred.js b/packages/slate-react/test/rendering/fixtures/custom-block-blurred.js
index 0c579114e..a608baa2c 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-block-blurred.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-block-blurred.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-block-focused.js b/packages/slate-react/test/rendering/fixtures/custom-block-focused.js
index 7ff57a4a9..2d403439f 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-block-focused.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-block-focused.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-block-multiple.js b/packages/slate-react/test/rendering/fixtures/custom-block-multiple.js
index b029884a2..46d703f0d 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-block-multiple.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-block-multiple.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-block-selected.js b/packages/slate-react/test/rendering/fixtures/custom-block-selected.js
index 34fba60fc..980b4eb70 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-block-selected.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-block-selected.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-block-void.js b/packages/slate-react/test/rendering/fixtures/custom-block-void.js
index 283d3b81d..def5e945f 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-block-void.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-block-void.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-block.js b/packages/slate-react/test/rendering/fixtures/custom-block.js
index ab03fcaa9..72ba5a7e5 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-block.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-block.js
@@ -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()
}
},
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-decorator.js b/packages/slate-react/test/rendering/fixtures/custom-decorator.js
index 351cd20be..27ff5b0cf 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-decorator.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-decorator.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-inline-multiple.js b/packages/slate-react/test/rendering/fixtures/custom-inline-multiple.js
index 574bf5933..c0c8e02ca 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-inline-multiple.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-inline-multiple.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-inline-void.js b/packages/slate-react/test/rendering/fixtures/custom-inline-void.js
index 9bddf6ec4..f8c1f2ed6 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-inline-void.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-inline-void.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-inline.js b/packages/slate-react/test/rendering/fixtures/custom-inline.js
index dd8cefc30..7d89d2c3a 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-inline.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-inline.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/custom-mark.js b/packages/slate-react/test/rendering/fixtures/custom-mark.js
index 7dca0c526..07b723abd 100644
--- a/packages/slate-react/test/rendering/fixtures/custom-mark.js
+++ b/packages/slate-react/test/rendering/fixtures/custom-mark.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js b/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js
index 14c86a5c2..02844f373 100644
--- a/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js
+++ b/packages/slate-react/test/rendering/fixtures/readonly-custom-block-void.js
@@ -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()
}
}
diff --git a/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js b/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js
index 469fb3723..53eb43d92 100644
--- a/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js
+++ b/packages/slate-react/test/rendering/fixtures/readonly-custom-inline-void.js
@@ -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()
}
}
diff --git a/packages/slate/src/controllers/editor.js b/packages/slate/src/controllers/editor.js
index 696d5aa82..4021ebef0 100644
--- a/packages/slate/src/controllers/editor.js
+++ b/packages/slate/src/controllers/editor.js
@@ -16,14 +16,6 @@ import Value from '../models/value'
const debug = Debug('slate:editor')
-/**
- * The core plugin.
- *
- * @type {Array|Object}
- */
-
-const corePlugin = CorePlugin()
-
/**
* Editor.
*
@@ -59,8 +51,8 @@ class Editor {
isChanging: false,
}
- registerPlugin(this, corePlugin)
- plugins.forEach(p => registerPlugin(this, p))
+ const core = CorePlugin({ plugins })
+ registerPlugin(this, core)
this.run('onConstruct', this)
@@ -304,12 +296,12 @@ function registerPlugin(editor, plugin) {
const { commands, queries, schema, ...rest } = plugin
if (commands) {
- const commandsPlugin = CommandsPlugin({ commands })
+ const commandsPlugin = CommandsPlugin(commands)
registerPlugin(editor, commandsPlugin)
}
if (queries) {
- const queriesPlugin = QueriesPlugin({ queries })
+ const queriesPlugin = QueriesPlugin(queries)
registerPlugin(editor, queriesPlugin)
}
diff --git a/packages/slate/src/plugins/commands.js b/packages/slate/src/plugins/commands.js
index 3c398f016..c66b9b61c 100644
--- a/packages/slate/src/plugins/commands.js
+++ b/packages/slate/src/plugins/commands.js
@@ -1,19 +1,11 @@
/**
* A plugin that adds a set of commands to the editor.
*
- * @param {Object} options
+ * @param {Object} commands
* @return {Object}
*/
-function CommandsPlugin(options = {}) {
- const { commands, defer = false } = options
-
- if (!commands) {
- throw new Error(
- 'You must pass in the `commands` option to the Slate commands plugin.'
- )
- }
-
+function CommandsPlugin(commands = {}) {
/**
* On command, if it exists in our list of commands, call it.
*
@@ -26,14 +18,7 @@ function CommandsPlugin(options = {}) {
const { type, args } = command
const fn = commands[type]
if (!fn) return next()
-
- if (defer) {
- const ret = next()
- if (ret !== undefined) return ret
- }
-
change.call(fn, ...args)
- return true
}
/**
diff --git a/packages/slate/src/plugins/core.js b/packages/slate/src/plugins/core.js
index f32c8f08e..782b6c097 100644
--- a/packages/slate/src/plugins/core.js
+++ b/packages/slate/src/plugins/core.js
@@ -12,10 +12,13 @@ import Text from '../models/text'
/**
* A plugin that defines the core Slate logic.
*
+ * @param {Object} options
* @return {Object}
*/
-function CorePlugin() {
+function CorePlugin(options = {}) {
+ const { plugins = [] } = options
+
/**
* The core Slate commands.
*
@@ -23,15 +26,12 @@ function CorePlugin() {
*/
const commands = Commands({
- defer: true,
- commands: {
- ...AtCurrentRange,
- ...AtRange,
- ...ByPath,
- ...OnHistory,
- ...OnSelection,
- ...OnValue,
- },
+ ...AtCurrentRange,
+ ...AtRange,
+ ...ByPath,
+ ...OnHistory,
+ ...OnSelection,
+ ...OnValue,
})
/**
@@ -41,13 +41,8 @@ function CorePlugin() {
*/
const queries = Queries({
- defer: true,
- queries: {
- isAtomic: () => false,
- isVoid: () => false,
- normalizeNode: () => {},
- validateNode: () => {},
- },
+ isAtomic: () => false,
+ isVoid: () => false,
})
/**
@@ -180,7 +175,7 @@ function CorePlugin() {
* @type {Array}
*/
- return [commands, queries, schema]
+ return [schema, ...plugins, commands, queries]
}
/**
diff --git a/packages/slate/src/plugins/queries.js b/packages/slate/src/plugins/queries.js
index 039739a5a..a6e31ea7d 100644
--- a/packages/slate/src/plugins/queries.js
+++ b/packages/slate/src/plugins/queries.js
@@ -1,19 +1,11 @@
/**
* A plugin that adds a set of queries to the editor.
*
- * @param {Object} options
+ * @param {Object} queries
* @return {Object}
*/
-function QueriesPlugin(options = {}) {
- const { queries, defer = false } = options
-
- if (!queries) {
- throw new Error(
- 'You must pass in the `queries` option to the Slate queries plugin.'
- )
- }
-
+function QueriesPlugin(queries = {}) {
/**
* On construct, register all the queries.
*
@@ -41,12 +33,6 @@ function QueriesPlugin(options = {}) {
const { type, args } = query
const fn = queries[type]
if (!fn) return next()
-
- if (defer) {
- const ret = next()
- if (ret !== undefined) return ret
- }
-
const ret = fn(editor, ...args)
return ret === undefined ? next() : ret
}
diff --git a/packages/slate/src/plugins/schema.js b/packages/slate/src/plugins/schema.js
index 0f82b3bde..8c08866e8 100644
--- a/packages/slate/src/plugins/schema.js
+++ b/packages/slate/src/plugins/schema.js
@@ -136,12 +136,7 @@ function SchemaPlugin(schema) {
* @param {Function} next
*/
- const queries = Queries({
- queries: {
- isAtomic,
- isVoid,
- },
- })
+ const queries = Queries({ isAtomic, isVoid })
/**
* Return the plugins.