1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-06-06 11:26:05 +02:00
slate/lib/components/node.js
2016-08-05 12:40:54 -07:00

304 lines
6.5 KiB
JavaScript

import Base64 from '../serializers/base-64'
import Debug from 'debug'
import React from 'react'
import ReactDOM from 'react-dom'
import TYPES from '../utils/types'
import Text from './text'
import Void from './void'
import scrollTo from '../utils/scroll-to'
/**
* 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,
renderDecorations: React.PropTypes.func.isRequired,
renderMark: React.PropTypes.func.isRequired,
renderNode: React.PropTypes.func.isRequired,
state: React.PropTypes.object.isRequired
};
/**
* Default properties.
*
* @type {Object}
*/
static defaultProps = {
style: {}
}
/**
* Constructor.
*
* @param {Object} props
*/
constructor(props) {
super(props)
this.state = {}
if (props.node.kind != 'text') {
this.state.Component = props.renderNode(props.node)
}
}
/**
* 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
this.setState({
Component: props.renderNode(props.node)
})
}
/**
* Should the node update?
*
* @param {Object} props
* @param {Object} state
* @return {Boolean}
*/
shouldComponentUpdate = (props) => {
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
}
// Otherwise, perform a peformant update check by default.
if (
props.node != this.props.node ||
(props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
) {
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' &&
this.props.state.isFocused &&
this.props.state.selection.hasEdgeIn(this.props.node)
) {
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) => {
const { editor, renderDecorations, renderMark, renderNode, state } = this.props
return (
<Node
key={child.key}
node={child}
state={state}
editor={editor}
renderDecorations={renderDecorations}
renderMark={renderMark}
renderNode={renderNode}
/>
)
}
/**
* Render an element `node`.
*
* @return {Element} element
*/
renderElement = () => {
const { editor, node, 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}
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, editor, renderDecorations, renderMark, state } = this.props
return (
<Text
key={node.key}
editor={editor}
node={node}
renderDecorations={renderDecorations}
renderMark={renderMark}
state={state}
/>
)
}
}
/**
* Export.
*/
export default Node