forked from ianstormtaylor/slate
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnode.js
More file actions
354 lines (297 loc) · 9.06 KB
/
node.js
File metadata and controls
354 lines (297 loc) · 9.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import Base64 from '../serializers/base-64'
import Debug from 'debug'
import React from 'react'
import ReactDOM from 'react-dom'
import TYPES from '../constants/types'
import Leaf from './leaf'
import Void from './void'
import getWindow from 'get-window'
import scrollToSelection from '../utils/scroll-to-selection'
/**
* Debug.
*
* @type {Function}
*/
const debug = Debug('slate:node')
/**
* Node.
*
* @type {Component}
*/
class Node extends React.Component {
/**
* Property types.
*
* @type {Object}
*/
static propTypes = {
block: React.PropTypes.object,
editor: React.PropTypes.object.isRequired,
node: React.PropTypes.object.isRequired,
parent: React.PropTypes.object.isRequired,
readOnly: React.PropTypes.bool.isRequired,
schema: React.PropTypes.object.isRequired,
state: React.PropTypes.object.isRequired
}
/**
* Constructor.
*
* @param {Object} props
*/
constructor(props) {
super(props)
const { node, schema } = props
this.state = {}
this.state.Component = node.kind == 'text' ? null : node.getComponent(schema)
}
/**
* Debug.
*
* @param {String} message
* @param {Mixed} ...args
*/
debug = (message, ...args) => {
const { node } = this.props
const { key, kind, type } = node
const id = kind == 'text' ? `${key} (${kind})` : `${key} (${type})`
debug(message, `${id}`, ...args)
}
/**
* On receiving new props, update the `Component` renderer.
*
* @param {Object} props
*/
componentWillReceiveProps = (props) => {
if (props.node.kind == 'text') return
if (props.node == this.props.node) return
const Component = props.node.getComponent(props.schema)
this.setState({ Component })
}
/**
* Should the node update?
*
* @param {Object} nextProps
* @param {Object} state
* @return {Boolean}
*/
shouldComponentUpdate = (nextProps) => {
const { props } = this
const { Component } = this.state
// If the `Component` has enabled suppression of update checking, always
// return true so that it can deal with update checking itself.
if (Component && Component.suppressShouldComponentUpdate) return true
// 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 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 the node is a block or inline, which can have custom renderers, we
// include an extra check to re-render if the node's focus changes, to make
// it simple for users to show a node's "selected" state.
if (nextProps.node.kind != 'text') {
const hasEdgeIn = props.state.selection.hasEdgeIn(props.node)
const nextHasEdgeIn = nextProps.state.selection.hasEdgeIn(nextProps.node)
const hasFocus = props.state.isFocused || nextProps.state.isFocused
const hasEdge = hasEdgeIn || nextHasEdgeIn
if (hasFocus && hasEdge) 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 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
}
// Otherwise, don't update.
return false
}
/**
* On mount, update the scroll position.
*/
componentDidMount = () => {
this.updateScroll()
}
/**
* After update, update the scroll position if the node's content changed.
*
* @param {Object} prevProps
* @param {Object} prevState
*/
componentDidUpdate = (prevProps, prevState) => {
if (this.props.node != prevProps.node) this.updateScroll()
}
/**
* Update the scroll position after a change as occured if this is a leaf
* block and it has the selection's ending edge. This ensures that scrolling
* matches native `contenteditable` behavior even for cases where the edit is
* not applied natively, like when enter is pressed.
*/
updateScroll = () => {
const { node, state } = this.props
const { selection } = state
// If this isn't a block, or it's a wrapping block, abort.
if (node.kind != 'block') return
if (node.nodes.first().kind == 'block') return
// If the selection is blurred, or this block doesn't contain it, abort.
if (selection.isBlurred) return
if (!selection.hasEndIn(node)) return
const el = ReactDOM.findDOMNode(this)
const window = getWindow(el)
const native = window.getSelection()
scrollToSelection(native)
this.debug('updateScroll', el)
}
/**
* On drag start, add a serialized representation of the node to the data.
*
* @param {Event} e
*/
onDragStart = (e) => {
const { node } = this.props
const encoded = Base64.serializeNode(node, { preserveKeys: true })
const data = e.nativeEvent.dataTransfer
data.setData(TYPES.NODE, encoded)
this.debug('onDragStart', e)
}
/**
* Render.
*
* @return {Element}
*/
render = () => {
const { props } = this
const { node } = this.props
this.debug('render', { props })
return node.kind == 'text'
? this.renderText()
: this.renderElement()
}
/**
* Render a `child` node.
*
* @param {Node} child
* @return {Element}
*/
renderNode = (child) => {
const { block, editor, node, readOnly, schema, state } = this.props
return (
<Node
key={child.key}
node={child}
block={node.kind == 'block' ? node : block}
parent={node}
editor={editor}
readOnly={readOnly}
schema={schema}
state={state}
/>
)
}
/**
* Render an element `node`.
*
* @return {Element}
*/
renderElement = () => {
const { editor, node, parent, readOnly, state } = this.props
const { Component } = this.state
const children = node.nodes.map(this.renderNode).toArray()
// Attributes that the developer must to mix into the element in their
// custom node renderer component.
const attributes = {
'data-key': node.key,
'onDragStart': this.onDragStart
}
// If it's a block node with inline children, add the proper `dir` attribute
// for text direction.
if (node.kind == 'block' && node.nodes.first().kind != 'block') {
const direction = node.getTextDirection()
if (direction == 'rtl') attributes.dir = 'rtl'
}
const element = (
<Component
attributes={attributes}
key={node.key}
editor={editor}
parent={parent}
node={node}
readOnly={readOnly}
state={state}
>
{children}
</Component>
)
return node.isVoid
? <Void {...this.props}>{element}</Void>
: element
}
/**
* Render a text node.
*
* @return {Element}
*/
renderText = () => {
const { node, schema, state } = this.props
const { document } = state
const decorators = schema.hasDecorators ? document.getDescendantDecorators(node.key, schema) : []
const ranges = node.getRanges(decorators)
let offset = 0
const leaves = ranges.map((range, i) => {
const leaf = this.renderLeaf(ranges, range, i, offset)
offset += range.text.length
return leaf
})
return (
<span data-key={node.key}>
{leaves}
</span>
)
}
/**
* Render a single leaf node given a `range` and `offset`.
*
* @param {List<Range>} ranges
* @param {Range} range
* @param {Number} index
* @param {Number} offset
* @return {Element} leaf
*/
renderLeaf = (ranges, range, index, offset) => {
const { block, node, parent, schema, state, editor } = this.props
const { text, marks } = range
return (
<Leaf
key={`${node.key}-${index}`}
block={block}
editor={editor}
index={index}
marks={marks}
node={node}
offset={offset}
parent={parent}
ranges={ranges}
schema={schema}
state={state}
text={text}
/>
)
}
}
/**
* Export.
*
* @type {Component}
*/
export default Node