mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-06 15:26:34 +02:00
add code highlighting example, still slow
This commit is contained in:
147
examples/code-highlighting/index.js
Normal file
147
examples/code-highlighting/index.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
|
||||||
|
import Editor, { Mark, Raw, Selection } from '../..'
|
||||||
|
import Prism from 'prismjs'
|
||||||
|
import React from 'react'
|
||||||
|
import keycode from 'keycode'
|
||||||
|
import state from './state.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node and mark renderers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NODES = {
|
||||||
|
code: props => <pre><code>{props.children}</code></pre>,
|
||||||
|
paragraph: props => <p>{props.children}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
const MARKS = {
|
||||||
|
'highlight-comment': {
|
||||||
|
opacity: '0.33'
|
||||||
|
},
|
||||||
|
'highlight-keyword': {
|
||||||
|
fontWeight: 'bold'
|
||||||
|
},
|
||||||
|
'highlight-punctuation': {
|
||||||
|
opacity: '0.75'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example.
|
||||||
|
*
|
||||||
|
* @type {Component} CodeHighlighting
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CodeHighlighting extends React.Component {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
state: Raw.deserialize(state)
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyDown(e, state, editor) {
|
||||||
|
const key = keycode(e.which)
|
||||||
|
if (key != 'enter') return
|
||||||
|
const { startBlock } = state
|
||||||
|
if (startBlock.type != 'code') return
|
||||||
|
|
||||||
|
let transform = state.transform()
|
||||||
|
if (state.isExpanded) transform = transform.delete()
|
||||||
|
transform = transform.insertText('\n')
|
||||||
|
|
||||||
|
return transform.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="editor">
|
||||||
|
<Editor
|
||||||
|
state={this.state.state}
|
||||||
|
renderNode={(...args) => this.renderNode(...args)}
|
||||||
|
renderMark={(...args) => this.renderMark(...args)}
|
||||||
|
renderDecorations={(...args) => this.renderDecorations(...args)}
|
||||||
|
onKeyDown={(...args) => this.onKeyDown(...args)}
|
||||||
|
onChange={(state) => {
|
||||||
|
console.groupCollapsed('Change!')
|
||||||
|
console.log('Document:', state.document.toJS())
|
||||||
|
console.log('Selection:', state.selection.toJS())
|
||||||
|
console.log('Content:', Raw.serialize(state))
|
||||||
|
console.groupEnd()
|
||||||
|
this.setState({ state })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNode(node) {
|
||||||
|
return NODES[node.type]
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMark(mark) {
|
||||||
|
return MARKS[mark.type] || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDecorations(text, state, editor) {
|
||||||
|
let characters = text.characters
|
||||||
|
const { document } = state
|
||||||
|
const block = document.getClosestBlock(text)
|
||||||
|
if (block.type != 'code') return characters
|
||||||
|
|
||||||
|
const string = text.text
|
||||||
|
console.log('render decorations:', string)
|
||||||
|
const grammar = Prism.languages.javascript
|
||||||
|
const tokens = Prism.tokenize(string, grammar)
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (typeof token == 'string') {
|
||||||
|
offset += token.length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = offset + token.content.length
|
||||||
|
const type = `highlight-${token.type}`
|
||||||
|
|
||||||
|
for (let i = offset; i < length; i++) {
|
||||||
|
let char = characters.get(i)
|
||||||
|
let { marks } = char
|
||||||
|
marks = marks.add(Mark.create({ type }))
|
||||||
|
char = char.merge({ marks })
|
||||||
|
characters = characters.set(i, char)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = length
|
||||||
|
}
|
||||||
|
|
||||||
|
return characters
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderDecorations(text) {
|
||||||
|
// const { state } = this.state
|
||||||
|
// const { document } = state
|
||||||
|
// const block = document.getClosestBlock(text)
|
||||||
|
// if (block.type != 'code') return
|
||||||
|
|
||||||
|
// const string = text.text
|
||||||
|
// if (cache[string]) return cache[string]
|
||||||
|
|
||||||
|
// const grammar = Prism.languages.javascript
|
||||||
|
// const tokens = Prism.tokenize(string, grammar)
|
||||||
|
// const ranges = tokens.map((token) => {
|
||||||
|
// return typeof token == 'string'
|
||||||
|
// ? { text: token }
|
||||||
|
// : {
|
||||||
|
// text: token.content,
|
||||||
|
// marks: [{ type: token.type }]
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return cached[string] = ranges
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default CodeHighlighting
|
46
examples/code-highlighting/state.json
Normal file
46
examples/code-highlighting/state.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "There are certain behaviors that require rendering dynamic marks on string of text, like rendering code highlighting. For example:"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "code",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "// A simple FizzBuzz implementation.\nfor (var i = 1; i <= 100; i++) {\n if (i % 15 == 0) {\n console.log('Fizz Buzz');\n } else if (i % 5 == 0) {\n console.log('Buzz');\n } else if (i % 3 == 0) {\n console.log('Fizz');\n } else {\n console.log(i);\n }\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"ranges": [
|
||||||
|
{
|
||||||
|
"text": "Try it out for yourself!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -8,6 +8,7 @@ import { Router, Route, Link, IndexRedirect, hashHistory } from 'react-router'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import AutoMarkdown from './auto-markdown'
|
import AutoMarkdown from './auto-markdown'
|
||||||
|
import CodeHighlighting from './code-highlighting'
|
||||||
import HoveringMenu from './hovering-menu'
|
import HoveringMenu from './hovering-menu'
|
||||||
import Images from './images'
|
import Images from './images'
|
||||||
import Links from './links'
|
import Links from './links'
|
||||||
@@ -32,7 +33,7 @@ class App extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="app">
|
<div className="app">
|
||||||
{this.renderTabBar()}
|
{this.renderTabBar()}
|
||||||
{this.renderExample()}
|
{this.renderExample()}
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +56,7 @@ class App extends React.Component {
|
|||||||
{this.renderTab('Links', 'links')}
|
{this.renderTab('Links', 'links')}
|
||||||
{this.renderTab('Images', 'images')}
|
{this.renderTab('Images', 'images')}
|
||||||
{this.renderTab('Tables', 'tables')}
|
{this.renderTab('Tables', 'tables')}
|
||||||
|
{this.renderTab('Code Highlighting', 'code-highlighting')}
|
||||||
{this.renderTab('Paste HTML', 'paste-html')}
|
{this.renderTab('Paste HTML', 'paste-html')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -100,6 +102,7 @@ const router = (
|
|||||||
<Route path="/" component={App}>
|
<Route path="/" component={App}>
|
||||||
<IndexRedirect to="rich-text" />
|
<IndexRedirect to="rich-text" />
|
||||||
<Route path="auto-markdown" component={AutoMarkdown} />
|
<Route path="auto-markdown" component={AutoMarkdown} />
|
||||||
|
<Route path="code-highlighting" component={CodeHighlighting} />
|
||||||
<Route path="hovering-menu" component={HoveringMenu} />
|
<Route path="hovering-menu" component={HoveringMenu} />
|
||||||
<Route path="images" component={Images} />
|
<Route path="images" component={Images} />
|
||||||
<Route path="links" component={Links} />
|
<Route path="links" component={Links} />
|
||||||
|
@@ -49,13 +49,11 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
shouldComponentUpdate(props, state) {
|
shouldComponentUpdate(props, state) {
|
||||||
// if (props.state.isNative) return false
|
if (props.state.isNative) return false
|
||||||
return true
|
return (
|
||||||
// return (
|
props.state.selection != this.props.state.selection ||
|
||||||
// props.state != this.props.state ||
|
props.state.document != this.props.state.document
|
||||||
// props.state.selection != this.props.state.selection ||
|
)
|
||||||
// props.state.document != this.props.state.document
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,22 +63,26 @@ class Content extends React.Component {
|
|||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
*/
|
*/
|
||||||
|
|
||||||
componentWillMount(props) {
|
componentWillMount() {
|
||||||
|
console.log('is rendering')
|
||||||
this.tmp.isRendering = true
|
this.tmp.isRendering = true
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(props) {
|
componentWillUpdate(props, state) {
|
||||||
|
console.log('is rendering')
|
||||||
this.tmp.isRendering = true
|
this.tmp.isRendering = true
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('not rendering')
|
||||||
this.tmp.isRendering = false
|
this.tmp.isRendering = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate(props, state) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('not rendering')
|
||||||
this.tmp.isRendering = false
|
this.tmp.isRendering = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -103,11 +105,13 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
onBlur(e) {
|
onBlur(e) {
|
||||||
if (this.tmp.isCopying) return
|
if (this.tmp.isCopying) return
|
||||||
|
|
||||||
let { state } = this.props
|
let { state } = this.props
|
||||||
let { document, selection } = state
|
|
||||||
selection = selection.merge({ isFocused: false })
|
state = state
|
||||||
state = state.merge({ selection })
|
.transform()
|
||||||
|
.blur()
|
||||||
|
.apply({ isNative: true })
|
||||||
|
|
||||||
this.onChange(state)
|
this.onChange(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,16 +310,17 @@ class Content extends React.Component {
|
|||||||
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
|
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
|
||||||
const focus = OffsetKey.findPoint(focusNode, focusOffset)
|
const focus = OffsetKey.findPoint(focusNode, focusOffset)
|
||||||
|
|
||||||
selection = selection.merge({
|
state = state
|
||||||
anchorKey: anchor.key,
|
.transform()
|
||||||
anchorOffset: anchor.offset,
|
.moveTo({
|
||||||
focusKey: focus.key,
|
anchorKey: anchor.key,
|
||||||
focusOffset: focus.offset,
|
anchorOffset: anchor.offset,
|
||||||
isFocused: true
|
focusKey: focus.key,
|
||||||
})
|
focusOffset: focus.offset
|
||||||
|
})
|
||||||
|
.focus()
|
||||||
|
.apply({ isNative: true })
|
||||||
|
|
||||||
selection = selection.normalize(document)
|
|
||||||
state = state.merge({ selection })
|
|
||||||
this.onChange(state)
|
this.onChange(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +331,7 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
console.log('render contents')
|
||||||
const { state } = this.props
|
const { state } = this.props
|
||||||
const { document } = state
|
const { document } = state
|
||||||
const children = document.nodes
|
const children = document.nodes
|
||||||
@@ -432,7 +438,7 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderText(node) {
|
renderText(node) {
|
||||||
const { editor, renderMark, renderNode, state } = this.props
|
const { editor, renderMark, state } = this.props
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={node.key}
|
key={node.key}
|
||||||
|
@@ -10,17 +10,21 @@ import corePlugin from '../plugins/core'
|
|||||||
|
|
||||||
class Editor extends React.Component {
|
class Editor extends React.Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties.
|
||||||
|
*/
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
plugins: React.PropTypes.array,
|
plugins: React.PropTypes.array,
|
||||||
|
renderDecorations: React.PropTypes.func,
|
||||||
renderMark: React.PropTypes.func,
|
renderMark: React.PropTypes.func,
|
||||||
renderNode: React.PropTypes.func,
|
renderNode: React.PropTypes.func,
|
||||||
state: React.PropTypes.object,
|
state: React.PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired
|
onChange: React.PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
plugins: [],
|
plugins: []
|
||||||
state: new State()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +37,7 @@ class Editor extends React.Component {
|
|||||||
super(props)
|
super(props)
|
||||||
this.state = {}
|
this.state = {}
|
||||||
this.state.plugins = this.resolvePlugins(props)
|
this.state.plugins = this.resolvePlugins(props)
|
||||||
|
this.state.state = this.resolveState(props.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,8 +47,8 @@ class Editor extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
componentWillReceiveProps(props) {
|
componentWillReceiveProps(props) {
|
||||||
const plugins = this.resolvePlugins(props)
|
this.setState({ plugins: this.resolvePlugins(props) })
|
||||||
this.setState({ plugins })
|
this.setState({ state: this.resolveState(props.state) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +58,7 @@ class Editor extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getState() {
|
getState() {
|
||||||
return this.props.state
|
return this.state.state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +68,7 @@ class Editor extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onChange(state) {
|
onChange(state) {
|
||||||
if (state == this.props.state) return
|
if (state == this.state.state) return
|
||||||
|
|
||||||
for (const plugin of this.state.plugins) {
|
for (const plugin of this.state.plugins) {
|
||||||
if (!plugin.onChange) continue
|
if (!plugin.onChange) continue
|
||||||
@@ -86,9 +91,9 @@ class Editor extends React.Component {
|
|||||||
onEvent(name, ...args) {
|
onEvent(name, ...args) {
|
||||||
for (const plugin of this.state.plugins) {
|
for (const plugin of this.state.plugins) {
|
||||||
if (!plugin[name]) continue
|
if (!plugin[name]) continue
|
||||||
const newState = plugin[name](...args, this.props.state, this)
|
const newState = plugin[name](...args, this.state.state, this)
|
||||||
if (!newState) continue
|
if (!newState) continue
|
||||||
this.props.onChange(newState)
|
this.onChange(newState)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,14 +101,14 @@ class Editor extends React.Component {
|
|||||||
/**
|
/**
|
||||||
* Render the editor.
|
* Render the editor.
|
||||||
*
|
*
|
||||||
* @return {Component} component
|
* @return {Element} element
|
||||||
*/
|
*/
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Content
|
<Content
|
||||||
editor={this}
|
editor={this}
|
||||||
state={this.props.state}
|
state={this.state.state}
|
||||||
onChange={state => this.onChange(state)}
|
onChange={state => this.onChange(state)}
|
||||||
renderMark={mark => this.renderMark(mark)}
|
renderMark={mark => this.renderMark(mark)}
|
||||||
renderNode={node => this.renderNode(node)}
|
renderNode={node => this.renderNode(node)}
|
||||||
@@ -118,13 +123,13 @@ class Editor extends React.Component {
|
|||||||
* Render a `node`, cascading through the plugins.
|
* Render a `node`, cascading through the plugins.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @return {Component} component
|
* @return {Element} element
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode(node) {
|
renderNode(node) {
|
||||||
for (const plugin of this.state.plugins) {
|
for (const plugin of this.state.plugins) {
|
||||||
if (!plugin.renderNode) continue
|
if (!plugin.renderNode) continue
|
||||||
const component = plugin.renderNode(node, this.props.state, this)
|
const component = plugin.renderNode(node, this.state.state, this)
|
||||||
if (component) return component
|
if (component) return component
|
||||||
throw new Error(`No renderer found for node with type "${node.type}".`)
|
throw new Error(`No renderer found for node with type "${node.type}".`)
|
||||||
}
|
}
|
||||||
@@ -140,7 +145,7 @@ class Editor extends React.Component {
|
|||||||
renderMark(mark) {
|
renderMark(mark) {
|
||||||
for (const plugin of this.state.plugins) {
|
for (const plugin of this.state.plugins) {
|
||||||
if (!plugin.renderMark) continue
|
if (!plugin.renderMark) continue
|
||||||
const style = plugin.renderMark(mark, this.props.state, this)
|
const style = plugin.renderMark(mark, this.state.state, this)
|
||||||
if (style) return style
|
if (style) return style
|
||||||
throw new Error(`No renderer found for mark with type "${mark.type}".`)
|
throw new Error(`No renderer found for mark with type "${mark.type}".`)
|
||||||
}
|
}
|
||||||
@@ -169,6 +174,32 @@ class Editor extends React.Component {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the editor's current state from `props` when they change.
|
||||||
|
*
|
||||||
|
* This is where we handle decorating the text nodes with the decorator
|
||||||
|
* functions, so that they are always accounted for when rendering.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
resolveState(state) {
|
||||||
|
const { plugins } = this.state
|
||||||
|
let { document } = state
|
||||||
|
|
||||||
|
document = document.decorateTexts((text) => {
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (!plugin.renderDecorations) continue
|
||||||
|
const characters = plugin.renderDecorations(text, state, this)
|
||||||
|
if (characters) return characters
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
state = state.merge({ document })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,6 +9,10 @@ import ReactDOM from 'react-dom'
|
|||||||
|
|
||||||
class Leaf extends React.Component {
|
class Leaf extends React.Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties.
|
||||||
|
*/
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
marks: React.PropTypes.object.isRequired,
|
marks: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
@@ -19,6 +23,23 @@ class Leaf extends React.Component {
|
|||||||
text: React.PropTypes.string.isRequired
|
text: React.PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should component update?
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} state
|
||||||
|
* @return {Boolean} shouldUpdate
|
||||||
|
*/
|
||||||
|
|
||||||
|
shouldComponentUpdate(props, state) {
|
||||||
|
return (
|
||||||
|
props.start != this.props.start ||
|
||||||
|
props.end != this.props.end ||
|
||||||
|
props.text != this.props.text ||
|
||||||
|
props.marks != this.props.marks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateSelection()
|
this.updateSelection()
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,10 @@ import { List } from 'immutable'
|
|||||||
|
|
||||||
class Text extends React.Component {
|
class Text extends React.Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties.
|
||||||
|
*/
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
editor: React.PropTypes.object.isRequired,
|
editor: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
@@ -18,8 +22,29 @@ class Text extends React.Component {
|
|||||||
state: React.PropTypes.object.isRequired
|
state: React.PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the component update?
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} state
|
||||||
|
* @return {Boolean} shouldUpdate
|
||||||
|
*/
|
||||||
|
|
||||||
|
shouldComponentUpdate(props, state) {
|
||||||
|
return (
|
||||||
|
props.node.decorations != this.props.node.decorations ||
|
||||||
|
props.node.characters != this.props.node.characters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render.
|
||||||
|
*
|
||||||
|
* @return {Element} element
|
||||||
|
*/
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { node } = this.props
|
console.log('render text:', this.props.node.key)
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{this.renderLeaves()}
|
{this.renderLeaves()}
|
||||||
@@ -27,10 +52,17 @@ class Text extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the leaf nodes.
|
||||||
|
*
|
||||||
|
* @return {Array} leaves
|
||||||
|
*/
|
||||||
|
|
||||||
renderLeaves() {
|
renderLeaves() {
|
||||||
const { node } = this.props
|
const { node } = this.props
|
||||||
const { characters } = node
|
const { characters, decorations } = node
|
||||||
const ranges = groupByMarks(characters)
|
const ranges = groupByMarks(decorations || characters)
|
||||||
|
|
||||||
return ranges.map((range, i, ranges) => {
|
return ranges.map((range, i, ranges) => {
|
||||||
const previous = ranges.slice(0, i)
|
const previous = ranges.slice(0, i)
|
||||||
const offset = previous.size
|
const offset = previous.size
|
||||||
@@ -40,6 +72,14 @@ class Text extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single leaf node given a `range` and `offset`.
|
||||||
|
*
|
||||||
|
* @param {Object} range
|
||||||
|
* @param {Number} offset
|
||||||
|
* @return {Element} leaf
|
||||||
|
*/
|
||||||
|
|
||||||
renderLeaf(range, offset) {
|
renderLeaf(range, offset) {
|
||||||
const { node, renderMark, state } = this.props
|
const { node, renderMark, state } = this.props
|
||||||
const text = range.text
|
const text = range.text
|
||||||
|
@@ -19,20 +19,6 @@ class Void extends React.Component {
|
|||||||
state: React.PropTypes.object.isRequired
|
state: React.PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
// onClick(e) {
|
|
||||||
// e.preventDefault()
|
|
||||||
// let { editor, node, state } = this.props
|
|
||||||
// let text = node.getTextNodes().first()
|
|
||||||
|
|
||||||
// state = state
|
|
||||||
// .transform()
|
|
||||||
// .moveToStartOf(text)
|
|
||||||
// .focus()
|
|
||||||
// .apply()
|
|
||||||
|
|
||||||
// editor.onChange(state)
|
|
||||||
// }
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, node } = this.props
|
const { children, node } = this.props
|
||||||
const Tag = node.kind == 'block' ? 'div' : 'span'
|
const Tag = node.kind == 'block' ? 'div' : 'span'
|
||||||
|
@@ -58,6 +58,21 @@ const Node = {
|
|||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorate all of the text nodes with a `decorator` function.
|
||||||
|
*
|
||||||
|
* @param {Function} decorator
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
decorateTexts(decorator) {
|
||||||
|
return this.mapDescendants((child) => {
|
||||||
|
return child.kind == 'text'
|
||||||
|
? child.decorateCharacters(decorator)
|
||||||
|
: child
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find all ancestor nodes by `iterator`.
|
* Recursively find all ancestor nodes by `iterator`.
|
||||||
*
|
*
|
||||||
|
@@ -285,6 +285,17 @@ class Selection extends Record(DEFAULTS) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the selection to a specific anchor and focus point.
|
||||||
|
*
|
||||||
|
* @param {Object} properties
|
||||||
|
* @return {Selection} selection
|
||||||
|
*/
|
||||||
|
|
||||||
|
moveTo(properties) {
|
||||||
|
return this.merge(properties)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the focus point to the anchor point.
|
* Move the focus point to the anchor point.
|
||||||
*
|
*
|
||||||
|
@@ -22,8 +22,7 @@ const DEFAULTS = {
|
|||||||
document: new Document(),
|
document: new Document(),
|
||||||
selection: new Selection(),
|
selection: new Selection(),
|
||||||
history: new History(),
|
history: new History(),
|
||||||
isNative: true,
|
isNative: false
|
||||||
copiedFragment: null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -10,7 +10,9 @@ import { List, Record } from 'immutable'
|
|||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
characters: new List(),
|
characters: new List(),
|
||||||
key: null
|
decorations: null,
|
||||||
|
key: null,
|
||||||
|
cache: null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +32,8 @@ class Text extends Record(DEFAULTS) {
|
|||||||
if (properties instanceof Text) return properties
|
if (properties instanceof Text) return properties
|
||||||
properties.key = uid(4)
|
properties.key = uid(4)
|
||||||
properties.characters = Character.createList(properties.characters)
|
properties.characters = Character.createList(properties.characters)
|
||||||
|
properties.decorations = null
|
||||||
|
properties.cache = null
|
||||||
return new Text(properties)
|
return new Text(properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +69,26 @@ class Text extends Record(DEFAULTS) {
|
|||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorate the text node's characters with a `decorator` function.
|
||||||
|
*
|
||||||
|
* @param {Function} decorator
|
||||||
|
* @return {Text} text
|
||||||
|
*/
|
||||||
|
|
||||||
|
decorateCharacters(decorator) {
|
||||||
|
let { characters, cache } = this
|
||||||
|
if (characters == cache) return this
|
||||||
|
|
||||||
|
const decorations = decorator(this)
|
||||||
|
if (decorations == characters) return this
|
||||||
|
|
||||||
|
return this.merge({
|
||||||
|
cache: characters,
|
||||||
|
decorations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove characters from the text node from `start` to `end`.
|
* Remove characters from the text node from `start` to `end`.
|
||||||
*
|
*
|
||||||
|
@@ -57,6 +57,7 @@ const SELECTION_TRANSFORMS = [
|
|||||||
'focus',
|
'focus',
|
||||||
'moveBackward',
|
'moveBackward',
|
||||||
'moveForward',
|
'moveForward',
|
||||||
|
'moveTo',
|
||||||
'moveToAnchor',
|
'moveToAnchor',
|
||||||
'moveToEnd',
|
'moveToEnd',
|
||||||
'moveToEndOf',
|
'moveToEndOf',
|
||||||
|
@@ -552,7 +552,7 @@ const Transforms = {
|
|||||||
* Remove an existing `mark` to the characters at `range`.
|
* Remove an existing `mark` to the characters at `range`.
|
||||||
*
|
*
|
||||||
* @param {Selection} range
|
* @param {Selection} range
|
||||||
* @param {Mark or String} mark
|
* @param {Mark or String} mark (optional)
|
||||||
* @return {Node} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -575,7 +575,9 @@ const Transforms = {
|
|||||||
let characters = text.characters.map((char, i) => {
|
let characters = text.characters.map((char, i) => {
|
||||||
if (!isInRange(i, text, range)) return char
|
if (!isInRange(i, text, range)) return char
|
||||||
let { marks } = char
|
let { marks } = char
|
||||||
marks = marks.remove(mark)
|
marks = mark
|
||||||
|
? marks.remove(mark)
|
||||||
|
: marks.clear()
|
||||||
return char.merge({ marks })
|
return char.merge({ marks })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -13,8 +13,13 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* The core `onBeforeInput` handler.
|
* The core `onBeforeInput` handler.
|
||||||
*
|
*
|
||||||
* If the current selection is collapsed, we can insert the text natively and
|
* If the current selection is expanded, we have to re-render.
|
||||||
* avoid a re-render, improving performance.
|
*
|
||||||
|
* If the next state resolves a new list of decorations for any of its text
|
||||||
|
* nodes, we have to re-render.
|
||||||
|
*
|
||||||
|
* Otherwise, we can allow the default, native text insertion, avoiding a
|
||||||
|
* re-render for improved performance.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
@@ -23,14 +28,20 @@ export default {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onBeforeInput(e, state, editor) {
|
onBeforeInput(e, state, editor) {
|
||||||
const isNative = state.isCollapsed
|
const transform = state.transform().insertText(e.data)
|
||||||
|
const synthetic = transform.apply()
|
||||||
|
const resolved = editor.resolveState(synthetic)
|
||||||
|
|
||||||
if (!isNative) e.preventDefault()
|
const isSynthenic = (
|
||||||
|
state.isExpanded ||
|
||||||
|
!resolved.equals(synthetic)
|
||||||
|
)
|
||||||
|
|
||||||
return state
|
if (isSynthenic) e.preventDefault()
|
||||||
.transform()
|
|
||||||
.insertText(e.data)
|
return isSynthenic
|
||||||
.apply({ isNative })
|
? synthetic
|
||||||
|
: transform.apply({ isNative: true })
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,9 +137,9 @@ export default {
|
|||||||
|
|
||||||
paste.text
|
paste.text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.forEach((block, i) => {
|
.forEach((line, i) => {
|
||||||
if (i > 0) transform = transform.splitBlock()
|
if (i > 0) transform = transform.splitBlock()
|
||||||
transform = transform.insertText(block)
|
transform = transform.insertText(line)
|
||||||
})
|
})
|
||||||
|
|
||||||
return transform.apply()
|
return transform.apply()
|
||||||
|
@@ -7,10 +7,13 @@
|
|||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"keycode": "^2.1.2",
|
"keycode": "^2.1.2",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"react": "^15.1.0",
|
|
||||||
"ua-parser-js": "^0.7.10",
|
"ua-parser-js": "^0.7.10",
|
||||||
"uid": "0.0.2"
|
"uid": "0.0.2"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^0.14.0 || ^15.0.0",
|
||||||
|
"react-dom": "^0.14.0 || ^15.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.10.1",
|
"babel-cli": "^6.10.1",
|
||||||
"babel-core": "^6.9.1",
|
"babel-core": "^6.9.1",
|
||||||
@@ -25,6 +28,8 @@
|
|||||||
"exorcist": "^0.4.0",
|
"exorcist": "^0.4.0",
|
||||||
"mocha": "^2.5.3",
|
"mocha": "^2.5.3",
|
||||||
"mocha-phantomjs": "^4.0.2",
|
"mocha-phantomjs": "^4.0.2",
|
||||||
|
"prismjs": "^1.5.1",
|
||||||
|
"react": "^15.2.0",
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
"react-portal": "^2.2.0",
|
"react-portal": "^2.2.0",
|
||||||
"react-router": "^2.5.1",
|
"react-router": "^2.5.1",
|
||||||
|
Reference in New Issue
Block a user