mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-23 15:32:59 +02:00
add isSelected prop, cleanup sCU, add custom component reference (#1084)
* add isSelected prop, cleanup sCU, add custom component reference, fixes #1080 * fix custom node reference * update custom node reference * remove sCU check for text-only children
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
## Component Reference
|
||||
|
||||
- [Custom](./reference/components/custom.md)
|
||||
- [Editor](./reference/components/editor.md)
|
||||
- [Placeholder](./reference/components/placeholder.md)
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
This is the full reference documentation for all of the pieces of Slate, broken up into sections by type:
|
||||
|
||||
- **Components**
|
||||
- [Custom](./components/custom.md)
|
||||
- [Editor](./components/editor.md)
|
||||
- [Placeholder](./components/placeholder.md)
|
||||
- **Models**
|
||||
|
99
docs/reference/components/custom.md
Normal file
99
docs/reference/components/custom.md
Normal file
@@ -0,0 +1,99 @@
|
||||
|
||||
# `<{Custom}>`
|
||||
|
||||
Slate will render custom nodes for `Block` and `Inline` models, based on what you pass in as your schema. This allows you to completely customize the rendering behavior of your Slate editor.
|
||||
|
||||
- [Properties](#properties)
|
||||
- [`attributes`](#attributes)
|
||||
- [`children`](#children)
|
||||
- [`editor`](#editor)
|
||||
- [`isSelected`](#isselected)
|
||||
- [`node`](#node)
|
||||
- [`parent`](#parent)
|
||||
- [`readOnly`](#readonly)
|
||||
- [`state`](#state)
|
||||
|
||||
## Properties
|
||||
|
||||
```js
|
||||
<{Custom}
|
||||
attributes={Object}
|
||||
children={Object}
|
||||
editor={Editor}
|
||||
isSelected={Boolean}
|
||||
node={Node}
|
||||
parent={Node}
|
||||
readOnly={Boolean}
|
||||
state={State}
|
||||
/>
|
||||
```
|
||||
|
||||
### `attributes`
|
||||
`Object`
|
||||
|
||||
A dictionary of DOM attributes that you must attach to the main DOM element of the node you render. For example:
|
||||
|
||||
```js
|
||||
return (
|
||||
<p {...props.attributes}>{props.children}</p>
|
||||
)
|
||||
```
|
||||
```js
|
||||
return (
|
||||
<figure {...props.attributes}>
|
||||
<img src={...} />
|
||||
</figure>
|
||||
)
|
||||
```
|
||||
|
||||
### `children`
|
||||
`Object`
|
||||
|
||||
A set of React children elements that are composed of internal Slate components that handle all of the editing logic of the editor for you. You must render these as the children of your non-void nodes. For example:
|
||||
|
||||
```js
|
||||
return (
|
||||
<p {...props.attributes}>
|
||||
{props.children}
|
||||
</p>
|
||||
)
|
||||
```
|
||||
|
||||
### `editor`
|
||||
`Editor`
|
||||
|
||||
A reference to the Slate [`<Editor>`](./editor.md) instance. This allows you to retrieve the current `state` of the editor, or perform a `change` on the state. For example:
|
||||
|
||||
```js
|
||||
const state = editor.getState()
|
||||
```
|
||||
```js
|
||||
editor.change((change) => {
|
||||
change.selectAll().delete()
|
||||
})
|
||||
```
|
||||
|
||||
### `isSelected`
|
||||
`Boolean`
|
||||
|
||||
A boolean representing whether the node you are rendering is currently selected. You can use this to render a visual representation of the selection.
|
||||
|
||||
### `node`
|
||||
`Node`
|
||||
|
||||
A reference to the [`Node`](../models/node.md) being rendered.
|
||||
|
||||
### `parent`
|
||||
`Node`
|
||||
|
||||
A reference to the parent of the current [`Node`](../models/node.md) being rendered.
|
||||
|
||||
### `readOnly`
|
||||
`Boolean`
|
||||
|
||||
Whether the editor is in "read-only" mode, where all of the rendering is the same, but the user is prevented from editing the editor's content.
|
||||
|
||||
### `state`
|
||||
`State`
|
||||
|
||||
A reference to the current [`State`](../models/state.md) of the editor.
|
@@ -9,18 +9,6 @@ import React from 'react'
|
||||
|
||||
class Video extends React.Component {
|
||||
|
||||
/**
|
||||
* Check if the node is selected.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
isSelected = () => {
|
||||
const { node, state } = this.props
|
||||
const isSelected = state.isFocused && state.blocks.includes(node)
|
||||
return isSelected
|
||||
}
|
||||
|
||||
/**
|
||||
* When the input text changes, update the `video` data on the node.
|
||||
*
|
||||
@@ -66,8 +54,8 @@ class Video extends React.Component {
|
||||
*/
|
||||
|
||||
renderVideo = () => {
|
||||
const video = this.props.node.data.get('video')
|
||||
const isSelected = this.isSelected()
|
||||
const { node, isSelected } = this.props
|
||||
const video = node.data.get('video')
|
||||
|
||||
const wrapperStyle = {
|
||||
position: 'relative',
|
||||
@@ -120,7 +108,8 @@ class Video extends React.Component {
|
||||
*/
|
||||
|
||||
renderInput = () => {
|
||||
const video = this.props.node.data.get('video')
|
||||
const { node } = this.props
|
||||
const video = node.data.get('video')
|
||||
return (
|
||||
<input
|
||||
value={video}
|
||||
|
@@ -25,10 +25,9 @@ const schema = {
|
||||
nodes: {
|
||||
paragraph: props => <p>{props.children}</p>,
|
||||
emoji: (props) => {
|
||||
const { state, node } = props
|
||||
const { isSelected, node } = props
|
||||
const { data } = node
|
||||
const code = data.get('code')
|
||||
const isSelected = state.selection.hasFocusIn(node)
|
||||
return <span className={`emoji ${isSelected ? 'selected' : ''}`} {...props.attributes} contentEditable={false}>{code}</span>
|
||||
}
|
||||
}
|
||||
|
@@ -27,10 +27,9 @@ const defaultBlock = {
|
||||
const schema = {
|
||||
nodes: {
|
||||
image: (props) => {
|
||||
const { node, state } = props
|
||||
const active = state.isFocused && state.blocks.includes(node)
|
||||
const { node, isSelected } = props
|
||||
const src = node.data.get('src')
|
||||
const className = active ? 'active' : null
|
||||
const className = isSelected ? 'active' : null
|
||||
return (
|
||||
<img src={src} className={className} {...props.attributes} />
|
||||
)
|
||||
|
@@ -917,25 +917,41 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a `node`.
|
||||
* Render a `child` node of the document.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {Node} child
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderNode = (node) => {
|
||||
renderNode = (child) => {
|
||||
const { editor, readOnly, schema, state } = this.props
|
||||
const { document, selection } = state
|
||||
const { startKey, endKey, isBlurred } = selection
|
||||
let isSelected
|
||||
|
||||
if (isBlurred) {
|
||||
isSelected = false
|
||||
}
|
||||
|
||||
else {
|
||||
isSelected = document.nodes
|
||||
.skipUntil(n => n.kind == 'text' ? n.key == startKey : n.hasDescendant(startKey))
|
||||
.reverse()
|
||||
.skipUntil(n => n.kind == 'text' ? n.key == endKey : n.hasDescendant(endKey))
|
||||
.includes(child)
|
||||
}
|
||||
|
||||
return (
|
||||
<Node
|
||||
key={node.key}
|
||||
block={null}
|
||||
node={node}
|
||||
parent={state.document}
|
||||
editor={editor}
|
||||
isSelected={isSelected}
|
||||
key={child.key}
|
||||
node={child}
|
||||
parent={document}
|
||||
readOnly={readOnly}
|
||||
schema={schema}
|
||||
state={state}
|
||||
editor={editor}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ class Node extends React.Component {
|
||||
static propTypes = {
|
||||
block: SlateTypes.block,
|
||||
editor: Types.object.isRequired,
|
||||
isSelected: Types.bool.isRequired,
|
||||
node: SlateTypes.node.isRequired,
|
||||
parent: SlateTypes.node.isRequired,
|
||||
readOnly: Types.bool.isRequired,
|
||||
@@ -97,6 +98,8 @@ class Node extends React.Component {
|
||||
shouldComponentUpdate = (nextProps) => {
|
||||
const { props } = this
|
||||
const { Component } = this.state
|
||||
const n = nextProps
|
||||
const p = props
|
||||
|
||||
// If the `Component` has enabled suppression of update checking, always
|
||||
// return true so that it can deal with update checking itself.
|
||||
@@ -104,49 +107,34 @@ class Node extends React.Component {
|
||||
|
||||
// If the `readOnly` status has changed, re-render in case there is any
|
||||
// user-land logic that depends on it, like nested editable contents.
|
||||
if (nextProps.readOnly != props.readOnly) return true
|
||||
if (n.readOnly != p.readOnly) return true
|
||||
|
||||
// If the node has changed, update. PERF: There are cases where it will have
|
||||
// changed, but it's properties will be exactly the same (eg. copy-paste)
|
||||
// which this won't catch. But that's rare and not a drag on performance, so
|
||||
// for simplicity we just let them through.
|
||||
if (nextProps.node != props.node) return true
|
||||
if (n.node != p.node) return true
|
||||
|
||||
// If the Node has children that aren't just Text's then allow them to decide
|
||||
// If they should update it or not.
|
||||
if (nextProps.node.kind != 'text' && Text.isTextList(nextProps.node.nodes) == false) return true
|
||||
|
||||
// If the node is a block or inline, which can have custom renderers, we
|
||||
// include an extra check to re-render if the node either becomes part of,
|
||||
// or leaves, a selection. This is to make it simple for users to show a
|
||||
// node's "selected" state.
|
||||
if (nextProps.node.kind != 'text') {
|
||||
const nodes = `${props.node.kind}s`
|
||||
const isInSelection = props.state[nodes].includes(props.node)
|
||||
const nextIsInSelection = nextProps.state[nodes].includes(nextProps.node)
|
||||
const hasFocus = props.state.isFocused
|
||||
const nextHasFocus = nextProps.state.isFocused
|
||||
const selectionChanged = isInSelection != nextIsInSelection
|
||||
const focusChanged = hasFocus != nextHasFocus
|
||||
if (selectionChanged || focusChanged) return true
|
||||
}
|
||||
// If the node's selection state has changed, re-render in case there is any
|
||||
// user-land logic depends on it to render.
|
||||
if (n.isSelected != p.isSelected) return true
|
||||
|
||||
// If the node is a text node, re-render if the current decorations have
|
||||
// changed, even if the content of the text node itself hasn't.
|
||||
if (nextProps.node.kind == 'text' && nextProps.schema.hasDecorators) {
|
||||
const nextDecorators = nextProps.state.document.getDescendantDecorators(nextProps.node.key, nextProps.schema)
|
||||
const decorators = props.state.document.getDescendantDecorators(props.node.key, props.schema)
|
||||
const nextRanges = nextProps.node.getRanges(nextDecorators)
|
||||
const ranges = props.node.getRanges(decorators)
|
||||
if (!nextRanges.equals(ranges)) return true
|
||||
if (n.node.kind == 'text' && n.schema.hasDecorators) {
|
||||
const nDecorators = n.state.document.getDescendantDecorators(n.node.key, n.schema)
|
||||
const pDecorators = p.state.document.getDescendantDecorators(p.node.key, p.schema)
|
||||
const nRanges = n.node.getRanges(nDecorators)
|
||||
const pRanges = p.node.getRanges(pDecorators)
|
||||
if (!nRanges.equals(pRanges)) return true
|
||||
}
|
||||
|
||||
// If the node is a text node, and its parent is a block node, and it was
|
||||
// the last child of the block, re-render to cleanup extra `<br/>` or `\n`.
|
||||
if (nextProps.node.kind == 'text' && nextProps.parent.kind == 'block') {
|
||||
const last = props.parent.nodes.last()
|
||||
const nextLast = nextProps.parent.nodes.last()
|
||||
if (props.node == last && nextProps.node != nextLast) return true
|
||||
if (n.node.kind == 'text' && n.parent.kind == 'block') {
|
||||
const pLast = p.parent.nodes.last()
|
||||
const nLast = n.parent.nodes.last()
|
||||
if (p.node == pLast && n.node != nLast) return true
|
||||
}
|
||||
|
||||
// Otherwise, don't update.
|
||||
@@ -261,14 +249,35 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
renderNode = (child) => {
|
||||
const { block, editor, node, readOnly, schema, state } = this.props
|
||||
const { block, editor, isSelected, node, readOnly, schema, state } = this.props
|
||||
const { selection } = state
|
||||
const { startKey, endKey } = selection
|
||||
let isChildSelected
|
||||
|
||||
if (!isSelected) {
|
||||
isChildSelected = false
|
||||
}
|
||||
|
||||
else if (node.kind == 'text') {
|
||||
isChildSelected = node.key == startKey || node.key == endKey
|
||||
}
|
||||
|
||||
else {
|
||||
isChildSelected = node.nodes
|
||||
.skipUntil(n => n.kind == 'text' ? n.key == startKey : n.hasDescendant(startKey))
|
||||
.reverse()
|
||||
.skipUntil(n => n.kind == 'text' ? n.key == endKey : n.hasDescendant(endKey))
|
||||
.includes(child)
|
||||
}
|
||||
|
||||
return (
|
||||
<Node
|
||||
block={node.kind == 'block' ? node : block}
|
||||
editor={editor}
|
||||
isSelected={isChildSelected}
|
||||
key={child.key}
|
||||
node={child}
|
||||
block={node.kind == 'block' ? node : block}
|
||||
parent={node}
|
||||
editor={editor}
|
||||
readOnly={readOnly}
|
||||
schema={schema}
|
||||
state={state}
|
||||
@@ -283,7 +292,7 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
renderElement = () => {
|
||||
const { editor, node, parent, readOnly, state } = this.props
|
||||
const { editor, isSelected, node, parent, readOnly, state } = this.props
|
||||
const { Component } = this.state
|
||||
const children = node.nodes.map(this.renderNode).toArray()
|
||||
|
||||
@@ -304,10 +313,11 @@ class Node extends React.Component {
|
||||
const element = (
|
||||
<Component
|
||||
attributes={attributes}
|
||||
key={node.key}
|
||||
editor={editor}
|
||||
parent={parent}
|
||||
isSelected={isSelected}
|
||||
key={node.key}
|
||||
node={node}
|
||||
parent={parent}
|
||||
readOnly={readOnly}
|
||||
state={state}
|
||||
>
|
||||
|
@@ -6,7 +6,7 @@ import Inline from './inline'
|
||||
import Text from './text'
|
||||
import direction from 'direction'
|
||||
import generateKey from '../utils/generate-key'
|
||||
import isInRange from '../utils/is-in-range'
|
||||
import isIndexInRange from '../utils/is-index-in-range'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import logger from '../utils/logger'
|
||||
import memoize from '../utils/memoize'
|
||||
@@ -460,7 +460,7 @@ class Node {
|
||||
.getTextsAtRange(range)
|
||||
.reduce((arr, text) => {
|
||||
const chars = text.characters
|
||||
.filter((char, i) => isInRange(i, text, range))
|
||||
.filter((char, i) => isIndexInRange(i, text, range))
|
||||
.toArray()
|
||||
|
||||
return arr.concat(chars)
|
||||
@@ -1560,6 +1560,49 @@ class Node {
|
||||
return this.set('nodes', nodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the node is in a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
isInRange(range) {
|
||||
range = range.normalize(this)
|
||||
|
||||
const node = this
|
||||
const { startKey, endKey, isCollapsed } = range
|
||||
|
||||
// PERF: solve the most common cast where the start or end key are inside
|
||||
// the node, for collapsed selections.
|
||||
if (
|
||||
node.key == startKey ||
|
||||
node.key == endKey ||
|
||||
node.hasDescendant(startKey) ||
|
||||
node.hasDescendant(endKey)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// PERF: if the selection is collapsed and the previous check didn't return
|
||||
// true, then it must be false.
|
||||
if (isCollapsed) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise, look through all of the leaf text nodes in the range, to see
|
||||
// if any of them are inside the node.
|
||||
const texts = node.getTextsAtRange(range)
|
||||
let memo = false
|
||||
|
||||
texts.forEach((text) => {
|
||||
if (node.hasDescendant(text.key)) memo = true
|
||||
return memo
|
||||
})
|
||||
|
||||
return memo
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the node is a leaf block.
|
||||
*
|
||||
|
@@ -8,7 +8,7 @@
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
function isInRange(index, text, range) {
|
||||
function isIndexInRange(index, text, range) {
|
||||
const { startKey, startOffset, endKey, endOffset } = range
|
||||
|
||||
if (text.key == startKey && text.key == endKey) {
|
||||
@@ -28,4 +28,4 @@ function isInRange(index, text, range) {
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default isInRange
|
||||
export default isIndexInRange
|
Reference in New Issue
Block a user