1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-17 20:51:20 +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 ```js
function renderEditor(props, next) { function renderEditor(props, next) {
const { children, editor } = props const { editor } = props
const wordCount = countWords(editor.value.text) const wordCount = countWords(editor.value.text)
const children = next()
return ( return (
<React.Fragment> <React.Fragment>
{props.children} {children}
<span className="word-count">{wordCount}</span> <span className="word-count">{wordCount}</span>
</React.Fragment> </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: 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 ```js
function normalizeNode(node) { function normalizeNode(node, next) {
const { nodes } = node const { nodes } = node
if (node.object !== 'block') return if (node.object !== 'block') return next()
if (nodes.size !== 3) return if (nodes.size !== 3) return next()
if (nodes.first().object !== 'text') return if (nodes.first().object !== 'text') return next()
if (nodes.last().object !== 'text') return if (nodes.last().object !== 'text') return next()
return change => change.removeNodeByKey(node.key) 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 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` ### `decorateNode`
@@ -107,7 +107,7 @@ This handler is called whenever the native DOM selection changes.
### `renderEditor` ### `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. 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} * @return {Element}
*/ */
renderNode = props => { renderNode = (props, next) => {
switch (props.node.type) { switch (props.node.type) {
case 'check-list-item': case 'check-list-item':
return <CheckListItem {...props} /> return <CheckListItem {...props} />
default:
return next()
} }
} }
@@ -152,15 +154,15 @@ class CheckLists extends React.Component {
* *
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @return {Value|Void} * @param {Function} next
*/ */
onKeyDown = (event, change) => { onKeyDown = (event, change, next) => {
const { value } = change const { value } = change
if (event.key == 'Enter' && value.startBlock.type == 'check-list-item') { if (event.key == 'Enter' && value.startBlock.type == 'check-list-item') {
change.splitBlock().setBlocks({ data: { checked: false } }) change.splitBlock().setBlocks({ data: { checked: false } })
return true return
} }
if ( if (
@@ -170,8 +172,10 @@ class CheckLists extends React.Component {
value.selection.startOffset == 0 value.selection.startOffset == 0
) { ) {
change.setBlocks('paragraph') change.setBlocks('paragraph')
return true return
} }
next()
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -223,40 +223,52 @@ class InputTester extends React.Component {
ref={this.ref} ref={this.ref}
value={this.state.value} value={this.state.value}
onChange={this.onChange} onChange={this.onChange}
renderNode={({ attributes, children, node }) => { renderNode={this.renderNode}
switch (node.type) { renderMark={this.renderMark}
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'bulleted-list':
return <ul {...attributes}>{children}</ul>
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
case 'heading-two':
return <h2 {...attributes}>{children}</h2>
case 'list-item':
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
}
}}
renderMark={({ attributes, children, mark }) => {
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>
}
}}
/> />
<EventsList /> <EventsList />
</Wrapper> </Wrapper>
) )
} }
renderNode = (props, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'bulleted-list':
return <ul {...attributes}>{children}</ul>
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
case 'heading-two':
return <h2 {...attributes}>{children}</h2>
case 'list-item':
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
default:
return next()
}
}
renderMark = (props, next) => {
const { attributes, children, mark } = 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>
default:
return next()
}
}
onRef = ref => { onRef = ref => {
this.el = ref this.el = ref
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ class Tables extends React.Component {
* @return {Element} * @return {Element}
*/ */
renderNode = props => { renderNode = (props, next) => {
const { attributes, children, node } = props const { attributes, children, node } = props
switch (node.type) { switch (node.type) {
@@ -64,6 +64,8 @@ class Tables extends React.Component {
return <tr {...attributes}>{children}</tr> return <tr {...attributes}>{children}</tr>
case 'table-cell': case 'table-cell':
return <td {...attributes}>{children}</td> return <td {...attributes}>{children}</td>
default:
return next()
} }
} }
@@ -74,12 +76,14 @@ class Tables extends React.Component {
* @return {Element} * @return {Element}
*/ */
renderMark = props => { renderMark = (props, next) => {
const { children, mark, attributes } = props const { children, mark, attributes } = props
switch (mark.type) { switch (mark.type) {
case 'bold': case 'bold':
return <strong {...attributes}>{children}</strong> return <strong {...attributes}>{children}</strong>
default:
return next()
} }
} }
@@ -90,12 +94,11 @@ class Tables extends React.Component {
* @param {Change} change * @param {Change} change
*/ */
onBackspace = (event, change) => { onBackspace = (event, change, next) => {
const { value } = change const { value } = change
const { selection } = value const { selection } = value
if (selection.start.offset != 0) return if (selection.start.offset != 0) return next()
event.preventDefault() event.preventDefault()
return true
} }
/** /**
@@ -115,12 +118,11 @@ class Tables extends React.Component {
* @param {Change} change * @param {Change} change
*/ */
onDelete = (event, change) => { onDelete = (event, change, next) => {
const { value } = change const { value } = change
const { selection } = value const { selection } = value
if (selection.end.offset != value.startText.text.length) return if (selection.end.offset != value.startText.text.length) return next()
event.preventDefault() event.preventDefault()
return true
} }
/** /**
@@ -130,23 +132,22 @@ class Tables extends React.Component {
* @param {Change} change * @param {Change} change
*/ */
onDropOrPaste = (event, change) => { onDropOrPaste = (event, change, next) => {
const transfer = getEventTransfer(event) const transfer = getEventTransfer(event)
const { value } = change const { value } = change
const { text = '' } = transfer const { text = '' } = transfer
if (value.startBlock.type !== 'table-cell') { if (value.startBlock.type !== 'table-cell') {
return return next()
} }
if (!text) { if (!text) {
return return next()
} }
const lines = text.split('\n') const lines = text.split('\n')
const { document } = Plain.deserialize(lines[0] || '') const { document } = Plain.deserialize(lines[0] || '')
change.insertFragment(document) change.insertFragment(document)
return false
} }
/** /**
@@ -156,9 +157,8 @@ class Tables extends React.Component {
* @param {Change} change * @param {Change} change
*/ */
onEnter = (event, change) => { onEnter = (event, change, next) => {
event.preventDefault() event.preventDefault()
return true
} }
/** /**
@@ -168,7 +168,7 @@ class Tables extends React.Component {
* @param {Change} change * @param {Change} change
*/ */
onKeyDown = (event, change) => { onKeyDown = (event, change, next) => {
const { value } = change const { value } = change
const { document, selection } = value const { document, selection } = value
const { start, isCollapsed } = selection const { start, isCollapsed } = selection
@@ -181,24 +181,25 @@ class Tables extends React.Component {
if (prevBlock.type === 'table-cell') { if (prevBlock.type === 'table-cell') {
if (['Backspace', 'Delete', 'Enter'].includes(event.key)) { if (['Backspace', 'Delete', 'Enter'].includes(event.key)) {
event.preventDefault() event.preventDefault()
return true
} else { } else {
return return next()
} }
} }
} }
if (value.startBlock.type !== 'table-cell') { if (value.startBlock.type !== 'table-cell') {
return return next()
} }
switch (event.key) { switch (event.key) {
case 'Backspace': case 'Backspace':
return this.onBackspace(event, change) return this.onBackspace(event, change, next)
case 'Delete': case 'Delete':
return this.onDelete(event, change) return this.onDelete(event, change, next)
case 'Enter': 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 { Editor as Controller } from 'slate'
import EVENT_HANDLERS from '../constants/event-handlers' import EVENT_HANDLERS from '../constants/event-handlers'
import BrowserPlugin from '../plugins/browser'
import PropsPlugin from '../plugins/props'
import ReactPlugin from '../plugins/react' import ReactPlugin from '../plugins/react'
/** /**
@@ -143,7 +141,7 @@ class Editor extends React.Component {
render() { render() {
debug('render', this) debug('render', this)
const props = { ...this.props } const props = { ...this.props, editor: this }
// Re-resolve the controller if needed based on memoized props. // Re-resolve the controller if needed based on memoized props.
const { commands, plugins, queries, schema } = props const { commands, plugins, queries, schema } = props
@@ -155,7 +153,7 @@ class Editor extends React.Component {
this.controller.setValue(value, options) this.controller.setValue(value, options)
// Render the editor's children with the controller. // 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 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.' '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 react = ReactPlugin(this.props)
const reactPlugin = ReactPlugin() const attrs = { onChange: this.onControllerChange, plugins: [react] }
const browserPlugin = BrowserPlugin()
const propsPlugin = PropsPlugin(props)
const allPlugins = [reactPlugin, browserPlugin, propsPlugin, ...plugins]
const attrs = { onChange: onControllerChange, plugins: allPlugins }
this.controller = new Controller(attrs, { editor: this, normalize: false }) this.controller = new Controller(attrs, { editor: this, normalize: false })
} }

View File

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

View File

@@ -2,14 +2,8 @@ import Base64 from 'slate-base64-serializer'
import Debug from 'debug' import Debug from 'debug'
import Hotkeys from 'slate-hotkeys' import Hotkeys from 'slate-hotkeys'
import Plain from 'slate-plain-serializer' import Plain from 'slate-plain-serializer'
import ReactDOM from 'react-dom'
import getWindow from 'get-window' import getWindow from 'get-window'
import { import { IS_IOS } from 'slate-dev-environment'
IS_FIREFOX,
IS_IE,
IS_IOS,
HAS_INPUT_EVENTS_LEVEL_2,
} from 'slate-dev-environment'
import cloneFragment from '../utils/clone-fragment' import cloneFragment from '../utils/clone-fragment'
import findDOMNode from '../utils/find-dom-node' import findDOMNode from '../utils/find-dom-node'
@@ -26,20 +20,16 @@ import setEventTransfer from '../utils/set-event-transfer'
* @type {Function} * @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} * @return {Object}
*/ */
function BrowserPlugin() { function AfterPlugin(options = {}) {
let activeElement = null
let compositionCount = 0
let isComposing = false
let isCopying = false
let isDragging = false
let isDraggingInternally = null let isDraggingInternally = null
/** /**
@@ -48,24 +38,11 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onBeforeInput(event, change, next) { function onBeforeInput(event, change, next) {
const { editor, value } = change const { editor, value } = change
const isSynthetic = !!event.nativeEvent 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 // If the event is synthetic, it's React's polyfill of `beforeinput` that
// isn't a true `beforeinput` event with meaningful information. It only // isn't a true `beforeinput` event with meaningful information. It only
@@ -73,13 +50,15 @@ function BrowserPlugin() {
if (isSynthetic) { if (isSynthetic) {
event.preventDefault() event.preventDefault()
change.insertText(event.data) change.insertText(event.data)
return return next()
} }
// Otherwise, we can use the information in the `beforeinput` event to // Otherwise, we can use the information in the `beforeinput` event to
// figure out the exact change that will occur, and prevent it. // figure out the exact change that will occur, and prevent it.
const [targetRange] = event.getTargetRanges() const [targetRange] = event.getTargetRanges()
if (!targetRange) return if (!targetRange) return next()
debug('onBeforeInput', { event })
event.preventDefault() event.preventDefault()
@@ -93,29 +72,29 @@ function BrowserPlugin() {
case 'deleteContentBackward': case 'deleteContentBackward':
case 'deleteContentForward': { case 'deleteContentForward': {
change.deleteAtRange(range) change.deleteAtRange(range)
return break
} }
case 'deleteWordBackward': { case 'deleteWordBackward': {
change.deleteWordBackwardAtRange(range) change.deleteWordBackwardAtRange(range)
return break
} }
case 'deleteWordForward': { case 'deleteWordForward': {
change.deleteWordForwardAtRange(range) change.deleteWordForwardAtRange(range)
return break
} }
case 'deleteSoftLineBackward': case 'deleteSoftLineBackward':
case 'deleteHardLineBackward': { case 'deleteHardLineBackward': {
change.deleteLineBackwardAtRange(range) change.deleteLineBackwardAtRange(range)
return break
} }
case 'deleteSoftLineForward': case 'deleteSoftLineForward':
case 'deleteHardLineForward': { case 'deleteHardLineForward': {
change.deleteLineForwardAtRange(range) change.deleteLineForwardAtRange(range)
return break
} }
case 'insertLineBreak': case 'insertLineBreak':
@@ -131,7 +110,7 @@ function BrowserPlugin() {
change.splitBlockAtRange(range) change.splitBlockAtRange(range)
} }
return break
} }
case 'insertFromYank': case 'insertFromYank':
@@ -146,7 +125,7 @@ function BrowserPlugin() {
? event.dataTransfer.getData('text/plain') ? event.dataTransfer.getData('text/plain')
: event.data : event.data
if (text == null) return if (text == null) break
change.insertTextAtRange(range, text, selection.marks) change.insertTextAtRange(range, text, selection.marks)
@@ -156,9 +135,11 @@ function BrowserPlugin() {
change.select({ marks: null }) change.select({ marks: null })
} }
return break
} }
} }
next()
} }
/** /**
@@ -167,88 +148,12 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onBlur(event, change, next) { 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 }) debug('onBlur', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
change.blur() change.blur()
return true next()
}
/**
* 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()
} }
/** /**
@@ -257,23 +162,18 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onClick(event, change, next) { function onClick(event, change, next) {
debug('onClick', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change const { editor } = change
if (editor.readOnly) return true if (editor.readOnly) return next()
const { value } = editor const { value } = editor
const { document } = value const { document } = value
const node = findNode(event.target, editor) const node = findNode(event.target, editor)
if (!node) return true if (!node) return next()
debug('onClick', { event })
const ancestors = document.getAncestors(node.key) const ancestors = document.getAncestors(node.key)
const isVoid = const isVoid =
@@ -287,35 +187,7 @@ function BrowserPlugin() {
change.focus().moveToEndOfNode(node) change.focus().moveToEndOfNode(node)
} }
return true next()
}
/**
* 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()
} }
/** /**
@@ -324,23 +196,13 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onCopy(event, change, next) { function onCopy(event, change, next) {
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => (isCopying = false))
debug('onCopy', { event }) debug('onCopy', { event })
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change const { editor } = change
cloneFragment(event, editor) cloneFragment(event, editor)
return true next()
} }
/** /**
@@ -349,22 +211,11 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onCut(event, change, next) { 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 }) debug('onCut', { event })
const { editor } = change
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
// Once the fake cut content has successfully been added to the clipboard, // Once the fake cut content has successfully been added to the clipboard,
// delete the content in the current selection. // delete the content in the current selection.
@@ -385,6 +236,8 @@ function BrowserPlugin() {
editor.change(c => c.delete()) editor.change(c => c.delete())
} }
}) })
next()
} }
/** /**
@@ -393,96 +246,12 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onDragEnd(event, change, next) { function onDragEnd(event, change, next) {
debug('onDragEnd', { event }) debug('onDragEnd', { event })
isDragging = false
isDraggingInternally = null isDraggingInternally = null
return next() 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()
} }
/** /**
@@ -491,19 +260,13 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onDragStart(event, change, next) { function onDragStart(event, change, next) {
debug('onDragStart', { event }) debug('onDragStart', { event })
isDragging = true
isDraggingInternally = true isDraggingInternally = true
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { editor } = change const { editor } = change
const { value } = editor const { value } = editor
const { document } = value const { document } = value
@@ -523,6 +286,7 @@ function BrowserPlugin() {
const fragment = change.value.fragment const fragment = change.value.fragment
const encoded = Base64.serializeNode(fragment) const encoded = Base64.serializeNode(fragment)
setEventTransfer(event, 'fragment', encoded) setEventTransfer(event, 'fragment', encoded)
next()
} }
/** /**
@@ -531,26 +295,16 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onDrop(event, change, next) { function onDrop(event, change, next) {
const { editor, value } = change 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 { document, selection } = value
const window = getWindow(event.target) const window = getWindow(event.target)
let target = getEventRange(event, editor) let target = getEventRange(event, editor)
if (!target) return true if (!target) return next()
debug('onDrop', { event })
const transfer = getEventTransfer(event) const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer const { type, fragment, text } = transfer
@@ -611,49 +365,18 @@ function BrowserPlugin() {
// DOM node, since that will make it go back to normal. // DOM node, since that will make it go back to normal.
const focusNode = document.getNode(target.focus.key) const focusNode = document.getNode(target.focus.key)
const el = findDOMNode(focusNode, window) const el = findDOMNode(focusNode, window)
if (!el) return true
el.dispatchEvent( if (el) {
new MouseEvent('mouseup', { el.dispatchEvent(
view: window, new MouseEvent('mouseup', {
bubbles: true, view: window,
cancelable: true, bubbles: true,
}) 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 }) next()
return next()
} }
/** /**
@@ -662,19 +385,9 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onInput(event, change, next) { 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 window = getWindow(event.target)
const { editor, value } = change const { editor, value } = change
@@ -682,7 +395,7 @@ function BrowserPlugin() {
const native = window.getSelection() const native = window.getSelection()
const { anchorNode } = native const { anchorNode } = native
const point = findPoint(anchorNode, 0, editor) const point = findPoint(anchorNode, 0, editor)
if (!point) return if (!point) return next()
// Get the text node and leaf in question. // Get the text node and leaf in question.
const { document, selection } = value const { document, selection } = value
@@ -716,7 +429,9 @@ function BrowserPlugin() {
} }
// If the text is no different, abort. // 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. // Determine what the selection should be after changing the text.
const delta = textContent.length - text.length const delta = textContent.length - text.length
@@ -729,6 +444,7 @@ function BrowserPlugin() {
// Change the current value to have the leaf's text replaced. // Change the current value to have the leaf's text replaced.
change.insertTextAtRange(entire, textContent, leaf.marks).select(corrected) change.insertTextAtRange(entire, textContent, leaf.marks).select(corrected)
next()
} }
/** /**
@@ -737,48 +453,12 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onKeyDown(event, change, next) { 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 }) debug('onKeyDown', { event })
// Certain hotkeys have native editing behaviors in `contenteditable` const { editor, value } = change
// 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 { document, selection } = value const { document, selection } = value
const hasVoidParent = document.hasVoidParent(selection.start.path, editor) 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 {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onPaste(event, change, next) { function onPaste(event, change, next) {
const { editor, value } = change
if (editor.readOnly) return true
debug('onPaste', { event }) debug('onPaste', { event })
// Prevent defaults so the DOM state isn't corrupted. const { value } = change
event.preventDefault()
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const transfer = getEventTransfer(event) const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer const { type, fragment, text } = transfer
@@ -926,9 +596,9 @@ function BrowserPlugin() {
} }
if (type == 'text' || type == 'html') { if (type == 'text' || type == 'html') {
if (!text) return true if (!text) return next()
const { document, selection, startBlock } = value const { document, selection, startBlock } = value
if (change.isVoid(startBlock)) return true if (change.isVoid(startBlock)) return next()
const defaultBlock = startBlock const defaultBlock = startBlock
const defaultMarks = document.getInsertMarksAtRange(selection) const defaultMarks = document.getInsertMarksAtRange(selection)
@@ -937,7 +607,7 @@ function BrowserPlugin() {
change.insertFragment(frag) change.insertFragment(frag)
} }
return true next()
} }
/** /**
@@ -946,38 +616,25 @@ function BrowserPlugin() {
* @param {Event} event * @param {Event} event
* @param {Change} change * @param {Change} change
* @param {Function} next * @param {Function} next
* @return {Boolean}
*/ */
function onSelect(event, change, next) { 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 }) debug('onSelect', { event })
// Save the new `activeElement`.
const window = getWindow(event.target) const window = getWindow(event.target)
activeElement = window.document.activeElement const { editor, value } = change
// Delegate to the plugins stack.
const ret = next()
if (ret !== undefined) return ret
const { document } = value const { document } = value
const native = window.getSelection() const native = window.getSelection()
// If there are no ranges, the editor was blurred natively. // If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) { if (!native.rangeCount) {
change.blur() change.blur()
return true return
} }
// Otherwise, determine the Slate selection from the native one. // Otherwise, determine the Slate selection from the native one.
let range = findRange(native, editor) let range = findRange(native, editor)
if (!range) return true if (!range) return
const { anchor, focus } = range const { anchor, focus } = range
const anchorText = document.getNode(anchor.key) const anchorText = document.getNode(anchor.key)
@@ -1036,7 +693,7 @@ function BrowserPlugin() {
selection = selection.set('marks', value.selection.marks) selection = selection.set('marks', value.selection.marks)
change.select(selection) change.select(selection)
return true next()
} }
/** /**
@@ -1049,18 +706,11 @@ function BrowserPlugin() {
onBeforeInput, onBeforeInput,
onBlur, onBlur,
onClick, onClick,
onCompositionEnd,
onCompositionStart,
onCopy, onCopy,
onCut, onCut,
onDragEnd, onDragEnd,
onDragEnter,
onDragExit,
onDragLeave,
onDragOver,
onDragStart, onDragStart,
onDrop, onDrop,
onFocus,
onInput, onInput,
onKeyDown, onKeyDown,
onPaste, onPaste,
@@ -1074,4 +724,4 @@ function BrowserPlugin() {
* @type {Object} * @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 React from 'react'
import { Text } from 'slate' import { Text } from 'slate'
import DOMPlugin from './dom'
import Content from '../components/content' 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. * A plugin that adds the React-specific rendering logic to the editor.
* *
* @param {Object} options
* @return {Object} * @return {Object}
*/ */
function ReactPlugin() { function ReactPlugin(options = {}) {
const { plugins = [] } = options
/** /**
* Render editor. * Render editor.
* *
* @param {Object} props * @param {Object} props
* @param {Editor} editor
* @param {Function} next * @param {Function} next
* @return {Object} * @return {Object}
*/ */
function renderEditor(props, editor, next) { function renderEditor(props, next) {
const children = ( const { editor } = props
return (
<Content <Content
onEvent={editor.event} onEvent={editor.event}
autoCorrect={props.autoCorrect} autoCorrect={props.autoCorrect}
@@ -34,27 +57,22 @@ function ReactPlugin() {
tagName={props.tagName} tagName={props.tagName}
/> />
) )
const ret = next({ ...props, children }, editor)
return ret !== undefined ? ret : children
} }
/** /**
* Render node. * Render node.
* *
* @param {Object} props * @param {Object} props
* @param {Editor} editor
* @param {Function} next * @param {Function} next
* @return {Element} * @return {Element}
*/ */
function renderNode(props, editor, next) { function renderNode(props, next) {
const ret = next()
if (ret !== undefined) return ret
const { attributes, children, node } = props const { attributes, children, node } = props
if (node.object != 'block' && node.object != 'inline') return null const { object } = node
const Tag = node.object == 'block' ? 'div' : 'span' if (object != 'block' && object != 'inline') return null
const Tag = object == 'block' ? 'div' : 'span'
const style = { position: 'relative' } const style = { position: 'relative' }
return ( return (
<Tag {...attributes} style={style}> <Tag {...attributes} style={style}>
@@ -67,16 +85,12 @@ function ReactPlugin() {
* Render placeholder. * Render placeholder.
* *
* @param {Object} props * @param {Object} props
* @param {Editor} editor
* @param {Function} next * @param {Function} next
* @return {Element} * @return {Element}
*/ */
function renderPlaceholder(props, editor, next) { function renderPlaceholder(props, next) {
const ret = next() const { editor, node } = props
if (ret !== undefined) return ret
const { node } = props
if (!editor.props.placeholder) return null if (!editor.props.placeholder) return null
if (editor.state.isComposing) return null if (editor.state.isComposing) return null
if (node.object != 'block') 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 { const editorPlugin = PROPS.reduce((memo, prop) => {
renderEditor, if (prop in options) memo[prop] = options[prop]
renderNode, return memo
renderPlaceholder, }, {})
}
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) { switch (props.node.type) {
case 'image': case 'image':
return Image(props) 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) { switch (props.node.type) {
case 'image': case 'image':
return Image(props) 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) { switch (props.node.type) {
case 'code': case 'code':
return Code(props) 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) { switch (props.node.type) {
case 'image': case 'image':
return Image(props) 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) { switch (props.node.type) {
case 'image': case 'image':
return Image(props) return Image(props)
default:
return next()
} }
} }

View File

@@ -12,10 +12,12 @@ function Code(props) {
} }
export const props = { export const props = {
renderNode(p) { renderNode(p, editor, next) {
switch (p.node.type) { switch (p.node.type) {
case 'code': case 'code':
return Code(p) return Code(p)
default:
return next()
} }
}, },
} }

View File

@@ -26,10 +26,12 @@ function Bold(props) {
return React.createElement('strong', { ...props.attributes }, props.children) return React.createElement('strong', { ...props.attributes }, props.children)
} }
function renderMark(props) { function renderMark(props, next) {
switch (props.mark.type) { switch (props.mark.type) {
case 'bold': case 'bold':
return Bold(props) 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) { switch (props.node.type) {
case 'link': case 'link':
return Link(props) return Link(props)
default:
return next()
} }
} }

View File

@@ -7,10 +7,12 @@ function Emoji(props) {
return React.createElement('img', props.attributes) return React.createElement('img', props.attributes)
} }
function renderNode(props) { function renderNode(props, next) {
switch (props.node.type) { switch (props.node.type) {
case 'emoji': case 'emoji':
return Emoji(props) 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) { switch (props.node.type) {
case 'link': case 'link':
return Link(props) return Link(props)
default:
return next()
} }
} }

View File

@@ -7,10 +7,12 @@ function Bold(props) {
return React.createElement('strong', { ...props.attributes }, props.children) return React.createElement('strong', { ...props.attributes }, props.children)
} }
function renderMark(props) { function renderMark(props, next) {
switch (props.mark.type) { switch (props.mark.type) {
case 'bold': case 'bold':
return Bold(props) 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) { switch (props.node.type) {
case 'image': case 'image':
return Image(props) return Image(props)
default:
return next()
} }
} }

View File

@@ -7,10 +7,12 @@ function Emoji(props) {
return React.createElement('img', props.attributes) return React.createElement('img', props.attributes)
} }
function renderNode(props) { function renderNode(props, next) {
switch (props.node.type) { switch (props.node.type) {
case 'emoji': case 'emoji':
return Emoji(props) return Emoji(props)
default:
return next()
} }
} }

View File

@@ -16,14 +16,6 @@ import Value from '../models/value'
const debug = Debug('slate:editor') const debug = Debug('slate:editor')
/**
* The core plugin.
*
* @type {Array|Object}
*/
const corePlugin = CorePlugin()
/** /**
* Editor. * Editor.
* *
@@ -59,8 +51,8 @@ class Editor {
isChanging: false, isChanging: false,
} }
registerPlugin(this, corePlugin) const core = CorePlugin({ plugins })
plugins.forEach(p => registerPlugin(this, p)) registerPlugin(this, core)
this.run('onConstruct', this) this.run('onConstruct', this)
@@ -304,12 +296,12 @@ function registerPlugin(editor, plugin) {
const { commands, queries, schema, ...rest } = plugin const { commands, queries, schema, ...rest } = plugin
if (commands) { if (commands) {
const commandsPlugin = CommandsPlugin({ commands }) const commandsPlugin = CommandsPlugin(commands)
registerPlugin(editor, commandsPlugin) registerPlugin(editor, commandsPlugin)
} }
if (queries) { if (queries) {
const queriesPlugin = QueriesPlugin({ queries }) const queriesPlugin = QueriesPlugin(queries)
registerPlugin(editor, queriesPlugin) registerPlugin(editor, queriesPlugin)
} }

View File

@@ -1,19 +1,11 @@
/** /**
* A plugin that adds a set of commands to the editor. * A plugin that adds a set of commands to the editor.
* *
* @param {Object} options * @param {Object} commands
* @return {Object} * @return {Object}
*/ */
function CommandsPlugin(options = {}) { function CommandsPlugin(commands = {}) {
const { commands, defer = false } = options
if (!commands) {
throw new Error(
'You must pass in the `commands` option to the Slate commands plugin.'
)
}
/** /**
* On command, if it exists in our list of commands, call it. * On command, if it exists in our list of commands, call it.
* *
@@ -26,14 +18,7 @@ function CommandsPlugin(options = {}) {
const { type, args } = command const { type, args } = command
const fn = commands[type] const fn = commands[type]
if (!fn) return next() if (!fn) return next()
if (defer) {
const ret = next()
if (ret !== undefined) return ret
}
change.call(fn, ...args) 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. * A plugin that defines the core Slate logic.
* *
* @param {Object} options
* @return {Object} * @return {Object}
*/ */
function CorePlugin() { function CorePlugin(options = {}) {
const { plugins = [] } = options
/** /**
* The core Slate commands. * The core Slate commands.
* *
@@ -23,15 +26,12 @@ function CorePlugin() {
*/ */
const commands = Commands({ const commands = Commands({
defer: true, ...AtCurrentRange,
commands: { ...AtRange,
...AtCurrentRange, ...ByPath,
...AtRange, ...OnHistory,
...ByPath, ...OnSelection,
...OnHistory, ...OnValue,
...OnSelection,
...OnValue,
},
}) })
/** /**
@@ -41,13 +41,8 @@ function CorePlugin() {
*/ */
const queries = Queries({ const queries = Queries({
defer: true, isAtomic: () => false,
queries: { isVoid: () => false,
isAtomic: () => false,
isVoid: () => false,
normalizeNode: () => {},
validateNode: () => {},
},
}) })
/** /**
@@ -180,7 +175,7 @@ function CorePlugin() {
* @type {Array} * @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. * A plugin that adds a set of queries to the editor.
* *
* @param {Object} options * @param {Object} queries
* @return {Object} * @return {Object}
*/ */
function QueriesPlugin(options = {}) { function QueriesPlugin(queries = {}) {
const { queries, defer = false } = options
if (!queries) {
throw new Error(
'You must pass in the `queries` option to the Slate queries plugin.'
)
}
/** /**
* On construct, register all the queries. * On construct, register all the queries.
* *
@@ -41,12 +33,6 @@ function QueriesPlugin(options = {}) {
const { type, args } = query const { type, args } = query
const fn = queries[type] const fn = queries[type]
if (!fn) return next() if (!fn) return next()
if (defer) {
const ret = next()
if (ret !== undefined) return ret
}
const ret = fn(editor, ...args) const ret = fn(editor, ...args)
return ret === undefined ? next() : ret return ret === undefined ? next() : ret
} }

View File

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