forked from steemit/slate
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnode.js
More file actions
371 lines (311 loc) · 8.88 KB
/
node.js
File metadata and controls
371 lines (311 loc) · 8.88 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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
import Immutable from 'immutable'
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 IS_DEV from '../constants/is-dev'
import Leaf from './leaf'
import Void from './void'
import scrollTo from '../utils/scroll-to'
import warning from '../utils/warning'
/**
* Debug.
*
* @type {Function}
*/
const debug = Debug('slate:node')
/**
* Node.
*
* @type {Component}
*/
class Node extends React.Component {
/**
* Property types.
*
* @type {Object}
*/
static propTypes = {
editor: React.PropTypes.object.isRequired,
node: React.PropTypes.object.isRequired,
parent: React.PropTypes.object.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
let 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 { Component } = this.state
// If the node is rendered with a `Component` that 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 node has changed, update.
if (nextProps.node != this.props.node) {
if (!IS_DEV || !Immutable.is(nextProps.node, this.props.node)) {
return true
} else {
warning('Encountered different references for identical node values in "shouldComponentUpdate". Check that you are preserving references for the following node:\n', nextProps.node)
}
}
const nextHasEdgeIn = nextProps.state.selection.hasEdgeIn(nextProps.node)
// If the selection is focused and is inside the node, we need to update so
// that the selection will be set by one of the <Leaf> components.
if (
nextProps.state.isFocused &&
nextHasEdgeIn
) {
return true
}
const hasEdgeIn = this.props.state.selection.hasEdgeIn(nextProps.node)
// If the selection is blurred but was previously focused (or vice versa) inside the node,
// we need to update to ensure the selection gets updated by re-rendering.
if (
this.props.state.isFocused != nextProps.state.isFocused &&
(
hasEdgeIn || nextHasEdgeIn
)
) {
return true
}
// For block and inline nodes, which can have custom renderers, we need to
// include another check for whether the previous selection had an edge in
// the node, to allow for intuitive selection-based rendering.
if (
this.props.node.kind != 'text' &&
hasEdgeIn != nextHasEdgeIn
) {
return true
}
// For text nodes, which can have custom decorations, we need to check to
// see if the block has changed, which has caused the decorations to change.
if (nextProps.node.kind == 'text' && nextProps.schema.hasDecorators) {
const { node, schema, state } = nextProps
const { document } = state
const decorators = document.getDescendantDecorators(node.key, schema)
const ranges = node.getRanges(decorators)
const prevNode = this.props.node
const prevSchema = this.props.schema
const prevDocument = this.props.state.document
const prevDecorators = prevDocument.getDescendantDecorators(prevNode.key, prevSchema)
const prevRanges = prevNode.getRanges(prevDecorators)
if (!ranges.equals(prevRanges)) {
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)
scrollTo(el)
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)
const data = e.nativeEvent.dataTransfer
data.setData(TYPES.NODE, encoded)
this.debug('onDragStart', e)
}
/**
* Render.
*
* @return {Element} element
*/
render = () => {
this.debug('render')
const { node } = this.props
return node.kind == 'text'
? this.renderText()
: this.renderElement()
}
/**
* Render a `child` node.
*
* @param {Node} child
* @return {Element} element
*/
renderNode = (child) => {
return (
<Node
key={child.key}
node={child}
parent={this.props.node}
editor={this.props.editor}
schema={this.props.schema}
state={this.props.state}
/>
)
}
/**
* Render an element `node`.
*
* @return {Element} element
*/
renderElement = () => {
const { editor, node, parent, state } = this.props
const { Component } = this.state
const children = node.nodes
.map(child => this.renderNode(child))
.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}
state={state}
>
{children}
</Component>
)
return node.isVoid
? <Void {...this.props}>{element}</Void>
: element
}
/**
* Render a text node.
*
* @return {Element} 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} ranges
* @param {Range} range
* @param {Number} index
* @param {Number} offset
* @return {Element} leaf
*/
renderLeaf = (ranges, range, index, offset) => {
const { node, parent, schema, state } = this.props
const text = range.text
const marks = range.marks
return (
<Leaf
key={`${node.key}-${index}`}
index={index}
marks={marks}
node={node}
parent={parent}
ranges={ranges}
schema={schema}
state={state}
text={text}
/>
)
}
}
/**
* Export.
*
* @type {Component}
*/
export default Node