1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-17 13:38:37 +01:00

Remove renderPlaceholder (#2370)

* add placeholder plugin in slate-react

* remove renderPlaceholder logic

* extract placeholder into a plugin

* remove other old placeholder-based logic

* update changelogs
This commit is contained in:
Ian Storm Taylor 2018-11-02 17:04:20 -07:00 committed by GitHub
parent 0cf94c73c9
commit a09c3c2fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 394 additions and 225 deletions

View File

@ -2,7 +2,7 @@
One of the best parts of Slate is that it's built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible.
To that end, Slate gives you control over the rendering behavior of every node and mark in your document, any placeholders you want to render, and even the top-level editor itself.
To that end, Slate gives you control over the rendering behavior of every node and mark in your document, and even the top-level editor itself.
You can define these behaviors by passing `props` into the editor, or you can define them in Slate plugins.
@ -100,37 +100,6 @@ That way, if you happen to have a global stylesheet that defines `strong`, `em`,
> 🤖 Be aware though that marks aren't guaranteed to be "contiguous". Which means even though a **word** is bolded, it's not guaranteed to render as a single `<strong>` element. If some of its characters are also italic, it might be split up into multiple elements—one `<strong>wo</strong>` and one `<em><strong>rd</strong></em>`.
## Placeholders
By default Slate will render a placeholder for you which mimics the native DOM `placeholder` attribute of `<input>` and `<textarea>` elements—it's in the same typeface as the editor, and it's slightly translucent. And as soon as the document has any content, the placeholder disappears.
However sometimes you want to customize things. Or maybe you want to render placeholders inside specific blocks like inside an image caption. To do that, you can define your own `renderPlaceholder` function:
```js
function renderPlaceholder(props, editor, next) {
const { node } = props
if (node.object != 'block') return next()
if (node.type != 'caption') return next()
if (node.text != '') return next()
return (
<span
contentEditable={false}
style={{ display: 'inline-block', width: '0', whiteSpace: 'nowrap', opacity: '0.33' }}
>
{editor.props.placeholder}
</span>
)
}
<Editor
renderPlaceholder={renderPlaceholder}
...
/>
```
That will render a simple placeholder element inside all of your `caption` blocks until someone decides to write in a caption.
## The Editor Itself
Not only can you control the rendering behavior of the components inside the editor, but you can also control the rendering of the editor itself.

View File

@ -56,7 +56,7 @@ A change handler that will be called with the `change` that applied the change.
`String || Element`
A placeholder string (or React element) that will be rendered as the default block type's placeholder.
A placeholder string (or React element) that will be rendered if the document only contains a single empty block.
### `plugins`

View File

@ -24,7 +24,6 @@ In addition to the [core plugin hooks](../slate/plugins.md), when using `slate-r
renderEditor: Function,
renderMark: Function,
renderNode: Function,
renderPlaceholder: Function,
shouldNodeComponentUpdate: Function,
}
```
@ -153,21 +152,6 @@ Render a `Node` with `props`. The `props` object contains:
You must spread the `props.attributes` onto the top-level DOM node you use to render the node.
### `renderPlaceholder`
`Function renderPlaceholder(props: Object, editor: Editor, next: Function) => ReactNode|Void`
Render the placeholder that is shown when the editor has no `value`. The `props` object contains:
```js
{
editor: Editor,
readOnly: Boolean,
}
```
The `placeholder` prop that was passed to the editor can be found at `editor.props.placeholder`.
### `shouldNodeComponentUpdate`
`Function shouldNodeComponentUpdate(previousProps: Object, props: Object, editor: Editor, next: Function) => Boolean|Void`

View File

@ -0,0 +1,9 @@
# Changelog
This document maintains a list of changes to the `slate-react-placeholder` package with each new version. Until `1.0.0` is released, breaking changes will be added as minor version bumps, and smaller changes won't be accounted for since the library is moving quickly.
---
### `0.1.0` — November 2, 2017
:tada:

View File

@ -0,0 +1 @@
This package contains a set of React prop types for Slate values that you can use in your own components and plugins.

View File

@ -0,0 +1,38 @@
{
"name": "slate-react-placeholder",
"description": "A Slate plugin to render a placeholder with React.",
"version": "0.0.0",
"license": "MIT",
"repository": "git://github.com/ianstormtaylor/slate.git",
"main": "lib/slate-react-placeholder.js",
"module": "lib/slate-react-placeholder.es.js",
"umd": "dist/slate-react-placeholder.js",
"umdMin": "dist/slate-react-placeholder.min.js",
"files": [
"dist/",
"lib/"
],
"peerDependencies": {
"react": ">=0.14.0",
"slate": ">=0.32.0"
},
"devDependencies": {
"mocha": "^2.5.3",
"slate": "^0.43.4",
"tiny-invariant": "^1.0.1"
},
"scripts": {
"clean": "rm -rf ./dist ./lib ./node_modules"
},
"umdGlobals": {
"react": "React",
"slate": "Slate"
},
"keywords": [
"editor",
"placeholder",
"react",
"render",
"slate"
]
}

View File

@ -0,0 +1,100 @@
import invariant from 'tiny-invariant'
import React from 'react'
/**
* A plugin that renders a React placeholder for a given Slate node.
*
* @param {Object} options
* @return {Object}
*/
function SlateReactPlaceholder(options = {}) {
const { placeholder, when } = options
invariant(
placeholder,
'You must pass `SlateReactPlaceholder` an `options.placeholder` string.'
)
invariant(
when,
'You must pass `SlateReactPlaceholder` an `options.when` query.'
)
/**
* Decorate a match node with a placeholder mark when it fits the query.
*
* @param {Node} node
* @param {Editor} editor
* @param {Function} next
* @return {Array}
*/
function decorateNode(node, editor, next) {
if (!editor.query(when, node)) {
return next()
}
const others = next()
const first = node.getFirstText()
const last = node.getLastText()
const decoration = {
anchor: { key: first.key, offset: 0 },
focus: { key: last.key, offset: last.text.length },
mark: { type: 'placeholder' },
}
return [...others, decoration]
}
/**
* Render an inline placeholder for the placeholder mark.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
function renderMark(props, editor, next) {
const { children, mark } = props
if (mark.type === 'placeholder') {
const style = {
pointerEvents: 'none',
display: 'inline-block',
width: '0',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
}
return (
<React.Fragment>
<span contentEditable={false} style={style}>
{placeholder}
</span>
{children}
</React.Fragment>
)
}
return next()
}
/**
* Return the plugin.
*
* @return {Object}
*/
return { decorateNode, renderMark }
}
/**
* Export.
*
* @type {Function}
*/
export default SlateReactPlaceholder

View File

@ -4,6 +4,18 @@ This document maintains a list of changes to the `slate-react` package with each
---
### `0.21.0` — November 2, 2018
###### NEW
**Introducing the `slate-react-placeholder` package.** This new package is what handles the default `placeholder=` prop logic for the editor, and it can be used yourself for situations where you want to render browser-like placeholders in custom nodes.
###### BREAKING
**The `renderPlacehodler` middleware has been removed.** Previously this was how you rendered custom placeholders in the editor, but that logic can now be implemented with `decorateNode` instead, in a way that causes less confusion and overlap in the API. The new `slate-react-placeholder` package does exactly that, adding a decoration to the editor when it is empty.
---
### `0.20.0` — October 27, 2018
###### BREAKING

View File

@ -26,6 +26,7 @@
"slate-hotkeys": "^0.2.7",
"slate-plain-serializer": "^0.6.21",
"slate-prop-types": "^0.5.12",
"slate-react-placeholder": "^0.0.0",
"tiny-invariant": "^1.0.1",
"tiny-warning": "^0.0.3"
},

View File

@ -63,6 +63,7 @@ class Editor extends React.Component {
autoCorrect: true,
onChange: () => {},
options: {},
placeholder: '',
plugins: [],
readOnly: false,
schema: {},
@ -132,8 +133,8 @@ class Editor extends React.Component {
const props = { ...this.props, editor: this }
// Re-resolve the controller if needed based on memoized props.
const { commands, plugins, queries, schema } = props
this.resolveController(plugins, schema, commands, queries)
const { commands, placeholder, plugins, queries, schema } = props
this.resolveController(plugins, schema, commands, queries, placeholder)
// Set the current props on the controller.
const { options, readOnly, value } = props
@ -154,35 +155,38 @@ class Editor extends React.Component {
* @param {Object} schema
* @param {Object} commands
* @param {Object} queries
* @param {String} placeholder
* @return {Editor}
*/
resolveController = memoizeOne((plugins = [], schema, commands, queries) => {
// If we've resolved a few times already, and it's exactly in line with
// the updates, then warn the user that they may be doing something wrong.
warning(
this.tmp.resolves < 5 || this.tmp.resolves !== this.tmp.updates,
'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.'
)
resolveController = memoizeOne(
(plugins = [], schema, commands, queries, placeholder) => {
// If we've resolved a few times already, and it's exactly in line with
// the updates, then warn the user that they may be doing something wrong.
warning(
this.tmp.resolves < 5 || this.tmp.resolves !== this.tmp.updates,
'A Slate <Editor> component is re-resolving the `plugins`, `schema`, `commands`, `queries` or `placeholder` prop 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.'
)
this.tmp.resolves++
const react = ReactPlugin(this.props)
this.tmp.resolves++
const react = ReactPlugin(this.props)
const onChange = change => {
if (this.tmp.mounted) {
this.props.onChange(change)
} else {
this.tmp.change = change
const onChange = change => {
if (this.tmp.mounted) {
this.props.onChange(change)
} else {
this.tmp.change = change
}
}
this.controller = new Controller(
{ plugins: [react], onChange },
{ controller: this, construct: false }
)
this.controller.run('onConstruct')
}
this.controller = new Controller(
{ plugins: [react], onChange },
{ controller: this, construct: false }
)
this.controller.run('onConstruct')
})
)
/**
* Mimic the API of the `Editor` controller, so that this component instance

View File

@ -86,7 +86,11 @@ class Leaf extends React.Component {
index,
})
return <span data-offset-key={offsetKey}>{this.renderMarks()}</span>
return (
<span data-slate-leaf data-offset-key={offsetKey}>
{this.renderMarks()}
</span>
)
}
/**
@ -99,7 +103,7 @@ class Leaf extends React.Component {
const { marks, node, offset, text, editor } = this.props
const leaf = this.renderText()
const attributes = {
'data-slate-leaf': true,
'data-slate-mark': true,
}
return marks.reduce((children, mark) => {
@ -165,8 +169,8 @@ class Leaf extends React.Component {
const isLastLeaf = index === leaves.size - 1
if (isLastText && isLastLeaf && lastChar === '\n') return `${text}\n`
// Otherwise, just return the text.
return text
// Otherwise, just return the content.
return <span data-slate-content>{text}</span>
}
}

View File

@ -134,8 +134,7 @@ class Node extends React.Component {
const indexes = node.getSelectionIndexes(selection, isSelected)
const decs = decorations.concat(node.getDecorations(editor))
const childrenDecorations = getChildrenDecorations(node, decs)
let children = []
const children = []
node.nodes.forEach((child, i) => {
const isChildSelected = !!indexes && indexes.start <= i && i < indexes.end
@ -166,16 +165,6 @@ class Node extends React.Component {
readOnly,
}
let placeholder = editor.run('renderPlaceholder', props)
if (placeholder) {
placeholder = React.cloneElement(placeholder, {
key: `${node.key}-placeholder`,
})
children = [placeholder, ...children]
}
const element = editor.run('renderNode', {
...props,
attributes,

View File

@ -725,7 +725,7 @@ function AfterPlugin(options = {}) {
/**
* Export.
*
* @type {Object}
* @type {Function}
*/
export default AfterPlugin

View File

@ -116,14 +116,6 @@ function BeforePlugin() {
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 })
@ -155,14 +147,6 @@ function BeforePlugin() {
isComposing = true
compositionCount++
// 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 })
}
const { value } = editor
const { selection } = value
@ -502,7 +486,7 @@ function BeforePlugin() {
/**
* Export.
*
* @type {Object}
* @type {Function}
*/
export default BeforePlugin

View File

@ -18,7 +18,7 @@ function DOMPlugin(options = {}) {
/**
* Export.
*
* @type {Object}
* @type {Function}
*/
export default DOMPlugin

View File

@ -1,5 +1,5 @@
import PlaceholderPlugin from 'slate-react-placeholder'
import React from 'react'
import { Text } from 'slate'
import DOMPlugin from './dom'
import Content from '../components/content'
@ -19,7 +19,6 @@ const PROPS = [
'renderEditor',
'renderMark',
'renderNode',
'renderPlaceholder',
'schema',
]
@ -31,14 +30,28 @@ const PROPS = [
*/
function ReactPlugin(options = {}) {
const { plugins = [] } = options
const { placeholder, plugins = [] } = options
/**
* Decorate node.
*
* @param {Object} node
* @param {Editor} editor
* @param {Function} next
* @return {Array}
*/
function decorateNode(node, editor, next) {
return []
}
/**
* Render editor.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Object}
* @return {Element}
*/
function renderEditor(props, editor, next) {
@ -81,62 +94,49 @@ function ReactPlugin(options = {}) {
)
}
/**
* Render placeholder.
*
* @param {Object} props
* @param {Function} next
* @return {Element}
*/
function renderPlaceholder(props, editor, next) {
const { node } = props
if (!editor.props.placeholder) return null
if (editor.state.isComposing) return null
if (node.object != 'block') return null
if (!Text.isTextList(node.nodes)) return null
if (node.text != '') return null
if (editor.value.document.getBlocks().size > 1) return null
const style = {
pointerEvents: 'none',
display: 'inline-block',
width: '0',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
}
return (
<span contentEditable={false} style={style}>
{editor.props.placeholder}
</span>
)
}
/**
* Return the plugins.
*
* @type {Array}
*/
const ret = []
const editorPlugin = PROPS.reduce((memo, prop) => {
if (prop in options) memo[prop] = options[prop]
return memo
}, {})
const domPlugin = DOMPlugin({
plugins: [editorPlugin, ...plugins],
ret.push(
DOMPlugin({
plugins: [editorPlugin, ...plugins],
})
)
if (placeholder) {
ret.push(
PlaceholderPlugin({
placeholder,
when: (editor, node) =>
node.object === 'document' &&
node.text === '' &&
node.nodes.size === 1,
})
)
}
ret.push({
decorateNode,
renderEditor,
renderNode,
})
const defaultsPlugin = { renderEditor, renderNode, renderPlaceholder }
return [domPlugin, defaultsPlugin]
return ret
}
/**
* Export.
*
* @type {Object}
* @type {Function}
*/
export default ReactPlugin

View File

@ -11,24 +11,25 @@ import findDOMNode from './find-dom-node'
function findDOMPoint(point, win = window) {
const el = findDOMNode(point.key, win)
let start = 0
let n
// COMPAT: In IE, this method's arguments are not optional, so we have to
// pass in all four even though the last two are defaults. (2017/10/25)
const iterator = win.document.createNodeIterator(
el,
NodeFilter.SHOW_TEXT,
() => NodeFilter.FILTER_ACCEPT,
false
// For each leaf, we need to isolate its content, which means filtering to its
// direct text and zero-width spans. (We have to filter out any other siblings
// that may have been rendered alongside them.)
const texts = Array.from(
el.querySelectorAll('[data-slate-content], [data-slate-zero-width]')
)
while ((n = iterator.nextNode())) {
const { length } = n.textContent
for (const text of texts) {
const node = text.childNodes[0]
// Account zero-width spaces which shouldn't really take up space, because
// we only render them when the text is empty.
const length = node.textContent === '\uFEFF' ? 0 : node.textContent.length
const end = start + length
if (point.offset <= end) {
const o = point.offset - start
return { node: n, offset: o >= 0 ? o : 0 }
const offset = Math.max(0, point.offset - start)
return { node, offset }
}
start = end

View File

@ -62,7 +62,7 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>
@ -70,7 +70,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
@ -81,7 +81,7 @@ export const output = `
</div>
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>
@ -89,7 +89,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -58,7 +58,7 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>
@ -66,7 +66,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
@ -77,7 +77,7 @@ export const output = `
</div>
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>
@ -85,7 +85,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -39,21 +39,27 @@ export const output = `
<pre>
<code>
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</code>
</pre>
<pre>
<code>
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</code>
</pre>
<pre>
<code>
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</code>
</pre>

View File

@ -58,7 +58,7 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>
@ -66,7 +66,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
@ -77,7 +77,7 @@ export const output = `
</div>
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>
@ -85,7 +85,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -45,7 +45,7 @@ export const output = `
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -35,7 +35,9 @@ export const output = `
<pre>
<code>
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</code>
</pre>

View File

@ -52,9 +52,17 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>o</span>
<span><strong data-slate-leaf="true">n</strong></span>
<span>e</span>
<span data-slate-leaf="true">
<span data-slate-content="true">o</span>
</span>
<span data-slate-leaf="true">
<strong data-slate-mark="true">
<span data-slate-content="true">n</span>
</strong>
</span>
<span data-slate-leaf="true">
<span data-slate-content="true">e</span>
</span>
</span>
</div>
</div>

View File

@ -44,37 +44,43 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
<a href="https://google.com">
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</a>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
<a href="https://google.com">
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</a>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
<a href="https://google.com">
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</a>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -45,14 +45,14 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
<span data-slate-void="true" contenteditable="false">
<span data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
@ -62,7 +62,7 @@ export const output = `
</span>
</span>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>

View File

@ -40,17 +40,19 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
<a href="https://google.com">
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</a>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -34,9 +34,17 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>one</span>
<span><strong data-slate-leaf="true">two</strong></span>
<span>three</span>
<span data-slate-leaf="true">
<span data-slate-content="true">one</span>
</span>
<span data-slate-leaf="true">
<strong data-slate-mark="true">
<span data-slate-content="true">two</span>
</strong>
</span>
<span data-slate-leaf="true">
<span data-slate-content="true">three</span>
</span>
</span>
</div>
</div>

View File

@ -20,17 +20,19 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
<span style="position:relative">
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</span>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>

View File

@ -16,7 +16,9 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>word</span>
<span data-slate-leaf="true">
<span data-slate-content="true">word</span>
</span>
</span>
</div>
</div>

View File

@ -18,7 +18,7 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>

View File

@ -20,12 +20,16 @@ export const output = `
<div style="position:relative">
<div dir="rtl" style="position:relative">
<span>
<span>مرحبا بالعالم</span>
<span data-slate-leaf="true">
<span data-slate-content="true">مرحبا بالعالم</span>
</span>
</span>
</div>
<div dir="rtl" style="position:relative">
<span>
<span>שלום עולם</span>
<span data-slate-leaf="true">
<span data-slate-content="true">שלום עולם</span>
</span>
</span>
</div>
</div>

View File

@ -44,7 +44,7 @@ export const output = `
<div data-slate-editor="true">
<div style="position:relative">
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z">&#xFEFF;</span>
</span>
</span>
@ -54,7 +54,7 @@ export const output = `
</span>
</span>
<span>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n">&#xFEFF;<br /></span>
</span>
</span>

View File

@ -18,17 +18,23 @@ export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<span>
<span>Hello, world!</span>
<span data-slate-leaf="true">
<span data-slate-content="true">Hello, world!</span>
</span>
</span>
</div>
<div dir="rtl" style="position:relative">
<span>
<span>مرحبا بالعالم</span>
<span data-slate-leaf="true">
<span data-slate-content="true">مرحبا بالعالم</span>
</span>
</span>
</div>
<div dir="rtl" style="position:relative">
<span>
<span>שלום עולם</span>
<span data-slate-leaf="true">
<span data-slate-content="true">שלום עולם</span>
</span>
</span>
</div>
</div>

View File

@ -448,7 +448,7 @@ class ElementInterface {
'As of Slate 0.42.0, the `node.getDecorations` method takes an `editor` instead of a `value`.'
)
const array = editor.run('decorateNode', this) || []
const array = editor.run('decorateNode', this)
const decorations = Decoration.createList(array)
return decorations
}

View File

@ -108,6 +108,10 @@ class Text extends Record(DEFAULTS) {
throw new Error('leaves must be either Array or Immutable.List')
}
if (leaves.size === 0) {
leaves = leaves.push(Leaf.create())
}
const node = new Text({
leaves: Leaf.createLeaves(leaves),
key,
@ -216,16 +220,33 @@ class Text extends Record(DEFAULTS) {
/**
* Derive the leaves for a list of `decorations`.
*
* @param {Array|Void} decorations (optional)
* @param {List} decorations (optional)
* @return {List<Leaf>}
*/
getLeaves(decorations = []) {
getLeaves(decorations) {
let { leaves } = this
if (leaves.size === 0) return List.of(Leaf.create({}))
if (!decorations || decorations.length === 0) return leaves
if (this.text.length === 0) return leaves
const { key } = this
// PERF: We can exit early without decorations.
if (!decorations || decorations.size === 0) return leaves
// HACK: We shouldn't need this, because text nodes should never be in a
// position of not having any leaves...
if (leaves.size === 0) {
const marks = decorations.map(d => d.mark)
const leaf = Leaf.create({ marks })
return List([leaf])
}
// HACK: this shouldn't be necessary, because the loop below should handle
// the `0` case without failures. It may already even, not sure.
if (this.text.length === 0) {
const marks = decorations.map(d => d.mark)
const leaf = Leaf.create({ marks })
return List([leaf])
}
const { key, text } = this
decorations.forEach(dec => {
const { start, end, mark } = dec
@ -234,12 +255,12 @@ class Text extends Record(DEFAULTS) {
if (hasStart && hasEnd) {
const index = hasStart ? start.offset : 0
const length = hasEnd ? end.offset - index : this.text.length - index
const length = hasEnd ? end.offset - index : text.length - index
if (length < 1) return
if (index >= this.text.length) return
if (index >= text.length) return
if (index !== 0 || length < this.text.length) {
if (index !== 0 || length < text.length) {
const [before, bundle] = Leaf.splitLeaves(leaves, index)
const [middle, after] = Leaf.splitLeaves(bundle, length)
leaves = before.concat(middle.map(x => x.addMark(mark)), after)
@ -614,19 +635,23 @@ class Text extends Record(DEFAULTS) {
*/
setLeaves(leaves) {
const result = Leaf.createLeaves(leaves)
leaves = Leaf.createLeaves(leaves)
if (result.size === 1) {
const first = result.first()
if (leaves.size === 1) {
const first = leaves.first()
if (!first.marks || first.marks.size === 0) {
if (first.text === '') {
return this.set('leaves', List())
return this.set('leaves', List([Leaf.create()]))
}
}
}
return this.set('leaves', Leaf.createLeaves(leaves))
if (leaves.size === 0) {
leaves = leaves.push(Leaf.create())
}
return this.set('leaves', leaves)
}
}

View File

@ -8,6 +8,7 @@ import slateHyperscript from '../../packages/slate-hyperscript/package.json'
import slatePlainSerializer from '../../packages/slate-plain-serializer/package.json'
import slatePropTypes from '../../packages/slate-prop-types/package.json'
import slateReact from '../../packages/slate-react/package.json'
import slateReactPlaceholder from '../../packages/slate-react-placeholder/package.json'
const configurations = [
...factory(slate),
@ -19,6 +20,7 @@ const configurations = [
...factory(slatePlainSerializer),
...factory(slatePropTypes),
...factory(slateReact),
...factory(slateReactPlaceholder),
]
export default configurations