mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-17 20:51:20 +02:00
Speed up decorations rendering (#1801)
* order, assign decoration ranges to node children * move decorations ordering method to utils * Add immutable dep. Prettify * Try to improve perfs * Fix orderChildDecorations * Compute correct order from the start for range starts * Add tests for order-child-decorations * Optimize text decoration rendering * Rewrite with simpler API. Apply it to Content as well * Lint * Fix tests
This commit is contained in:
committed by
Ian Storm Taylor
parent
8a2368f851
commit
0cfd54fc19
@@ -30,11 +30,13 @@
|
||||
"slate-prop-types": "^0.4.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"immutable": "^3.8.1",
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0",
|
||||
"react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0",
|
||||
"slate": ">=0.32.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"immutable": "^3.8.1",
|
||||
"mocha": "^2.5.3",
|
||||
"slate": "^0.33.5",
|
||||
"slate-hyperscript": "^0.5.11",
|
||||
|
@@ -15,6 +15,7 @@ import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import Node from './node'
|
||||
import findDOMRange from '../utils/find-dom-range'
|
||||
import findRange from '../utils/find-range'
|
||||
import getChildrenDecorations from '../utils/get-children-decorations'
|
||||
import scrollToSelection from '../utils/scroll-to-selection'
|
||||
import removeAllRanges from '../utils/remove-all-ranges'
|
||||
|
||||
@@ -453,13 +454,17 @@ class Content extends React.Component {
|
||||
tagName,
|
||||
spellCheck,
|
||||
} = props
|
||||
const { value } = editor
|
||||
const { value, stack } = editor
|
||||
const Container = tagName
|
||||
const { document, selection } = value
|
||||
const { document, selection, decorations } = value
|
||||
const indexes = document.getSelectionIndexes(selection, selection.isFocused)
|
||||
const decs = document.getDecorations(stack).concat(decorations || [])
|
||||
const childrenDecorations = getChildrenDecorations(document, decs)
|
||||
|
||||
const children = document.nodes.toArray().map((child, i) => {
|
||||
const isSelected = !!indexes && indexes.start <= i && i < indexes.end
|
||||
return this.renderNode(child, isSelected)
|
||||
|
||||
return this.renderNode(child, isSelected, childrenDecorations[i])
|
||||
})
|
||||
|
||||
const handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
@@ -532,18 +537,16 @@ class Content extends React.Component {
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderNode = (child, isSelected) => {
|
||||
renderNode = (child, isSelected, decorations) => {
|
||||
const { editor, readOnly } = this.props
|
||||
const { value } = editor
|
||||
const { document, decorations } = value
|
||||
const { stack } = editor
|
||||
let decs = document.getDecorations(stack)
|
||||
if (decorations) decs = decorations.concat(decs)
|
||||
const { document } = value
|
||||
|
||||
return (
|
||||
<Node
|
||||
block={null}
|
||||
editor={editor}
|
||||
decorations={decs}
|
||||
decorations={decorations}
|
||||
isSelected={isSelected}
|
||||
key={child.key}
|
||||
node={child}
|
||||
|
@@ -7,6 +7,7 @@ import Types from 'prop-types'
|
||||
|
||||
import Void from './void'
|
||||
import Text from './text'
|
||||
import getChildrenDecorations from '../utils/get-children-decorations'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
@@ -118,18 +119,31 @@ class Node extends React.Component {
|
||||
|
||||
render() {
|
||||
this.debug('render', this)
|
||||
|
||||
const { editor, isSelected, node, parent, readOnly } = this.props
|
||||
const {
|
||||
editor,
|
||||
isSelected,
|
||||
node,
|
||||
decorations,
|
||||
parent,
|
||||
readOnly,
|
||||
} = this.props
|
||||
const { value } = editor
|
||||
const { selection } = value
|
||||
const { stack } = editor
|
||||
const indexes = node.getSelectionIndexes(selection, isSelected)
|
||||
let children = node.nodes.toArray().map((child, i) => {
|
||||
const decs = decorations.concat(node.getDecorations(stack))
|
||||
const childrenDecorations = getChildrenDecorations(node, decs)
|
||||
|
||||
let children = []
|
||||
node.nodes.forEach((child, i) => {
|
||||
const isChildSelected = !!indexes && indexes.start <= i && i < indexes.end
|
||||
return this.renderNode(child, isChildSelected)
|
||||
|
||||
children.push(
|
||||
this.renderNode(child, isChildSelected, childrenDecorations[i])
|
||||
)
|
||||
})
|
||||
|
||||
// Attributes that the developer must to mix into the element in their
|
||||
// Attributes that the developer must mix into the element in their
|
||||
// custom node renderer component.
|
||||
const attributes = { 'data-key': node.key }
|
||||
|
||||
@@ -172,18 +186,18 @@ class Node extends React.Component {
|
||||
*
|
||||
* @param {Node} child
|
||||
* @param {Boolean} isSelected
|
||||
* @param {Array<Decoration>} decorations
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderNode = (child, isSelected) => {
|
||||
const { block, decorations, editor, node, readOnly } = this.props
|
||||
const { stack } = editor
|
||||
renderNode = (child, isSelected, decorations) => {
|
||||
const { block, editor, node, readOnly } = this.props
|
||||
const Component = child.object == 'text' ? Text : Node
|
||||
const decs = decorations.concat(node.getDecorations(stack))
|
||||
|
||||
return (
|
||||
<Component
|
||||
block={node.object == 'block' ? node : block}
|
||||
decorations={decs}
|
||||
decorations={decorations}
|
||||
editor={editor}
|
||||
isSelected={isSelected}
|
||||
key={child.key}
|
||||
|
@@ -110,12 +110,15 @@ class Text extends React.Component {
|
||||
const decs = decorations.filter(d => {
|
||||
const { startKey, endKey } = d
|
||||
if (startKey == key || endKey == key) return true
|
||||
if (startKey === endKey) return false
|
||||
const startsBefore = document.areDescendantsSorted(startKey, key)
|
||||
if (!startsBefore) return false
|
||||
const endsAfter = document.areDescendantsSorted(key, endKey)
|
||||
return startsBefore && endsAfter
|
||||
return endsAfter
|
||||
})
|
||||
|
||||
const leaves = node.getLeaves(decs)
|
||||
// PERF: Take advantage of cache by avoiding arguments
|
||||
const leaves = decs.size === 0 ? node.getLeaves() : node.getLeaves(decs)
|
||||
let offset = 0
|
||||
|
||||
const children = leaves.map((leaf, i) => {
|
||||
|
129
packages/slate-react/src/utils/get-children-decorations.js
Normal file
129
packages/slate-react/src/utils/get-children-decorations.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Set } from 'immutable'
|
||||
|
||||
/**
|
||||
* Split the decorations in lists of relevant decorations for each child.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {List} decorations
|
||||
* @return {Array<List<Decoration>>}
|
||||
*/
|
||||
|
||||
function getChildrenDecorations(node, decorations) {
|
||||
const activeDecorations = Set().asMutable()
|
||||
const childrenDecorations = []
|
||||
|
||||
orderChildDecorations(node, decorations).forEach(item => {
|
||||
if (item.isRangeStart) {
|
||||
// Item is a decoration start
|
||||
activeDecorations.add(item.decoration)
|
||||
} else if (item.isRangeEnd) {
|
||||
// item is a decoration end
|
||||
activeDecorations.remove(item.decoration)
|
||||
} else {
|
||||
// Item is a child node
|
||||
childrenDecorations.push(activeDecorations.toList())
|
||||
}
|
||||
})
|
||||
|
||||
return childrenDecorations
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders the children of provided node and its decoration endpoints (start, end)
|
||||
* so that decorations can be passed only to relevant children (see use in Node.render())
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {List} decorations
|
||||
* @return {Array<Item>}
|
||||
*
|
||||
* where type Item =
|
||||
* {
|
||||
* child: Node,
|
||||
* // Index of the child in its parent
|
||||
* index: number
|
||||
* }
|
||||
* or {
|
||||
* // True if this represents the start of the given decoration
|
||||
* isRangeStart: boolean,
|
||||
* // True if this represents the end of the given decoration
|
||||
* isRangeEnd: boolean,
|
||||
* decoration: Range
|
||||
* }
|
||||
*/
|
||||
|
||||
function orderChildDecorations(node, decorations) {
|
||||
if (decorations.isEmpty()) {
|
||||
return node.nodes.toArray().map((child, index) => ({
|
||||
child,
|
||||
index,
|
||||
}))
|
||||
}
|
||||
|
||||
// Map each key to its global order
|
||||
const keyOrders = { [node.key]: 0 }
|
||||
let globalOrder = 1
|
||||
node.forEachDescendant(child => {
|
||||
keyOrders[child.key] = globalOrder
|
||||
globalOrder = globalOrder + 1
|
||||
})
|
||||
|
||||
const childNodes = node.nodes.toArray()
|
||||
|
||||
const endPoints = childNodes.map((child, index) => ({
|
||||
child,
|
||||
index,
|
||||
order: keyOrders[child.key],
|
||||
}))
|
||||
|
||||
decorations.forEach(decoration => {
|
||||
// Range start.
|
||||
// A rangeStart should be before the child containing its startKey, in order
|
||||
// to consider it active before going down the child.
|
||||
const startKeyOrder = keyOrders[decoration.startKey]
|
||||
const containingChildOrder =
|
||||
startKeyOrder === undefined
|
||||
? 0
|
||||
: getContainingChildOrder(childNodes, keyOrders, startKeyOrder)
|
||||
endPoints.push({
|
||||
isRangeStart: true,
|
||||
order: containingChildOrder - 0.5,
|
||||
decoration,
|
||||
})
|
||||
|
||||
// Range end.
|
||||
const endKeyOrder = (keyOrders[decoration.endKey] || globalOrder) + 0.5
|
||||
endPoints.push({
|
||||
isRangeEnd: true,
|
||||
order: endKeyOrder,
|
||||
decoration,
|
||||
})
|
||||
})
|
||||
|
||||
return endPoints.sort((a, b) => (a.order > b.order ? 1 : -1))
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the key order of the child right before the given order.
|
||||
*/
|
||||
|
||||
function getContainingChildOrder(children, keyOrders, order) {
|
||||
// Find the first child that is after the given key
|
||||
const nextChildIndex = children.findIndex(
|
||||
child => order < keyOrders[child.key]
|
||||
)
|
||||
|
||||
if (nextChildIndex <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const containingChild = children[nextChildIndex - 1]
|
||||
return keyOrders[containingChild.key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default getChildrenDecorations
|
@@ -11,6 +11,7 @@ import { resetKeyGenerator } from 'slate'
|
||||
describe('slate-react', () => {
|
||||
require('./plugins')
|
||||
require('./rendering')
|
||||
require('./utils')
|
||||
})
|
||||
|
||||
/**
|
||||
|
93
packages/slate-react/test/utils/get-children-decorations.js
Normal file
93
packages/slate-react/test/utils/get-children-decorations.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/** @jsx h */
|
||||
|
||||
import { List } from 'immutable'
|
||||
import assert from 'assert'
|
||||
|
||||
import h from '../helpers/h'
|
||||
|
||||
import getChildrenDecorations from '../../src/utils/get-children-decorations'
|
||||
|
||||
const value = (
|
||||
<value>
|
||||
<document key="a">
|
||||
<paragraph key="b">
|
||||
<text key="c">First line</text>
|
||||
</paragraph>
|
||||
<paragraph key="d">
|
||||
<text key="e">Second line</text>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
)
|
||||
|
||||
const { document } = value
|
||||
const [paragraphB] = document.nodes.toArray()
|
||||
|
||||
describe('getChildrenDecorations', () => {
|
||||
it('should return the child list when no decorations are given', () => {
|
||||
const actual = getChildrenDecorations(document, List())
|
||||
|
||||
const expected = [[], []]
|
||||
|
||||
assert.deepEqual(actual.map(l => l.toArray()), expected)
|
||||
})
|
||||
|
||||
it('should wrap a block with the range it contains', () => {
|
||||
const decoration1 = {
|
||||
startKey: 'c',
|
||||
startOffset: 1,
|
||||
endKey: 'c',
|
||||
endOffset: 2,
|
||||
decoration: 'd1',
|
||||
}
|
||||
|
||||
const actual = getChildrenDecorations(document, List([decoration1]))
|
||||
|
||||
const expected = [[decoration1], []]
|
||||
|
||||
assert.deepEqual(actual.map(l => l.toArray()), expected)
|
||||
})
|
||||
|
||||
it('should sort two decorations inside a node', () => {
|
||||
const decoration1 = {
|
||||
startKey: 'c',
|
||||
startOffset: 1,
|
||||
endKey: 'c',
|
||||
endOffset: 2,
|
||||
decoration: 'd1',
|
||||
}
|
||||
|
||||
const decoration2 = {
|
||||
startKey: 'c',
|
||||
startOffset: 1,
|
||||
endKey: 'e',
|
||||
endOffset: 2,
|
||||
decoration: 'd2',
|
||||
}
|
||||
|
||||
const actual = getChildrenDecorations(
|
||||
document,
|
||||
List([decoration1, decoration2])
|
||||
)
|
||||
|
||||
const expected = [[decoration1, decoration2], [decoration2]]
|
||||
|
||||
assert.deepEqual(actual.map(l => l.toArray()), expected)
|
||||
})
|
||||
|
||||
it('should sort decorations outside the node', () => {
|
||||
const decoration1 = {
|
||||
startKey: 'c',
|
||||
startOffset: 1,
|
||||
endKey: 'e',
|
||||
endOffset: 2,
|
||||
decoration: 'd1',
|
||||
}
|
||||
|
||||
const actual = getChildrenDecorations(paragraphB, List([decoration1]))
|
||||
|
||||
const expected = [[decoration1]]
|
||||
|
||||
assert.deepEqual(actual.map(l => l.toArray()), expected)
|
||||
})
|
||||
})
|
21
packages/slate-react/test/utils/index.js
Normal file
21
packages/slate-react/test/utils/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Dependencies.
|
||||
*/
|
||||
|
||||
import { resetKeyGenerator } from 'slate'
|
||||
|
||||
/**
|
||||
* Tests.
|
||||
*/
|
||||
|
||||
describe('utils', () => {
|
||||
require('./get-children-decorations')
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset Slate's internal state before each text.
|
||||
*/
|
||||
|
||||
beforeEach(() => {
|
||||
resetKeyGenerator()
|
||||
})
|
Reference in New Issue
Block a user