mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-29 18:09:49 +02:00
refactor core plugin for readability
This commit is contained in:
@@ -6,7 +6,6 @@ import React from 'react'
|
|||||||
import String from '../utils/string'
|
import String from '../utils/string'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
||||||
import OffsetKey from '../utils/offset-key'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default plugin.
|
* The default plugin.
|
||||||
@@ -26,68 +25,57 @@ function Plugin(options = {}) {
|
|||||||
} = options
|
} = options
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a default block renderer.
|
* The default block renderer.
|
||||||
*
|
*
|
||||||
* @type {Component}
|
* @param {Object} props
|
||||||
|
* @return {Element}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class DEFAULT_BLOCK extends React.Component {
|
function DEFAULT_BLOCK(props) {
|
||||||
render = () => {
|
|
||||||
const { attributes, children } = this.props
|
|
||||||
return (
|
return (
|
||||||
<div {...attributes} style={{ position: 'relative' }}>
|
<div {...props.attributes} style={{ position: 'relative' }}>
|
||||||
{this.renderPlaceholder()}
|
{props.children}
|
||||||
{children}
|
{placeholder
|
||||||
</div>
|
? <Placeholder
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPlaceholder = () => {
|
|
||||||
if (!placeholder) return null
|
|
||||||
const { node, state } = this.props
|
|
||||||
return (
|
|
||||||
<Placeholder
|
|
||||||
className={placeholderClassName}
|
className={placeholderClassName}
|
||||||
node={node}
|
node={props.node}
|
||||||
parent={state.document}
|
parent={props.state.document}
|
||||||
state={state}
|
state={props.state}
|
||||||
style={placeholderStyle}
|
style={placeholderStyle}
|
||||||
>
|
>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a default inline renderer.
|
* The default inline renderer.
|
||||||
*
|
*
|
||||||
* @type {Component}
|
* @param {Object} props
|
||||||
|
* @return {Element}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class DEFAULT_INLINE extends React.Component {
|
function DEFAULT_INLINE(props) {
|
||||||
render = () => {
|
return (
|
||||||
const { attributes, children } = this.props
|
<span {...props.attributes} style={{ position: 'relative' }}>
|
||||||
return <span {...attributes} style={{ position: 'relative' }}>{children}</span>
|
{props.children}
|
||||||
}
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the plugin.
|
* On before input, see if we can let the browser continue with it's native
|
||||||
*/
|
* input behavior, to avoid a re-render for performance.
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The core `onBeforeInput` handler.
|
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
* @param {Editor} editor
|
||||||
* @return {State or Null}
|
* @return {State}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onBeforeInput(e, state, editor) {
|
function onBeforeInput(e, state, editor) {
|
||||||
const { renderDecorations } = editor
|
const { renderDecorations } = editor
|
||||||
const { startOffset, startText, startBlock } = state
|
const { startOffset, startText, startBlock } = state
|
||||||
|
|
||||||
@@ -129,23 +117,39 @@ function Plugin(options = {}) {
|
|||||||
|
|
||||||
// Return the new state.
|
// Return the new state.
|
||||||
return next
|
return next
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core `onDrop` handler.
|
* On drop.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {Object} drop
|
* @param {Object} data
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
* @return {State}
|
||||||
* @return {State or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onDrop(e, drop, state, editor) {
|
function onDrop(e, data, state) {
|
||||||
switch (drop.type) {
|
switch (data.type) {
|
||||||
case 'fragment': {
|
case 'text':
|
||||||
|
case 'html':
|
||||||
|
return onDropText(e, data, state)
|
||||||
|
case 'fragment':
|
||||||
|
return onDropFragment(e, data, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On drop fragment.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onDropFragment(e, data, state) {
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
let { fragment, target, isInternal } = drop
|
let { fragment, target, isInternal } = data
|
||||||
|
|
||||||
// If the drag is internal and the target is after the selection, it
|
// If the drag is internal and the target is after the selection, it
|
||||||
// needs to account for the selection's content being deleted.
|
// needs to account for the selection's content being deleted.
|
||||||
@@ -169,9 +173,17 @@ function Plugin(options = {}) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'text':
|
/**
|
||||||
case 'html': {
|
* On drop text, split the blocks at new lines.
|
||||||
const { text, target } = drop
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onDropText(e, data, state) {
|
||||||
|
const { text, target } = data
|
||||||
let transform = state
|
let transform = state
|
||||||
.transform()
|
.transform()
|
||||||
.moveTo(target)
|
.moveTo(target)
|
||||||
@@ -185,40 +197,75 @@ function Plugin(options = {}) {
|
|||||||
|
|
||||||
return transform.apply()
|
return transform.apply()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core `onKeyDown` handler.
|
* On key down.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
* @return {State}
|
||||||
* @return {State or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onKeyDown(e, state, editor) {
|
function onKeyDown(e, state) {
|
||||||
const key = keycode(e.which)
|
switch (keycode(e.which)) {
|
||||||
let transform = state.transform()
|
case 'enter': return onKeyDownEnter(e, state)
|
||||||
|
case 'backspace': return onKeyDownBackspace(e, state)
|
||||||
switch (key) {
|
case 'delete': return onKeyDownDelete(e, state)
|
||||||
case 'enter': {
|
case 'y': return onKeyDownY(e, state)
|
||||||
const { startBlock } = state
|
case 'z': return onKeyDownZ(e, state)
|
||||||
if (startBlock && !startBlock.isVoid) return transform.splitBlock().apply()
|
}
|
||||||
|
}
|
||||||
const { document, startKey } = state
|
|
||||||
const text = document.getNextText(startKey)
|
/**
|
||||||
if (!text) return
|
* On `enter` key down, split the current block in half.
|
||||||
|
*
|
||||||
return transform.collapseToStartOf(text).apply()
|
* @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()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'backspace': {
|
|
||||||
if (state.isExpanded) return transform.delete().apply()
|
|
||||||
const { startOffset, startBlock } = state
|
const { startOffset, startBlock } = state
|
||||||
const text = startBlock.text
|
const text = startBlock.text
|
||||||
let n
|
let n
|
||||||
|
|
||||||
|
// Determine how far backwards to delete.
|
||||||
if (Key.isWord(e)) {
|
if (Key.isWord(e)) {
|
||||||
n = String.getWordOffsetBackward(text, startOffset)
|
n = String.getWordOffsetBackward(text, startOffset)
|
||||||
} else if (Key.isLine(e)) {
|
} else if (Key.isLine(e)) {
|
||||||
@@ -227,15 +274,34 @@ function Plugin(options = {}) {
|
|||||||
n = String.getCharOffsetBackward(text, startOffset)
|
n = String.getCharOffsetBackward(text, startOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
return transform.deleteBackward(n).apply()
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'delete': {
|
|
||||||
if (state.isExpanded) return transform.delete().apply()
|
|
||||||
const { startOffset, startBlock } = state
|
const { startOffset, startBlock } = state
|
||||||
const text = startBlock.text
|
const text = startBlock.text
|
||||||
let n
|
let n
|
||||||
|
|
||||||
|
// Determine how far forwards to delete.
|
||||||
if (Key.isWord(e)) {
|
if (Key.isWord(e)) {
|
||||||
n = String.getWordOffsetForward(text, startOffset)
|
n = String.getWordOffsetForward(text, startOffset)
|
||||||
} else if (Key.isLine(e)) {
|
} else if (Key.isLine(e)) {
|
||||||
@@ -244,72 +310,72 @@ function Plugin(options = {}) {
|
|||||||
n = String.getCharOffsetForward(text, startOffset)
|
n = String.getCharOffsetForward(text, startOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
return transform.deleteForward(n).apply()
|
return state
|
||||||
|
.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.
|
* On `y` key down, redo.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {Object} paste
|
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
* @return {State}
|
||||||
* @return {State or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onPaste(e, paste, state, editor) {
|
function onKeyDownY(e, state) {
|
||||||
switch (paste.type) {
|
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 'text':
|
||||||
case 'html': {
|
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()
|
let transform = state.transform()
|
||||||
|
|
||||||
paste.text
|
data.text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.forEach((line, i) => {
|
.forEach((line, i) => {
|
||||||
if (i > 0) transform = transform.splitBlock()
|
if (i > 0) transform = transform.splitBlock()
|
||||||
@@ -318,27 +384,24 @@ function Plugin(options = {}) {
|
|||||||
|
|
||||||
return transform.apply()
|
return transform.apply()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core `onSelect` handler.
|
* On select.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {Object} select
|
* @param {Object} data
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
* @return {State}
|
||||||
* @return {State or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onSelect(e, select, state, editor) {
|
function onSelect(e, data, state) {
|
||||||
const { selection, isNative } = select
|
const { selection, isNative } = data
|
||||||
return state
|
return state
|
||||||
.transform()
|
.transform()
|
||||||
.moveTo(selection)
|
.moveTo(selection)
|
||||||
.focus()
|
.focus()
|
||||||
.apply({ isNative })
|
.apply({ isNative })
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core `node` renderer, which uses plain `<div>` or `<span>` depending on
|
* The core `node` renderer, which uses plain `<div>` or `<span>` depending on
|
||||||
@@ -348,11 +411,23 @@ function Plugin(options = {}) {
|
|||||||
* @return {Component} component
|
* @return {Component} component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode(node) {
|
function renderNode(node) {
|
||||||
return node.kind == 'block'
|
return node.kind == 'block'
|
||||||
? DEFAULT_BLOCK
|
? DEFAULT_BLOCK
|
||||||
: DEFAULT_INLINE
|
: DEFAULT_INLINE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the core plugin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
onBeforeInput,
|
||||||
|
onDrop,
|
||||||
|
onKeyDown,
|
||||||
|
onPaste,
|
||||||
|
onSelect,
|
||||||
|
renderNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user