diff --git a/lib/plugins/core.js b/lib/plugins/core.js
index e560fa45e..8661909e5 100644
--- a/lib/plugins/core.js
+++ b/lib/plugins/core.js
@@ -6,7 +6,6 @@ import React from 'react'
import String from '../utils/string'
import keycode from 'keycode'
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
-import OffsetKey from '../utils/offset-key'
/**
* The default plugin.
@@ -26,333 +25,409 @@ function Plugin(options = {}) {
} = options
/**
- * Define a default block renderer.
+ * The default block renderer.
*
- * @type {Component}
+ * @param {Object} props
+ * @return {Element}
*/
- class DEFAULT_BLOCK extends React.Component {
- render = () => {
- const { attributes, children } = this.props
- return (
-
- {this.renderPlaceholder()}
- {children}
-
- )
+ function DEFAULT_BLOCK(props) {
+ return (
+
+ {props.children}
+ {placeholder
+ ?
+ {placeholder}
+
+ : null}
+
+ )
+ }
+
+ /**
+ * The default inline renderer.
+ *
+ * @param {Object} props
+ * @return {Element}
+ */
+
+ function DEFAULT_INLINE(props) {
+ return (
+
+ {props.children}
+
+ )
+ }
+
+ /**
+ * On before input, see if we can let the browser continue with it's native
+ * input behavior, to avoid a re-render for performance.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @param {Editor} editor
+ * @return {State}
+ */
+
+ function onBeforeInput(e, state, editor) {
+ const { renderDecorations } = editor
+ const { startOffset, startText, startBlock } = state
+
+ // Determine what the characters would be if natively inserted.
+ const prev = startText.getDecoratedCharacters(startBlock, renderDecorations)
+ const char = prev.get(startOffset)
+ const chars = prev
+ .slice(0, startOffset)
+ .push(Character.create({ text: e.data, marks: char && char.marks }))
+ .concat(prev.slice(startOffset))
+
+ // Determine what the characters should be, if not natively inserted.
+ let next = state
+ .transform()
+ .insertText(e.data)
+ .apply()
+
+ const nextText = next.startText
+ const nextBlock = next.startBlock
+ const nextChars = nextText.getDecoratedCharacters(nextBlock, renderDecorations)
+
+ // We do not have to re-render if the current selection is collapsed, the
+ // current node is not empty, there are no marks on the cursor, and the
+ // natively inserted characters would be the same as the non-native.
+ const isNative = (
+ state.isCollapsed &&
+ state.startText.text != '' &&
+ state.cursorMarks == null &&
+ chars.equals(nextChars)
+ )
+
+ // Add the `isNative` flag directly, so we don't have to re-transform.
+ if (isNative) {
+ next = next.merge({ isNative })
}
- renderPlaceholder = () => {
- if (!placeholder) return null
- const { node, state } = this.props
- return (
-
- {placeholder}
-
- )
+ // If not native, prevent default so that the DOM remains untouched.
+ if (!isNative) e.preventDefault()
+
+ // Return the new state.
+ return next
+ }
+
+ /**
+ * On drop.
+ *
+ * @param {Event} e
+ * @param {Object} data
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onDrop(e, data, state) {
+ switch (data.type) {
+ case 'text':
+ case 'html':
+ return onDropText(e, data, state)
+ case 'fragment':
+ return onDropFragment(e, data, state)
}
}
/**
- * Define a default inline renderer.
+ * On drop fragment.
*
- * @type {Component}
+ * @param {Event} e
+ * @param {Object} data
+ * @param {State} state
+ * @return {State}
*/
- class DEFAULT_INLINE extends React.Component {
- render = () => {
- const { attributes, children } = this.props
- return {children}
+ function onDropFragment(e, data, state) {
+ const { selection } = state
+ let { fragment, target, isInternal } = data
+
+ // If the drag is internal and the target is after the selection, it
+ // needs to account for the selection's content being deleted.
+ if (
+ isInternal &&
+ selection.endKey == target.endKey &&
+ selection.endOffset < target.endOffset
+ ) {
+ target = target.moveBackward(selection.startKey == selection.endKey
+ ? selection.endOffset - selection.startOffset
+ : selection.endOffset)
+ }
+
+ let transform = state.transform()
+
+ if (isInternal) transform = transform.delete()
+
+ return transform
+ .moveTo(target)
+ .insertFragment(fragment)
+ .apply()
+ }
+
+ /**
+ * On drop text, split the blocks at new lines.
+ *
+ * @param {Event} e
+ * @param {Object} data
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onDropText(e, data, state) {
+ const { text, target } = data
+ let transform = state
+ .transform()
+ .moveTo(target)
+
+ text
+ .split('\n')
+ .forEach((line, i) => {
+ if (i > 0) transform = transform.splitBlock()
+ transform = transform.insertText(line)
+ })
+
+ return transform.apply()
+ }
+
+ /**
+ * On key down.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onKeyDown(e, state) {
+ switch (keycode(e.which)) {
+ case 'enter': return onKeyDownEnter(e, state)
+ case 'backspace': return onKeyDownBackspace(e, state)
+ case 'delete': return onKeyDownDelete(e, state)
+ case 'y': return onKeyDownY(e, state)
+ case 'z': return onKeyDownZ(e, state)
}
}
/**
- * Return the plugin.
+ * On `enter` key down, split the current block in half.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onKeyDownEnter(e, state) {
+ const { document, startKey, startBlock } = state
+
+ // For void blocks, we don't want to split. Instead we just move to the
+ // start of the next text node if one exists.
+ if (startBlock && startBlock.isVoid) {
+ const text = document.getNextText(startKey)
+ if (!text) return
+ return state
+ .transform()
+ .collapseToStartOf(text)
+ .apply()
+ }
+
+ return state
+ .transform()
+ .splitBlock()
+ .apply()
+ }
+
+ /**
+ * On `backspace` key down, delete backwards.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onKeyDownBackspace(e, state) {
+ // If expanded, delete regularly.
+ if (state.isExpanded) {
+ return state
+ .transform()
+ .delete()
+ .apply()
+ }
+
+ const { startOffset, startBlock } = state
+ const text = startBlock.text
+ let n
+
+ // Determine how far backwards to delete.
+ if (Key.isWord(e)) {
+ n = String.getWordOffsetBackward(text, startOffset)
+ } else if (Key.isLine(e)) {
+ n = startOffset
+ } else {
+ n = String.getCharOffsetBackward(text, startOffset)
+ }
+
+ return state
+ .transform()
+ .deleteBackward(n)
+ .apply()
+ }
+
+ /**
+ * On `delete` key down, delete forwards.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onKeyDownDelete(e, state) {
+ // If expanded, delete regularly.
+ if (state.isExpanded) {
+ return state
+ .transform()
+ .delete()
+ .apply()
+ }
+
+ const { startOffset, startBlock } = state
+ const text = startBlock.text
+ let n
+
+ // Determine how far forwards to delete.
+ if (Key.isWord(e)) {
+ n = String.getWordOffsetForward(text, startOffset)
+ } else if (Key.isLine(e)) {
+ n = text.length - startOffset
+ } else {
+ n = String.getCharOffsetForward(text, startOffset)
+ }
+
+ return state
+ .transform()
+ .deleteForward(n)
+ .apply()
+ }
+
+ /**
+ * On `y` key down, redo.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onKeyDownY(e, state) {
+ if (!Key.isWindowsCommand(e)) return
+ return state
+ .transform()
+ .redo()
+ }
+
+ /**
+ * On `z` key down, undo or redo.
+ *
+ * @param {Event} e
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onKeyDownZ(e, state) {
+ if (!Key.isCommand(e)) return
+ return state
+ .transform()
+ [IS_MAC && Key.isShift(e) ? 'redo' : 'undo']()
+ }
+
+ /**
+ * On paste.
+ *
+ * @param {Event} e
+ * @param {Object} data
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onPaste(e, data, state) {
+ switch (data.type) {
+ case 'text':
+ case 'html':
+ return onPasteText(e, data, state)
+ }
+ }
+
+ /**
+ * On paste text, split blocks at new lines.
+ *
+ * @param {Event} e
+ * @param {Object} data
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onPasteText(e, data, state) {
+ let transform = state.transform()
+
+ data.text
+ .split('\n')
+ .forEach((line, i) => {
+ if (i > 0) transform = transform.splitBlock()
+ transform = transform.insertText(line)
+ })
+
+ return transform.apply()
+ }
+
+ /**
+ * On select.
+ *
+ * @param {Event} e
+ * @param {Object} data
+ * @param {State} state
+ * @return {State}
+ */
+
+ function onSelect(e, data, state) {
+ const { selection, isNative } = data
+ return state
+ .transform()
+ .moveTo(selection)
+ .focus()
+ .apply({ isNative })
+ }
+
+ /**
+ * The core `node` renderer, which uses plain `` or `
` depending on
+ * what kind of node it is.
+ *
+ * @param {Node} node
+ * @return {Component} component
+ */
+
+ function renderNode(node) {
+ return node.kind == 'block'
+ ? DEFAULT_BLOCK
+ : DEFAULT_INLINE
+ }
+
+ /**
+ * Return the core plugin.
*/
return {
-
- /**
- * The core `onBeforeInput` handler.
- *
- * @param {Event} e
- * @param {State} state
- * @param {Editor} editor
- * @return {State or Null}
- */
-
- onBeforeInput(e, state, editor) {
- const { renderDecorations } = editor
- const { startOffset, startText, startBlock } = state
-
- // Determine what the characters would be if natively inserted.
- const prev = startText.getDecoratedCharacters(startBlock, renderDecorations)
- const char = prev.get(startOffset)
- const chars = prev
- .slice(0, startOffset)
- .push(Character.create({ text: e.data, marks: char && char.marks }))
- .concat(prev.slice(startOffset))
-
- // Determine what the characters should be, if not natively inserted.
- let next = state
- .transform()
- .insertText(e.data)
- .apply()
-
- const nextText = next.startText
- const nextBlock = next.startBlock
- const nextChars = nextText.getDecoratedCharacters(nextBlock, renderDecorations)
-
- // We do not have to re-render if the current selection is collapsed, the
- // current node is not empty, there are no marks on the cursor, and the
- // natively inserted characters would be the same as the non-native.
- const isNative = (
- state.isCollapsed &&
- state.startText.text != '' &&
- state.cursorMarks == null &&
- chars.equals(nextChars)
- )
-
- // Add the `isNative` flag directly, so we don't have to re-transform.
- if (isNative) {
- next = next.merge({ isNative })
- }
-
- // If not native, prevent default so that the DOM remains untouched.
- if (!isNative) e.preventDefault()
-
- // Return the new state.
- return next
- },
-
- /**
- * The core `onDrop` handler.
- *
- * @param {Event} e
- * @param {Object} drop
- * @param {State} state
- * @param {Editor} editor
- * @return {State or Null}
- */
-
- onDrop(e, drop, state, editor) {
- switch (drop.type) {
- case 'fragment': {
- const { selection } = state
- let { fragment, target, isInternal } = drop
-
- // If the drag is internal and the target is after the selection, it
- // needs to account for the selection's content being deleted.
- if (
- isInternal &&
- selection.endKey == target.endKey &&
- selection.endOffset < target.endOffset
- ) {
- target = target.moveBackward(selection.startKey == selection.endKey
- ? selection.endOffset - selection.startOffset
- : selection.endOffset)
- }
-
- let transform = state.transform()
-
- if (isInternal) transform = transform.delete()
-
- return transform
- .moveTo(target)
- .insertFragment(fragment)
- .apply()
- }
-
- case 'text':
- case 'html': {
- const { text, target } = drop
- let transform = state
- .transform()
- .moveTo(target)
-
- text
- .split('\n')
- .forEach((line, i) => {
- if (i > 0) transform = transform.splitBlock()
- transform = transform.insertText(line)
- })
-
- return transform.apply()
- }
- }
- },
-
- /**
- * The core `onKeyDown` handler.
- *
- * @param {Event} e
- * @param {State} state
- * @param {Editor} editor
- * @return {State or Null}
- */
-
- onKeyDown(e, state, editor) {
- const key = keycode(e.which)
- let transform = state.transform()
-
- switch (key) {
- case 'enter': {
- const { startBlock } = state
- if (startBlock && !startBlock.isVoid) return transform.splitBlock().apply()
-
- const { document, startKey } = state
- const text = document.getNextText(startKey)
- if (!text) return
-
- return transform.collapseToStartOf(text).apply()
- }
-
- case 'backspace': {
- if (state.isExpanded) return transform.delete().apply()
- const { startOffset, startBlock } = state
- const text = startBlock.text
- let n
-
- if (Key.isWord(e)) {
- n = String.getWordOffsetBackward(text, startOffset)
- } else if (Key.isLine(e)) {
- n = startOffset
- } else {
- n = String.getCharOffsetBackward(text, startOffset)
- }
-
- return transform.deleteBackward(n).apply()
- }
-
- case 'delete': {
- if (state.isExpanded) return transform.delete().apply()
- const { startOffset, startBlock } = state
- const text = startBlock.text
- let n
-
- if (Key.isWord(e)) {
- n = String.getWordOffsetForward(text, startOffset)
- } else if (Key.isLine(e)) {
- n = text.length - startOffset
- } else {
- n = String.getCharOffsetForward(text, startOffset)
- }
-
- return transform.deleteForward(n).apply()
- }
-
- case 'up': {
- if (state.isExpanded) return
- const first = state.blocks.first()
- if (!first || !first.isVoid) return
- e.preventDefault()
- return transform.collapseToEndOfPreviousBlock().apply()
- }
-
- case 'down': {
- if (state.isExpanded) return
- const first = state.blocks.first()
- if (!first || !first.isVoid) return
- e.preventDefault()
- return transform.collapseToStartOfNextBlock().apply()
- }
-
- case 'left': {
- if (state.isExpanded) return
- const node = state.blocks.first() || state.inlines.first()
- if (!node || !node.isVoid) return
- e.preventDefault()
- return transform.collapseToEndOfPreviousText().apply()
- }
-
- case 'right': {
- if (state.isExpanded) return
- const node = state.blocks.first() || state.inlines.first()
- if (!node || !node.isVoid) return
- e.preventDefault()
- return transform.collapseToStartOfNextText().apply()
- }
-
- case 'y': {
- if (!Key.isWindowsCommand(e)) return
- return transform.redo()
- }
-
- case 'z': {
- if (!Key.isCommand(e)) return
- return IS_MAC && Key.isShift(e)
- ? transform.redo()
- : transform.undo()
- }
- }
- },
-
- /**
- * The core `onPaste` handler, which treats everything as plain text.
- *
- * @param {Event} e
- * @param {Object} paste
- * @param {State} state
- * @param {Editor} editor
- * @return {State or Null}
- */
-
- onPaste(e, paste, state, editor) {
- switch (paste.type) {
- case 'text':
- case 'html': {
- let transform = state.transform()
-
- paste.text
- .split('\n')
- .forEach((line, i) => {
- if (i > 0) transform = transform.splitBlock()
- transform = transform.insertText(line)
- })
-
- return transform.apply()
- }
- }
- },
-
- /**
- * The core `onSelect` handler.
- *
- * @param {Event} e
- * @param {Object} select
- * @param {State} state
- * @param {Editor} editor
- * @return {State or Null}
- */
-
- onSelect(e, select, state, editor) {
- const { selection, isNative } = select
- return state
- .transform()
- .moveTo(selection)
- .focus()
- .apply({ isNative })
- },
-
- /**
- * The core `node` renderer, which uses plain `` or `` depending on
- * what kind of node it is.
- *
- * @param {Node} node
- * @return {Component} component
- */
-
- renderNode(node) {
- return node.kind == 'block'
- ? DEFAULT_BLOCK
- : DEFAULT_INLINE
- }
+ onBeforeInput,
+ onDrop,
+ onKeyDown,
+ onPaste,
+ onSelect,
+ renderNode
}
}