1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-23 15:32:59 +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
- [Editor](./reference/slate-react/editor.md)
- [Placeholder](./reference/slate-react/placeholder.md)
- [Plugins](./reference/slate-react/plugins.md)
- [Custom Nodes](./reference/slate-react/custom-nodes.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,
decorate: Function,
normalize: Function,
placeholder: Component || Function,
render: Component || Function || Object || String,
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.
### `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`
`Component` <br/>
`Function` <br/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -134,6 +134,7 @@ class Tables extends React.Component {
return (
<div className="editor">
<Editor
placeholder="Enter some text..."
schema={schema}
state={this.state.state}
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
this.state = {}
this.state.Component = node.getComponent(schema)
this.state.Placeholder = node.getPlaceholder(schema)
}
/**
@@ -78,7 +79,8 @@ class Node extends React.Component {
componentWillReceiveProps = (props) => {
if (props.node == this.props.node) return
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 })
const { editor, isSelected, node, parent, readOnly, state } = props
const { Component } = this.state
const { Component, Placeholder } = this.state
const { selection } = state
const indexes = node.getSelectionIndexes(selection, isSelected)
const children = node.nodes.toArray().map((child, i) => {
@@ -173,17 +175,19 @@ class Node extends React.Component {
if (direction == 'rtl') attributes.dir = 'rtl'
}
const p = {
editor,
isSelected,
key: node.key,
node,
parent,
readOnly,
state
}
const element = (
<Component
attributes={attributes}
editor={editor}
isSelected={isSelected}
key={node.key}
node={node}
parent={parent}
readOnly={readOnly}
state={state}
>
<Component {...p} attributes={attributes}>
{Placeholder && <Placeholder {...p} />}
{children}
</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 Placeholder from './components/placeholder'
import findDOMNode from './utils/find-dom-node'
import findDOMRange from './utils/find-dom-range'
import findNode from './utils/find-node'
@@ -17,7 +16,6 @@ import setEventTransfer from './utils/set-event-transfer'
export {
Editor,
Placeholder,
findDOMNode,
findDOMRange,
findNode,
@@ -29,7 +27,6 @@ export {
export default {
Editor,
Placeholder,
findDOMNode,
findDOMRange,
findNode,

View File

@@ -4,12 +4,13 @@ import Debug from 'debug'
import Plain from 'slate-plain-serializer'
import React from 'react'
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 HOTKEYS from '../constants/hotkeys'
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 findNode from '../utils/find-node'
import findPoint from '../utils/find-point'
@@ -31,19 +32,10 @@ const debug = Debug('slate:core:after')
* The after plugin.
*
* @param {Object} options
* @property {Element} placeholder
* @property {String} placeholderClassName
* @property {Object} placeholderStyle
* @return {Object}
*/
function AfterPlugin(options = {}) {
const {
placeholder,
placeholderClassName,
placeholderStyle,
} = options
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.
*
@@ -783,8 +726,14 @@ function AfterPlugin(options = {}) {
const schema = {
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
}
/**
* 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`.
*
@@ -2142,6 +2153,7 @@ memoize(Node.prototype, [
'getOffsetAtRange',
'getParent',
'getPath',
'getPlaceholder',
'getPreviousBlock',
'getPreviousSibling',
'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
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Component|Void}
* @return {Component|Null}
*/
__getComponent(object) {
const match = find(this.rules, rule => rule.render && rule.match(object))
if (!match) return
if (!match) return null
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`.
*