1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-18 21:21:21 +02:00

refactor placeholder to use schema (#1253)

* refactor placeholder to use schema

* update placeholder, remove old export

* add maxWidth to prevent overflow

* update docs
This commit is contained in:
Ian Storm Taylor
2017-10-18 00:23:39 -07:00
committed by GitHub
parent 117d8c55cc
commit f42a64ac8f
31 changed files with 209 additions and 275 deletions

View File

@@ -48,7 +48,6 @@
## Slate React ## Slate React
- [Editor](./reference/slate-react/editor.md) - [Editor](./reference/slate-react/editor.md)
- [Placeholder](./reference/slate-react/placeholder.md)
- [Plugins](./reference/slate-react/plugins.md) - [Plugins](./reference/slate-react/plugins.md)
- [Custom Nodes](./reference/slate-react/custom-nodes.md) - [Custom Nodes](./reference/slate-react/custom-nodes.md)
- [Core Plugins](./reference/slate-react/core-plugins.md) - [Core Plugins](./reference/slate-react/core-plugins.md)

View File

@@ -1,58 +0,0 @@
# `<Placeholder>`
```js
import { Placeholder } from 'slate-react'
```
A simple component that adds a placeholder to a node. It encapsulates all of the Slate-related logic that determines when to render the placeholder, so you don't have to think about it.
## Properties
```js
<Placeholder
className={String}
node={Node}
parent={Node}
state={State}
style={Object}
>
{children}
</Placeholder>
```
### `children`
`Any`
React child elements to render inside the placeholder `<span>` element.
### `className`
`String`
An optional class name string to add to the placeholder `<span>` element.
### `firstOnly`
`Boolean`
An optional toggle that allows the Placeholder to render even if it is not the first node of the parent. This is useful for cases where the Placeholder should show up at every empty instance of the node. Defaults to `true`.
### `node`
`Node`
The node to render the placeholder element on top of. The placeholder is positioned absolutely, covering the entire node.
### `parent`
`Node`
The node to check for non-empty content, to determine whether the placeholder should be shown or not, if `firstOnly` is set to `false`.
### `state`
`State`
The current state of the editor.
### `style`
`Object`
An optional dictionary of styles to pass to the placeholder `<span>` element.

View File

@@ -84,6 +84,7 @@ Internally, the `marks` and `nodes` properties of a schema are simply converted
match: Function, match: Function,
decorate: Function, decorate: Function,
normalize: Function, normalize: Function,
placeholder: Component || Function,
render: Component || Function || Object || String, render: Component || Function || Object || String,
validate: Function validate: Function
} }
@@ -140,6 +141,18 @@ The `decorate` property allows you define a function that will apply extra marks
The `normalize` property is a function to run that recovers the editor's state after the `validate` property of a rule has determined that an object is invalid. It is passed a [`Change`](./change.md) that it can use to make modifications. It is also passed the return value of the `validate` function, which makes it easy to quickly determine the failure reason from the validation. The `normalize` property is a function to run that recovers the editor's state after the `validate` property of a rule has determined that an object is invalid. It is passed a [`Change`](./change.md) that it can use to make modifications. It is also passed the return value of the `validate` function, which makes it easy to quickly determine the failure reason from the validation.
### `placeholder`
`Component` <br/>
`Function`
```js
{
placeholder: (props) => <span>{props.editor.props.placeholder}</span>
}
```
The `placeholder` property determines which React component Slate will use to render a placeholder for the editor.
### `render` ### `render`
`Component` <br/> `Component` <br/>
`Function` <br/> `Function` <br/>

View File

@@ -145,7 +145,7 @@ class CheckLists extends React.Component {
<div className="editor"> <div className="editor">
<Editor <Editor
spellCheck spellCheck
placeholder={'Enter some text...'} placeholder="Get to work..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -190,6 +190,7 @@ class CodeHighlighting extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Write some code..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View File

@@ -56,6 +56,7 @@ class Embeds extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Enter some text..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -129,6 +129,7 @@ class Emojis extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Write some 😍👋🎉..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -101,6 +101,7 @@ class ForcedLayout extends React.Component {
render() { render() {
return ( return (
<Editor <Editor
placeholder="Enter a title..."
state={this.state.state} state={this.state.state}
schema={this.state.schema} schema={this.state.schema}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -165,6 +165,7 @@ class HoveringMenu extends React.Component {
/> />
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Enter some text..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -105,7 +105,7 @@ class HugeDocument extends React.Component {
render() { render() {
return ( return (
<Editor <Editor
placeholder={'Enter some text...'} placeholder="Enter some text..."
schema={schema} schema={schema}
spellCheck={false} spellCheck={false}
state={this.state.state} state={this.state.state}

View File

@@ -152,6 +152,7 @@ class Images extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Enter some text..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -187,6 +187,7 @@ class Links extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Enter some text..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -155,6 +155,7 @@ class MarkdownPreview extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Write some markdown..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -77,6 +77,7 @@ class MarkdownShortcuts extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Write some markdown..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -196,6 +196,7 @@ class PasteHtml extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Paste in some HTML..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onPaste={this.onPaste} onPaste={this.onPaste}

View File

@@ -41,7 +41,7 @@ class PlainText extends React.Component {
render() { render() {
return ( return (
<Editor <Editor
placeholder={'Enter some plain text...'} placeholder="Enter some plain text..."
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}
/> />

View File

@@ -83,7 +83,7 @@ The third is an example of using the plugin.render property to create a higher-o
render() { render() {
return ( return (
<Editor <Editor
placeholder={'Enter some text...'} placeholder="Enter some text..."
plugins={plugins} plugins={plugins}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -42,7 +42,7 @@ class ReadOnly extends React.Component {
return ( return (
<Editor <Editor
readOnly readOnly
placeholder={'Enter some text...'} placeholder="Enter some text..."
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}
/> />

View File

@@ -295,12 +295,12 @@ class RichTextExample extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Enter some rich text..."
schema={schema}
spellCheck
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
schema={schema}
placeholder={'Enter some rich text...'}
spellCheck
/> />
</div> </div>
) )

View File

@@ -69,7 +69,7 @@ class PlainText extends React.Component {
render() { render() {
return ( return (
<Editor <Editor
placeholder={'Enter some plain text...'} placeholder="Enter some plain text..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}

View File

@@ -133,7 +133,7 @@ class SearchHighlighting extends React.Component {
state={this.state.state} state={this.state.state}
onChange={this.onChange} onChange={this.onChange}
schema={schema} schema={schema}
placeholder={'Enter some rich text...'} placeholder="Enter some rich text..."
spellCheck spellCheck
/> />
</div> </div>

View File

@@ -209,7 +209,7 @@ class SyncingEditor extends React.Component {
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
schema={schema} schema={schema}
placeholder={'Enter some rich text...'} placeholder="Enter some text..."
spellCheck spellCheck
/> />
</div> </div>

View File

@@ -134,6 +134,7 @@ class Tables extends React.Component {
return ( return (
<div className="editor"> <div className="editor">
<Editor <Editor
placeholder="Enter some text..."
schema={schema} schema={schema}
state={this.state.state} state={this.state.state}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View File

@@ -0,0 +1,51 @@
import React from 'react'
import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
/**
* Default node.
*
* @type {Component}
*/
class DefaultNode extends React.Component {
/**
* Prop types.
*
* @type {Object}
*/
static propTypes = {
attributes: Types.object.isRequired,
editor: Types.object.isRequired,
isSelected: Types.bool.isRequired,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node.isRequired,
readOnly: Types.bool.isRequired,
state: SlateTypes.state.isRequired,
}
/**
* Render.
*
* @return {Element}
*/
render() {
const { attributes, children, node } = this.props
const Tag = node.kind == 'block' ? 'div' : 'span'
const style = { position: 'relative' }
return <Tag {...attributes} style={style}>{children}</Tag>
}
}
/**
* Export.
*
* @type {Component}
*/
export default DefaultNode

View File

@@ -0,0 +1,64 @@
import React from 'react'
import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
/**
* Default placeholder.
*
* @type {Component}
*/
class DefaultPlaceholder extends React.Component {
/**
* Property types.
*
* @type {Object}
*/
static propTypes = {
editor: Types.object.isRequired,
isSelected: Types.bool.isRequired,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node.isRequired,
readOnly: Types.bool.isRequired,
state: SlateTypes.state.isRequired,
}
/**
* Render.
*
* @return {Element}
*/
render() {
const { editor, state } = this.props
if (state.document.nodes.size > 1) return null
if (!editor.props.placeholder) 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>
)
}
}
/**
* Export.
*
* @type {Component}
*/
export default DefaultPlaceholder

View File

@@ -54,6 +54,7 @@ class Node extends React.Component {
const { node, schema } = props const { node, schema } = props
this.state = {} this.state = {}
this.state.Component = node.getComponent(schema) this.state.Component = node.getComponent(schema)
this.state.Placeholder = node.getPlaceholder(schema)
} }
/** /**
@@ -78,7 +79,8 @@ class Node extends React.Component {
componentWillReceiveProps = (props) => { componentWillReceiveProps = (props) => {
if (props.node == this.props.node) return if (props.node == this.props.node) return
const Component = props.node.getComponent(props.schema) const Component = props.node.getComponent(props.schema)
this.setState({ Component }) const Placeholder = props.node.getPlaceholder(props.schema)
this.setState({ Component, Placeholder })
} }
/** /**
@@ -154,7 +156,7 @@ class Node extends React.Component {
this.debug('render', { props }) this.debug('render', { props })
const { editor, isSelected, node, parent, readOnly, state } = props const { editor, isSelected, node, parent, readOnly, state } = props
const { Component } = this.state const { Component, Placeholder } = this.state
const { selection } = state const { selection } = state
const indexes = node.getSelectionIndexes(selection, isSelected) const indexes = node.getSelectionIndexes(selection, isSelected)
const children = node.nodes.toArray().map((child, i) => { const children = node.nodes.toArray().map((child, i) => {
@@ -173,17 +175,19 @@ class Node extends React.Component {
if (direction == 'rtl') attributes.dir = 'rtl' if (direction == 'rtl') attributes.dir = 'rtl'
} }
const p = {
editor,
isSelected,
key: node.key,
node,
parent,
readOnly,
state
}
const element = ( const element = (
<Component <Component {...p} attributes={attributes}>
attributes={attributes} {Placeholder && <Placeholder {...p} />}
editor={editor}
isSelected={isSelected}
key={node.key}
node={node}
parent={parent}
readOnly={readOnly}
state={state}
>
{children} {children}
</Component> </Component>
) )

View File

@@ -1,125 +0,0 @@
import React from 'react'
import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
/**
* Placeholder.
*
* @type {Component}
*/
class Placeholder extends React.Component {
/**
* Property types.
*
* @type {Object}
*/
static propTypes = {
children: Types.any.isRequired,
className: Types.string,
firstOnly: Types.bool,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node,
state: SlateTypes.state.isRequired,
style: Types.object,
}
/**
* Default properties.
*
* @type {Object}
*/
static defaultProps = {
firstOnly: true,
}
/**
* Should the placeholder update?
*
* @param {Object} props
* @param {Object} state
* @return {Boolean}
*/
shouldComponentUpdate = (props, state) => {
return (
props.children != this.props.children ||
props.className != this.props.className ||
props.firstOnly != this.props.firstOnly ||
props.parent != this.props.parent ||
props.node != this.props.node ||
props.style != this.props.style
)
}
/**
* Is the placeholder visible?
*
* @return {Boolean}
*/
isVisible = () => {
const { firstOnly, node, parent } = this.props
if (node.text) return false
if (firstOnly) {
if (parent.nodes.size > 1) return false
if (parent.nodes.first() === node) return true
return false
} else {
return true
}
}
/**
* Render.
*
* If the placeholder is a string, and no `className` or `style` has been
* passed, give it a default style of lowered opacity.
*
* @return {Element}
*/
render() {
const isVisible = this.isVisible()
if (!isVisible) return null
const { children, className } = this.props
let { style } = this.props
if (typeof children === 'string' && style == null && className == null) {
style = { opacity: '0.333' }
} else if (style == null) {
style = {}
}
const styles = {
position: 'absolute',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px',
pointerEvents: 'none',
...style
}
return (
<span contentEditable={false} className={className} style={styles}>
{children}
</span>
)
}
}
/**
* Export.
*
* @type {Component}
*/
export default Placeholder

View File

@@ -1,6 +1,5 @@
import Editor from './components/editor' import Editor from './components/editor'
import Placeholder from './components/placeholder'
import findDOMNode from './utils/find-dom-node' import findDOMNode from './utils/find-dom-node'
import findDOMRange from './utils/find-dom-range' import findDOMRange from './utils/find-dom-range'
import findNode from './utils/find-node' import findNode from './utils/find-node'
@@ -17,7 +16,6 @@ import setEventTransfer from './utils/set-event-transfer'
export { export {
Editor, Editor,
Placeholder,
findDOMNode, findDOMNode,
findDOMRange, findDOMRange,
findNode, findNode,
@@ -29,7 +27,6 @@ export {
export default { export default {
Editor, Editor,
Placeholder,
findDOMNode, findDOMNode,
findDOMRange, findDOMRange,
findNode, findNode,

View File

@@ -4,12 +4,13 @@ import Debug from 'debug'
import Plain from 'slate-plain-serializer' import Plain from 'slate-plain-serializer'
import React from 'react' import React from 'react'
import getWindow from 'get-window' import getWindow from 'get-window'
import { Block, Inline, coreSchema } from 'slate' import { Block, Inline, Text, coreSchema } from 'slate'
import EVENT_HANDLERS from '../constants/event-handlers' import EVENT_HANDLERS from '../constants/event-handlers'
import HOTKEYS from '../constants/hotkeys' import HOTKEYS from '../constants/hotkeys'
import Content from '../components/content' import Content from '../components/content'
import Placeholder from '../components/placeholder' import DefaultNode from '../components/default-node'
import DefaultPlaceholder from '../components/default-placeholder'
import findDOMNode from '../utils/find-dom-node' import findDOMNode from '../utils/find-dom-node'
import findNode from '../utils/find-node' import findNode from '../utils/find-node'
import findPoint from '../utils/find-point' import findPoint from '../utils/find-point'
@@ -31,19 +32,10 @@ const debug = Debug('slate:core:after')
* The after plugin. * The after plugin.
* *
* @param {Object} options * @param {Object} options
* @property {Element} placeholder
* @property {String} placeholderClassName
* @property {Object} placeholderStyle
* @return {Object} * @return {Object}
*/ */
function AfterPlugin(options = {}) { function AfterPlugin(options = {}) {
const {
placeholder,
placeholderClassName,
placeholderStyle,
} = options
let isDraggingInternally = null let isDraggingInternally = null
/** /**
@@ -726,55 +718,6 @@ function AfterPlugin(options = {}) {
) )
} }
/**
* A default schema rule to render block nodes.
*
* @type {Object}
*/
const BLOCK_RENDER_RULE = {
match: (node) => {
return node.kind == 'block'
},
render: (props) => {
return (
<div {...props.attributes} style={{ position: 'relative' }}>
{props.children}
{placeholder
? <Placeholder
className={placeholderClassName}
node={props.node}
parent={props.state.document}
state={props.state}
style={placeholderStyle}
>
{placeholder}
</Placeholder>
: null}
</div>
)
}
}
/**
* A default schema rule to render inline nodes.
*
* @type {Object}
*/
const INLINE_RENDER_RULE = {
match: (node) => {
return node.kind == 'inline'
},
render: (props) => {
return (
<span {...props.attributes} style={{ position: 'relative' }}>
{props.children}
</span>
)
}
}
/** /**
* Add default rendering rules to the schema. * Add default rendering rules to the schema.
* *
@@ -783,8 +726,14 @@ function AfterPlugin(options = {}) {
const schema = { const schema = {
rules: [ rules: [
BLOCK_RENDER_RULE, {
INLINE_RENDER_RULE match: obj => obj.kind == 'block' || obj.kind == 'inline',
render: DefaultNode,
},
{
match: obj => obj.kind == 'block' && Text.isTextList(obj.nodes) && obj.text == '',
placeholder: DefaultPlaceholder,
},
] ]
} }

View File

@@ -1328,6 +1328,17 @@ class Node {
return path return path
} }
/**
* Get the placeholder for the node from a `schema`.
*
* @param {Schema} schema
* @return {Component|Void}
*/
getPlaceholder(schema) {
return schema.__getPlaceholder(this)
}
/** /**
* Get the block node before a descendant text node by `key`. * Get the block node before a descendant text node by `key`.
* *
@@ -2142,6 +2153,7 @@ memoize(Node.prototype, [
'getOffsetAtRange', 'getOffsetAtRange',
'getParent', 'getParent',
'getPath', 'getPath',
'getPlaceholder',
'getPreviousBlock', 'getPreviousBlock',
'getPreviousSibling', 'getPreviousSibling',
'getPreviousText', 'getPreviousText',

View File

@@ -110,22 +110,39 @@ class Schema extends Record(DEFAULTS) {
} }
/** /**
* Return the renderer for an `object`. * Return the component for an `object`.
* *
* This method is private, because it should always be called on one of the * This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for * often-changing immutable objects instead, since it will be memoized for
* much better performance. * much better performance.
* *
* @param {Mixed} object * @param {Mixed} object
* @return {Component|Void} * @return {Component|Null}
*/ */
__getComponent(object) { __getComponent(object) {
const match = find(this.rules, rule => rule.render && rule.match(object)) const match = find(this.rules, rule => rule.render && rule.match(object))
if (!match) return if (!match) return null
return match.render return match.render
} }
/**
* Return the placeholder for an `object`.
*
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Component|Null}
*/
__getPlaceholder(object) {
const match = find(this.rules, rule => rule.placeholder && rule.match(object))
if (!match) return null
return match.placeholder
}
/** /**
* Return the decorations for an `object`. * Return the decorations for an `object`.
* *