1
0
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:
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,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
} }
} }