1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-23 07:22:55 +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:
Ian Storm Taylor
2017-09-07 14:33:34 -07:00
committed by GitHub
parent d8004c33b1
commit 16d29db8cb
10 changed files with 225 additions and 68 deletions

View File

@@ -15,6 +15,7 @@
## Component Reference
- [Custom](./reference/components/custom.md)
- [Editor](./reference/components/editor.md)
- [Placeholder](./reference/components/placeholder.md)

View File

@@ -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**

View 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.

View File

@@ -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}

View File

@@ -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>
}
}

View File

@@ -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} />
)

View File

@@ -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}
/>
)
}

View File

@@ -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}
>

View File

@@ -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.
*

View File

@@ -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