1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 09:59:48 +02:00

refactor core plugin for readability

This commit is contained in:
Ian Storm Taylor
2016-07-27 12:54:04 -07:00
parent b9ae5d2af6
commit ee2192aa6e

View File

@@ -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,333 +25,409 @@ 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 = () => { return (
const { attributes, children } = this.props <div {...props.attributes} style={{ position: 'relative' }}>
return ( {props.children}
<div {...attributes} style={{ position: 'relative' }}> {placeholder
{this.renderPlaceholder()} ? <Placeholder
{children} className={placeholderClassName}
</div> node={props.node}
) parent={props.state.document}
state={props.state}
style={placeholderStyle}
>
{placeholder}
</Placeholder>
: null}
</div>
)
}
/**
* The default inline renderer.
*
* @param {Object} props
* @return {Element}
*/
function DEFAULT_INLINE(props) {
return (
<span {...props.attributes} style={{ position: 'relative' }}>
{props.children}
</span>
)
}
/**
* 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 not native, prevent default so that the DOM remains untouched.
if (!placeholder) return null if (!isNative) e.preventDefault()
const { node, state } = this.props
return ( // Return the new state.
<Placeholder return next
className={placeholderClassName} }
node={node}
parent={state.document} /**
state={state} * On drop.
style={placeholderStyle} *
> * @param {Event} e
{placeholder} * @param {Object} data
</Placeholder> * @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 { function onDropFragment(e, data, state) {
render = () => { const { selection } = state
const { attributes, children } = this.props let { fragment, target, isInternal } = data
return <span {...attributes} style={{ position: 'relative' }}>{children}</span>
// 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 `<div>` or `<span>` 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 { return {
onBeforeInput,
/** onDrop,
* The core `onBeforeInput` handler. onKeyDown,
* onPaste,
* @param {Event} e onSelect,
* @param {State} state renderNode
* @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 `<div>` or `<span>` depending on
* what kind of node it is.
*
* @param {Node} node
* @return {Component} component
*/
renderNode(node) {
return node.kind == 'block'
? DEFAULT_BLOCK
: DEFAULT_INLINE
}
} }
} }