1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-16 20:24:01 +02:00

fix plugins stack ordering and defaulting

This commit is contained in:
Ian Storm Taylor
2018-10-09 18:43:47 -07:00
parent 7304c9b343
commit 3528bb7366
47 changed files with 943 additions and 721 deletions

View File

@@ -139,11 +139,12 @@ This sounds weird, but it can be pretty useful if you want to render additional
```js
function renderEditor(props, next) {
const { children, editor } = props
const { editor } = props
const wordCount = countWords(editor.value.text)
const children = next()
return (
<React.Fragment>
{props.children}
{children}
<span className="word-count">{wordCount}</span>
</React.Fragment>
)

View File

@@ -100,12 +100,12 @@ Sometimes though, the declarative validation syntax isn't fine-grained enough to
When you define a `normalizeNode` function, you either return nothing if the node's already valid, or you return a normalizer function that will make the node valid if it isn't. Here's an example:
```js
function normalizeNode(node) {
function normalizeNode(node, next) {
const { nodes } = node
if (node.object !== 'block') return
if (nodes.size !== 3) return
if (nodes.first().object !== 'text') return
if (nodes.last().object !== 'text') return
if (node.object !== 'block') return next()
if (nodes.size !== 3) return next()
if (nodes.first().object !== 'text') return next()
if (nodes.last().object !== 'text') return next()
return change => change.removeNodeByKey(node.key)
}
```

View File

@@ -31,7 +31,7 @@ In addition to the [core plugin hooks](../slate/plugins.md), when using `slate-r
The event hooks have a signature of `(event, change, next)`—the `event` is a React object that you are used to from React's event handlers.
The rendering hooks are just like render props common to other React API's, and receive `(props, editor, next)`. For more information, see the [Rendering](./rendering.md) reference.
The rendering hooks are just like render props common to other React API's, and receive `(props, next)`. For more information, see the [Rendering](./rendering.md) reference.
### `decorateNode`
@@ -107,7 +107,7 @@ This handler is called whenever the native DOM selection changes.
### `renderEditor`
`Function renderEditor(props: Object, editor: Editor) => ReactNode|Void`
`Function renderEditor(props: Object, next: Function) => ReactNode|Void`
The `renderEditor` property allows you to define higher-order-component-like behavior. It is passed all of the properties of the editor, including `props.children`. You can then choose to wrap the existing `children` in any custom elements or proxy the properties however you choose. This can be useful for rendering toolbars, styling the editor, rendering validation, etc. Remember that the `renderEditor` function has to render `props.children` for editor's content to render.

View File

@@ -124,10 +124,12 @@ class CheckLists extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
switch (props.node.type) {
case 'check-list-item':
return <CheckListItem {...props} />
default:
return next()
}
}
@@ -152,15 +154,15 @@ class CheckLists extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @return {Value|Void}
* @param {Function} next
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
const { value } = change
if (event.key == 'Enter' && value.startBlock.type == 'check-list-item') {
change.splitBlock().setBlocks({ data: { checked: false } })
return true
return
}
if (
@@ -170,8 +172,10 @@ class CheckLists extends React.Component {
value.selection.startOffset == 0
) {
change.setBlocks('paragraph')
return true
return
}
next()
}
}

View File

@@ -106,12 +106,14 @@ class CodeHighlighting extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
switch (props.node.type) {
case 'code':
return <CodeBlock {...props} />
case 'code_line':
return <CodeBlockLine {...props} />
default:
return next()
}
}
@@ -122,7 +124,7 @@ class CodeHighlighting extends React.Component {
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -150,6 +152,8 @@ class CodeHighlighting extends React.Component {
{children}
</span>
)
default:
return next()
}
}
@@ -168,17 +172,20 @@ class CodeHighlighting extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Change}
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
const { value } = change
const { selection, startBlock } = value
if (event.key != 'Enter') return
if (startBlock.type != 'code') return
if (selection.isExpanded) change.delete()
const { startBlock } = value
if (event.key === 'Enter' && startBlock.type === 'code') {
change.insertText('\n')
return true
return
}
next()
}
/**

View File

@@ -58,13 +58,16 @@ class Embeds extends React.Component {
* Render a Slate node.
*
* @param {Object} props
* @return {Element}
* @param {Editor} editor
* @param {Function} next
*/
renderNode = props => {
renderNode = (props, next) => {
switch (props.node.type) {
case 'video':
return <Video {...props} />
default:
return next()
}
}

View File

@@ -123,16 +123,19 @@ class Emojis extends React.Component {
* Render a Slate node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node, isFocused } = props
switch (node.type) {
case 'paragraph': {
return <p {...attributes}>{children}</p>
}
case 'emoji': {
const code = node.data.get('code')
return (
@@ -146,6 +149,10 @@ class Emojis extends React.Component {
</Emoji>
)
}
default: {
return next()
}
}
}

View File

@@ -70,10 +70,12 @@ class ForcedLayout extends React.Component {
* Render a Slate node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
@@ -81,6 +83,8 @@ class ForcedLayout extends React.Component {
return <h2 {...attributes}>{children}</h2>
case 'paragraph':
return <p {...attributes}>{children}</p>
default:
return next()
}
}

View File

@@ -189,10 +189,12 @@ class HoveringMenu extends React.Component {
* Render a Slate mark.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -204,6 +206,8 @@ class HoveringMenu extends React.Component {
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
default:
return next()
}
}

View File

@@ -73,15 +73,19 @@ class HugeDocument extends React.Component {
* Render a Slate node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'heading':
return <h1 {...attributes}>{children}</h1>
default:
return next()
}
}
@@ -89,10 +93,12 @@ class HugeDocument extends React.Component {
* Render a Slate mark.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -104,6 +110,8 @@ class HugeDocument extends React.Component {
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
default:
return next()
}
}

View File

@@ -138,7 +138,7 @@ class Images extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, node, isFocused } = props
switch (node.type) {
@@ -146,6 +146,10 @@ class Images extends React.Component {
const src = node.data.get('src')
return <Image src={src} selected={isFocused} {...attributes} />
}
default: {
return next()
}
}
}
@@ -177,21 +181,22 @@ class Images extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
onDropOrPaste = (event, change) => {
onDropOrPaste = (event, change, next) => {
const { editor } = change
const target = getEventRange(event, editor)
if (!target && event.type == 'drop') return
if (!target && event.type === 'drop') return next()
const transfer = getEventTransfer(event)
const { type, text, files } = transfer
if (type == 'files') {
if (type === 'files') {
for (const file of files) {
const reader = new FileReader()
const [mime] = file.type.split('/')
if (mime != 'image') continue
if (mime !== 'image') continue
reader.addEventListener('load', () => {
editor.change(c => {
@@ -201,13 +206,17 @@ class Images extends React.Component {
reader.readAsDataURL(file)
}
return
}
if (type == 'text') {
if (!isUrl(text)) return
if (!isImage(text)) return
if (type === 'text') {
if (!isUrl(text)) return next()
if (!isImage(text)) return next()
change.call(insertImage, text, target)
return
}
next()
}
}

View File

@@ -223,7 +223,17 @@ class InputTester extends React.Component {
ref={this.ref}
value={this.state.value}
onChange={this.onChange}
renderNode={({ attributes, children, node }) => {
renderNode={this.renderNode}
renderMark={this.renderMark}
/>
<EventsList />
</Wrapper>
)
}
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
@@ -237,9 +247,14 @@ class InputTester extends React.Component {
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
default:
return next()
}
}}
renderMark={({ attributes, children, mark }) => {
}
renderMark = (props, next) => {
const { attributes, children, mark } = props
switch (mark.type) {
case 'bold':
return <strong {...attributes}>{children}</strong>
@@ -249,12 +264,9 @@ class InputTester extends React.Component {
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
default:
return next()
}
}}
/>
<EventsList />
</Wrapper>
)
}
onRef = ref => {

View File

@@ -100,10 +100,12 @@ class Links extends React.Component {
* Render a Slate node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
@@ -116,6 +118,10 @@ class Links extends React.Component {
</a>
)
}
default: {
return next()
}
}
}
@@ -179,22 +185,22 @@ class Links extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
onPaste = (event, change) => {
if (change.value.selection.isCollapsed) return
onPaste = (event, change, next) => {
if (change.value.selection.isCollapsed) return next()
const transfer = getEventTransfer(event)
const { type, text } = transfer
if (type != 'text' && type != 'html') return
if (!isUrl(text)) return
if (type != 'text' && type != 'html') return next()
if (!isUrl(text)) return next()
if (this.hasLinks()) {
change.call(unwrapLink)
}
change.call(wrapLink, text)
return true
}
}

View File

@@ -53,21 +53,27 @@ class MarkdownPreview extends React.Component {
* Render a Slate mark.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
case 'bold':
return <strong {...attributes}>{children}</strong>
case 'code':
return <code {...attributes}>{children}</code>
case 'italic':
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
case 'title': {
return (
<span
@@ -83,6 +89,7 @@ class MarkdownPreview extends React.Component {
</span>
)
}
case 'punctuation': {
return (
<span {...attributes} style={{ opacity: 0.2 }}>
@@ -90,6 +97,7 @@ class MarkdownPreview extends React.Component {
</span>
)
}
case 'list': {
return (
<span
@@ -104,6 +112,7 @@ class MarkdownPreview extends React.Component {
</span>
)
}
case 'hr': {
return (
<span
@@ -118,6 +127,10 @@ class MarkdownPreview extends React.Component {
</span>
)
}
default: {
return next()
}
}
}

View File

@@ -76,10 +76,12 @@ class MarkdownShortcuts extends React.Component {
* Render a Slate node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
@@ -101,6 +103,8 @@ class MarkdownShortcuts extends React.Component {
return <h6 {...attributes}>{children}</h6>
case 'list-item':
return <li {...attributes}>{children}</li>
default:
return next()
}
}
@@ -119,16 +123,19 @@ class MarkdownShortcuts extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
switch (event.key) {
case ' ':
return this.onSpace(event, change)
return this.onSpace(event, change, next)
case 'Backspace':
return this.onBackspace(event, change)
return this.onBackspace(event, change, next)
case 'Enter':
return this.onEnter(event, change)
return this.onEnter(event, change, next)
default:
return next()
}
}
@@ -138,20 +145,20 @@ class MarkdownShortcuts extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
onSpace = (event, change) => {
onSpace = (event, change, next) => {
const { value } = change
const { selection } = value
if (selection.isExpanded) return
if (selection.isExpanded) return next()
const { startBlock } = value
const { start } = selection
const chars = startBlock.text.slice(0, start.offset).replace(/\s*/g, '')
const type = this.getType(chars)
if (!type) return
if (type == 'list-item' && startBlock.type == 'list-item') return
if (!type) return next()
if (type == 'list-item' && startBlock.type == 'list-item') return next()
event.preventDefault()
change.setBlocks(type)
@@ -161,7 +168,6 @@ class MarkdownShortcuts extends React.Component {
}
change.moveFocusToStartOfNode(startBlock).delete()
return true
}
/**
@@ -170,16 +176,17 @@ class MarkdownShortcuts extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
onBackspace = (event, change) => {
onBackspace = (event, change, next) => {
const { value } = change
const { selection } = value
if (selection.isExpanded) return
if (selection.start.offset != 0) return
if (selection.isExpanded) return next()
if (selection.start.offset != 0) return next()
const { startBlock } = value
if (startBlock.type == 'paragraph') return
if (startBlock.type == 'paragraph') return next()
event.preventDefault()
change.setBlocks('paragraph')
@@ -187,8 +194,6 @@ class MarkdownShortcuts extends React.Component {
if (startBlock.type == 'list-item') {
change.unwrapBlock('bulleted-list')
}
return true
}
/**
@@ -197,18 +202,19 @@ class MarkdownShortcuts extends React.Component {
*
* @param {Event} event
* @param {Change} change
* @param {Function} next
*/
onEnter = (event, change) => {
onEnter = (event, change, next) => {
const { value } = change
const { selection } = value
const { start, end, isExpanded } = selection
if (isExpanded) return
if (isExpanded) return next()
const { startBlock } = value
if (start.offset == 0 && startBlock.text.length == 0)
return this.onBackspace(event, change)
if (end.offset != startBlock.text.length) return
return this.onBackspace(event, change, next)
if (end.offset != startBlock.text.length) return next()
if (
startBlock.type != 'heading-one' &&
@@ -219,12 +225,11 @@ class MarkdownShortcuts extends React.Component {
startBlock.type != 'heading-six' &&
startBlock.type != 'block-quote'
) {
return
return next()
}
event.preventDefault()
change.splitBlock().setBlocks('paragraph')
return true
}
}

View File

@@ -203,7 +203,7 @@ class PasteHtml extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node, isFocused } = props
switch (node.type) {
@@ -246,6 +246,10 @@ class PasteHtml extends React.Component {
const src = node.data.get('src')
return <Image src={src} selected={isFocused} {...attributes} />
}
default: {
return next()
}
}
}
@@ -256,7 +260,7 @@ class PasteHtml extends React.Component {
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -268,6 +272,8 @@ class PasteHtml extends React.Component {
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
default:
return next()
}
}
@@ -288,12 +294,11 @@ class PasteHtml extends React.Component {
* @param {Change} change
*/
onPaste = (event, change) => {
onPaste = (event, change, next) => {
const transfer = getEventTransfer(event)
if (transfer.type != 'html') return
if (transfer.type != 'html') return next()
const { document } = serializer.deserialize(transfer.html)
change.insertFragment(document)
return true
}
}

View File

@@ -28,10 +28,11 @@ const WordCounter = styled('span')`
function WordCount(options) {
return {
renderEditor(props) {
renderEditor(props, next) {
const children = next()
return (
<div>
<div>{props.children}</div>
<div>{children}</div>
<WordCounter>
Word Count: {props.value.document.text.split(' ').length}
</WordCounter>

View File

@@ -166,7 +166,7 @@ class RichTextExample extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
@@ -182,6 +182,8 @@ class RichTextExample extends React.Component {
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
default:
return next()
}
}
@@ -192,7 +194,7 @@ class RichTextExample extends React.Component {
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -204,6 +206,8 @@ class RichTextExample extends React.Component {
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
default:
return next()
}
}
@@ -225,7 +229,7 @@ class RichTextExample extends React.Component {
* @return {Change}
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
let mark
if (isBoldHotkey(event)) {
@@ -237,12 +241,11 @@ class RichTextExample extends React.Component {
} else if (isCodeHotkey(event)) {
mark = 'code'
} else {
return
return next()
}
event.preventDefault()
change.toggleMark(mark)
return true
}
/**

View File

@@ -46,12 +46,14 @@ class RTL extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
default:
return next()
}
}
@@ -72,12 +74,14 @@ class RTL extends React.Component {
* @param {Change} change
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
if (event.key == 'Enter' && event.shiftKey) {
event.preventDefault()
change.insertText('\n')
return true
return
}
next()
}
}

View File

@@ -108,7 +108,7 @@ class SearchHighlighting extends React.Component {
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -118,6 +118,8 @@ class SearchHighlighting extends React.Component {
{children}
</span>
)
default:
return next()
}
}

View File

@@ -136,7 +136,7 @@ class SyncingEditor extends React.Component {
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
@@ -148,6 +148,8 @@ class SyncingEditor extends React.Component {
return <em {...attributes}>{children}</em>
case 'underlined':
return <u {...attributes}>{children}</u>
default:
return next()
}
}
@@ -175,7 +177,7 @@ class SyncingEditor extends React.Component {
* @return {Change}
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
let mark
if (isBoldHotkey(event)) {
@@ -187,12 +189,11 @@ class SyncingEditor extends React.Component {
} else if (isCodeHotkey(event)) {
mark = 'code'
} else {
return
return next()
}
event.preventDefault()
change.toggleMark(mark)
return true
}
/**

View File

@@ -50,7 +50,7 @@ class Tables extends React.Component {
* @return {Element}
*/
renderNode = props => {
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
@@ -64,6 +64,8 @@ class Tables extends React.Component {
return <tr {...attributes}>{children}</tr>
case 'table-cell':
return <td {...attributes}>{children}</td>
default:
return next()
}
}
@@ -74,12 +76,14 @@ class Tables extends React.Component {
* @return {Element}
*/
renderMark = props => {
renderMark = (props, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
case 'bold':
return <strong {...attributes}>{children}</strong>
default:
return next()
}
}
@@ -90,12 +94,11 @@ class Tables extends React.Component {
* @param {Change} change
*/
onBackspace = (event, change) => {
onBackspace = (event, change, next) => {
const { value } = change
const { selection } = value
if (selection.start.offset != 0) return
if (selection.start.offset != 0) return next()
event.preventDefault()
return true
}
/**
@@ -115,12 +118,11 @@ class Tables extends React.Component {
* @param {Change} change
*/
onDelete = (event, change) => {
onDelete = (event, change, next) => {
const { value } = change
const { selection } = value
if (selection.end.offset != value.startText.text.length) return
if (selection.end.offset != value.startText.text.length) return next()
event.preventDefault()
return true
}
/**
@@ -130,23 +132,22 @@ class Tables extends React.Component {
* @param {Change} change
*/
onDropOrPaste = (event, change) => {
onDropOrPaste = (event, change, next) => {
const transfer = getEventTransfer(event)
const { value } = change
const { text = '' } = transfer
if (value.startBlock.type !== 'table-cell') {
return
return next()
}
if (!text) {
return
return next()
}
const lines = text.split('\n')
const { document } = Plain.deserialize(lines[0] || '')
change.insertFragment(document)
return false
}
/**
@@ -156,9 +157,8 @@ class Tables extends React.Component {
* @param {Change} change
*/
onEnter = (event, change) => {
onEnter = (event, change, next) => {
event.preventDefault()
return true
}
/**
@@ -168,7 +168,7 @@ class Tables extends React.Component {
* @param {Change} change
*/
onKeyDown = (event, change) => {
onKeyDown = (event, change, next) => {
const { value } = change
const { document, selection } = value
const { start, isCollapsed } = selection
@@ -181,24 +181,25 @@ class Tables extends React.Component {
if (prevBlock.type === 'table-cell') {
if (['Backspace', 'Delete', 'Enter'].includes(event.key)) {
event.preventDefault()
return true
} else {
return
return next()
}
}
}
if (value.startBlock.type !== 'table-cell') {
return
return next()
}
switch (event.key) {
case 'Backspace':
return this.onBackspace(event, change)
return this.onBackspace(event, change, next)
case 'Delete':
return this.onDelete(event, change)
return this.onDelete(event, change, next)
case 'Enter':
return this.onEnter(event, change)
return this.onEnter(event, change, next)
default:
return next()
}
}
}

View File

@@ -8,8 +8,6 @@ import warning from 'tiny-warning'
import { Editor as Controller } from 'slate'
import EVENT_HANDLERS from '../constants/event-handlers'
import BrowserPlugin from '../plugins/browser'
import PropsPlugin from '../plugins/props'
import ReactPlugin from '../plugins/react'
/**
@@ -143,7 +141,7 @@ class Editor extends React.Component {
render() {
debug('render', this)
const props = { ...this.props }
const props = { ...this.props, editor: this }
// Re-resolve the controller if needed based on memoized props.
const { commands, plugins, queries, schema } = props
@@ -155,7 +153,7 @@ class Editor extends React.Component {
this.controller.setValue(value, options)
// Render the editor's children with the controller.
const children = this.controller.run('renderEditor', props, this)
const children = this.controller.run('renderEditor', props)
return children
}
@@ -182,12 +180,8 @@ class Editor extends React.Component {
'A Slate <Editor> component is re-resolving the `plugins`, `schema`, `commands` or `queries` on each update, which leads to poor performance. This is often due to passing in a new references for these props with each render by declaring them inline in your render function. Do not do this! Declare them outside your render function, or memoize them instead.'
)
const { props, onControllerChange } = this
const reactPlugin = ReactPlugin()
const browserPlugin = BrowserPlugin()
const propsPlugin = PropsPlugin(props)
const allPlugins = [reactPlugin, browserPlugin, propsPlugin, ...plugins]
const attrs = { onChange: onControllerChange, plugins: allPlugins }
const react = ReactPlugin(this.props)
const attrs = { onChange: this.onControllerChange, plugins: [react] }
this.controller = new Controller(attrs, { editor: this, normalize: false })
}

View File

@@ -166,7 +166,7 @@ class Node extends React.Component {
readOnly,
}
let placeholder = editor.run('renderPlaceholder', props, editor)
let placeholder = editor.run('renderPlaceholder', props)
if (placeholder) {
placeholder = React.cloneElement(placeholder, {
@@ -176,15 +176,11 @@ class Node extends React.Component {
children = [placeholder, ...children]
}
const element = editor.run(
'renderNode',
{
const element = editor.run('renderNode', {
...props,
attributes,
children,
},
editor
)
})
return editor.query('isVoid', node) ? (
<Void {...this.props}>{element}</Void>

View File

@@ -2,14 +2,8 @@ import Base64 from 'slate-base64-serializer'
import Debug from 'debug'
import Hotkeys from 'slate-hotkeys'
import Plain from 'slate-plain-serializer'
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 { IS_IOS } from 'slate-dev-environment'
import cloneFragment from '../utils/clone-fragment'
import findDOMNode from '../utils/find-dom-node'
@@ -26,20 +20,16 @@ import setEventTransfer from '../utils/set-event-transfer'
* @type {Function}
*/
const debug = Debug('slate:browser')
const debug = Debug('slate:after')
/**
* A plugin that adds the browser-specific logic to the editor.
* A plugin that adds the "after" browser-specific logic to the editor.
*
* @param {Object} options
* @return {Object}
*/
function BrowserPlugin() {
let activeElement = null
let compositionCount = 0
let isComposing = false
let isCopying = false
let isDragging = false
function AfterPlugin(options = {}) {
let isDraggingInternally = null
/**
@@ -48,24 +38,11 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onBeforeInput(event, change, next) {
const { editor, value } = change
const isSynthetic = !!event.nativeEvent
if (editor.readOnly) return true
// 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 true
debug('onBeforeInput', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
// If the event is synthetic, it's React's polyfill of `beforeinput` that
// isn't a true `beforeinput` event with meaningful information. It only
@@ -73,13 +50,15 @@ function BrowserPlugin() {
if (isSynthetic) {
event.preventDefault()
change.insertText(event.data)
return
return next()
}
// Otherwise, we can use the information in the `beforeinput` event to
// figure out the exact change that will occur, and prevent it.
const [targetRange] = event.getTargetRanges()
if (!targetRange) return
if (!targetRange) return next()
debug('onBeforeInput', { event })
event.preventDefault()
@@ -93,29 +72,29 @@ function BrowserPlugin() {
case 'deleteContentBackward':
case 'deleteContentForward': {
change.deleteAtRange(range)
return
break
}
case 'deleteWordBackward': {
change.deleteWordBackwardAtRange(range)
return
break
}
case 'deleteWordForward': {
change.deleteWordForwardAtRange(range)
return
break
}
case 'deleteSoftLineBackward':
case 'deleteHardLineBackward': {
change.deleteLineBackwardAtRange(range)
return
break
}
case 'deleteSoftLineForward':
case 'deleteHardLineForward': {
change.deleteLineForwardAtRange(range)
return
break
}
case 'insertLineBreak':
@@ -131,7 +110,7 @@ function BrowserPlugin() {
change.splitBlockAtRange(range)
}
return
break
}
case 'insertFromYank':
@@ -146,7 +125,7 @@ function BrowserPlugin() {
? event.dataTransfer.getData('text/plain')
: event.data
if (text == null) return
if (text == null) break
change.insertTextAtRange(range, text, selection.marks)
@@ -156,9 +135,11 @@ function BrowserPlugin() {
change.select({ marks: null })
}
return
break
}
}
next()
}
/**
@@ -167,88 +148,12 @@ function BrowserPlugin() {
* @param {Event} event
* @param {Change} change
* @param {Function} next
* @return {Boolean}
*/
function onBlur(event, change, next) {
const { editor } = change
if (isCopying) return true
if (editor.readOnly) return true
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 true
// COMPAT: The `relatedTarget` can be null when the new focus target is not
// a "focusable" element (eg. a `<div>` 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 <input> 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,8 +365,8 @@ 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
if (el) {
el.dispatchEvent(
new MouseEvent('mouseup', {
view: window,
@@ -620,40 +374,9 @@ function BrowserPlugin() {
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
}
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

View File

@@ -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 `<div>` 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 <input> 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

View File

@@ -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

View File

@@ -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 `<Editor>` 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

View File

@@ -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 (
<Content
onEvent={editor.event}
autoCorrect={props.autoCorrect}
@@ -34,27 +57,22 @@ function ReactPlugin() {
tagName={props.tagName}
/>
)
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 (
<Tag {...attributes} style={style}>
@@ -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]
}
/**

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
},
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}

View File

@@ -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
}
/**

View File

@@ -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,
},
})
/**
@@ -41,13 +41,8 @@ function CorePlugin() {
*/
const queries = Queries({
defer: true,
queries: {
isAtomic: () => false,
isVoid: () => false,
normalizeNode: () => {},
validateNode: () => {},
},
})
/**
@@ -180,7 +175,7 @@ function CorePlugin() {
* @type {Array}
*/
return [commands, queries, schema]
return [schema, ...plugins, commands, queries]
}
/**

View File

@@ -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
}

View File

@@ -136,12 +136,7 @@ function SchemaPlugin(schema) {
* @param {Function} next
*/
const queries = Queries({
queries: {
isAtomic,
isVoid,
},
})
const queries = Queries({ isAtomic, isVoid })
/**
* Return the plugins.