mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-09-01 03:11:44 +02:00
add rendering of decorators from schema
This commit is contained in:
@@ -55,7 +55,39 @@ function CodeBlock(props) {
|
||||
|
||||
const schema = {
|
||||
nodes: {
|
||||
code: CodeBlock
|
||||
code: {
|
||||
component: CodeBlock,
|
||||
decorator: (block, text) => {
|
||||
let characters = text.characters.asMutable()
|
||||
const language = block.data.get('language')
|
||||
const string = text.text
|
||||
const grammar = Prism.languages[language]
|
||||
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.asImmutable()
|
||||
}
|
||||
}
|
||||
},
|
||||
marks: {
|
||||
'highlight-comment': {
|
||||
@@ -131,7 +163,6 @@ class CodeHighlighting extends React.Component {
|
||||
<Editor
|
||||
schema={schema}
|
||||
state={this.state.state}
|
||||
renderDecorations={this.renderDecorations}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
@@ -139,47 +170,6 @@ class CodeHighlighting extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render decorations on `text` nodes inside code blocks.
|
||||
*
|
||||
* @param {Text} text
|
||||
* @param {Block} block
|
||||
* @return {Characters}
|
||||
*/
|
||||
|
||||
renderDecorations = (text, block) => {
|
||||
if (block.type != 'code') return text.characters
|
||||
|
||||
let characters = text.characters.asMutable()
|
||||
const language = block.data.get('language')
|
||||
const string = text.text
|
||||
const grammar = Prism.languages[language]
|
||||
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.asImmutable()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -55,7 +55,6 @@ class Content extends React.Component {
|
||||
onPaste: React.PropTypes.func.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
readOnly: React.PropTypes.bool.isRequired,
|
||||
renderDecorations: React.PropTypes.func.isRequired,
|
||||
schema: React.PropTypes.object,
|
||||
spellCheck: React.PropTypes.bool.isRequired,
|
||||
state: React.PropTypes.object.isRequired,
|
||||
@@ -141,27 +140,18 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
getPoint(element, offset) {
|
||||
const { state, editor } = this.props
|
||||
const { document } = state
|
||||
const schema = editor.getSchema()
|
||||
const offsetKey = OffsetKey.findKey(element, offset)
|
||||
const ranges = this.getRanges(offsetKey.key)
|
||||
const { key } = offsetKey
|
||||
const node = document.getDescendant(key)
|
||||
const decorators = document.getDescendantDecorators(key, schema)
|
||||
const ranges = node.getRanges(decorators)
|
||||
const point = OffsetKey.findPoint(offsetKey, ranges)
|
||||
return point
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ranges for a text node by `key`.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {List}
|
||||
*/
|
||||
|
||||
getRanges(key) {
|
||||
const { state, renderDecorations } = this.props
|
||||
const node = state.document.getDescendant(key)
|
||||
const block = state.document.getClosestBlock(node)
|
||||
const ranges = node.getDecoratedRanges(block, renderDecorations)
|
||||
return ranges
|
||||
}
|
||||
|
||||
/**
|
||||
* On before input, bubble up.
|
||||
*
|
||||
@@ -372,7 +362,7 @@ class Content extends React.Component {
|
||||
e.preventDefault()
|
||||
|
||||
const window = getWindow(e.target)
|
||||
const { state, renderDecorations } = this.props
|
||||
const { state } = this.props
|
||||
const { selection } = state
|
||||
const { dataTransfer, x, y } = e.nativeEvent
|
||||
const transfer = new Transfer(dataTransfer)
|
||||
@@ -429,14 +419,23 @@ class Content extends React.Component {
|
||||
debug('onInput')
|
||||
|
||||
const window = getWindow(e.target)
|
||||
let { state, renderDecorations } = this.props
|
||||
const { selection } = state
|
||||
|
||||
// Get the selection point.
|
||||
const native = window.getSelection()
|
||||
const { anchorNode, anchorOffset, focusOffset } = native
|
||||
const point = this.getPoint(anchorNode, anchorOffset)
|
||||
const { key, index, start, end } = point
|
||||
const ranges = this.getRanges(key)
|
||||
|
||||
// Get the range in question.
|
||||
const { state, editor } = this.props
|
||||
const { document, selection } = state
|
||||
const schema = editor.getSchema()
|
||||
const decorators = document.getDescendantDecorators(key, schema)
|
||||
const node = document.getDescendant(key)
|
||||
const ranges = node.getRanges(decorators)
|
||||
const range = ranges.get(index)
|
||||
|
||||
// Get the text information.
|
||||
const isLast = index == ranges.size - 1
|
||||
const { text, marks } = range
|
||||
let { textContent } = anchorNode
|
||||
@@ -457,7 +456,7 @@ class Content extends React.Component {
|
||||
const after = selection.collapseToEnd().moveForward(delta)
|
||||
|
||||
// Create an updated state with the text replaced.
|
||||
state = state
|
||||
const next = state
|
||||
.transform()
|
||||
.moveTo({
|
||||
anchorKey: key,
|
||||
@@ -471,7 +470,7 @@ class Content extends React.Component {
|
||||
.apply()
|
||||
|
||||
// Change the current state.
|
||||
this.onChange(state)
|
||||
this.onChange(next)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -562,7 +561,7 @@ class Content extends React.Component {
|
||||
if (isNonEditable(e)) return
|
||||
|
||||
const window = getWindow(e.target)
|
||||
const { state, renderDecorations } = this.props
|
||||
const { state } = this.props
|
||||
let { document, selection } = state
|
||||
const native = window.getSelection()
|
||||
const data = {}
|
||||
@@ -683,7 +682,7 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
renderNode = (node) => {
|
||||
const { editor, renderDecorations, schema, state } = this.props
|
||||
const { editor, schema, state } = this.props
|
||||
|
||||
return (
|
||||
<Node
|
||||
@@ -692,7 +691,6 @@ class Content extends React.Component {
|
||||
schema={schema}
|
||||
state={state}
|
||||
editor={editor}
|
||||
renderDecorations={renderDecorations}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@@ -62,7 +62,6 @@ class Editor extends React.Component {
|
||||
placeholderStyle: React.PropTypes.object,
|
||||
plugins: React.PropTypes.array,
|
||||
readOnly: React.PropTypes.bool,
|
||||
renderDecorations: React.PropTypes.func,
|
||||
schema: React.PropTypes.object,
|
||||
spellCheck: React.PropTypes.bool,
|
||||
state: React.PropTypes.object.isRequired,
|
||||
@@ -264,7 +263,6 @@ class Editor extends React.Component {
|
||||
editor={this}
|
||||
onChange={this.onChange}
|
||||
readOnly={this.props.readOnly}
|
||||
renderDecorations={this.renderDecorations}
|
||||
schema={this.state.schema}
|
||||
spellCheck={this.props.spellCheck}
|
||||
state={this.state.state}
|
||||
@@ -273,24 +271,6 @@ class Editor extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the decorations for a `text`, cascading through the plugins.
|
||||
*
|
||||
* @param {Block} text
|
||||
* @param {Block} block
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
renderDecorations = (text, block) => {
|
||||
for (const plugin of this.state.plugins) {
|
||||
if (!plugin.renderDecorations) continue
|
||||
const style = plugin.renderDecorations(text, block, this.state.state, this)
|
||||
if (style) return style
|
||||
}
|
||||
|
||||
return text.characters
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the editor's current plugins from `props` when they change.
|
||||
*
|
||||
|
@@ -31,13 +31,11 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
static propTypes = {
|
||||
block: React.PropTypes.object,
|
||||
editor: React.PropTypes.object.isRequired,
|
||||
node: React.PropTypes.object.isRequired,
|
||||
renderDecorations: React.PropTypes.func.isRequired,
|
||||
schema: React.PropTypes.object.isRequired,
|
||||
state: React.PropTypes.object.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -122,8 +120,8 @@ class Node extends React.Component {
|
||||
props.node.kind == 'text' &&
|
||||
props.block != this.props.block
|
||||
) {
|
||||
const nextRanges = props.node.getDecoratedRanges(props.block, props.renderDecorations)
|
||||
const ranges = this.props.node.getDecoratedRanges(this.props.block, this.props.renderDecorations)
|
||||
const nextRanges = props.node.getRanges(props.decorators)
|
||||
const ranges = this.props.node.getRanges(this.props.decorators)
|
||||
if (!ranges.equals(nextRanges)) return true
|
||||
}
|
||||
|
||||
@@ -213,17 +211,13 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
renderNode = (child) => {
|
||||
const { editor, node, renderDecorations, schema, state } = this.props
|
||||
const block = node.kind == 'block' ? node : this.props.block
|
||||
return (
|
||||
<Node
|
||||
key={child.key}
|
||||
block={block}
|
||||
node={child}
|
||||
schema={schema}
|
||||
state={state}
|
||||
editor={editor}
|
||||
renderDecorations={renderDecorations}
|
||||
editor={this.props.editor}
|
||||
schema={this.props.schema}
|
||||
state={this.props.state}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -279,8 +273,10 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
renderText = () => {
|
||||
const { node, block, renderDecorations } = this.props
|
||||
const ranges = node.getDecoratedRanges(block, renderDecorations)
|
||||
const { node, schema, state } = this.props
|
||||
const { document } = state
|
||||
const decorators = document.getDescendantDecorators(node.key, schema)
|
||||
const ranges = node.getRanges(decorators)
|
||||
let offset = 0
|
||||
|
||||
const leaves = ranges.map((range, i, original) => {
|
||||
|
@@ -349,6 +349,39 @@ const Node = {
|
||||
return schema.__getComponent(this)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the decorations for the node from a `schema`.
|
||||
*
|
||||
* @param {Schema} schema
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
getDecorators(schema) {
|
||||
return schema.__getDecorators(this)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the decorations for a descendant by `key` given a `schema`.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Schema} schema
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
getDescendantDecorators(key, schema) {
|
||||
const descendant = this.assertDescendant(key)
|
||||
let child = this.getHighestChild(key)
|
||||
let decorators = []
|
||||
|
||||
while (child != descendant) {
|
||||
decorators = decorators.concat(child.getDecorators(schema))
|
||||
child = child.getHighestChild(key)
|
||||
}
|
||||
|
||||
decorators = decorators.concat(descendant.getDecorators(schema))
|
||||
return decorators
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a descendant node by `key`.
|
||||
*
|
||||
@@ -1104,9 +1137,9 @@ const Node = {
|
||||
memoize(Node, [
|
||||
'assertChild',
|
||||
'assertDescendant',
|
||||
'findDescendant',
|
||||
'filterDescendants',
|
||||
'filterDescendantsDeep',
|
||||
'findDescendant',
|
||||
'getBlocks',
|
||||
'getBlocksAtRange',
|
||||
'getCharactersAtRange',
|
||||
@@ -1121,8 +1154,10 @@ memoize(Node, [
|
||||
'getClosestBlock',
|
||||
'getClosestInline',
|
||||
'getComponent',
|
||||
'getDescendant',
|
||||
'getDecorators',
|
||||
'getDepth',
|
||||
'getDescendant',
|
||||
'getDescendantDecorators',
|
||||
'getFragmentAtRange',
|
||||
'getFurthest',
|
||||
'getFurthestBlock',
|
||||
@@ -1137,9 +1172,9 @@ memoize(Node, [
|
||||
'getOffset',
|
||||
'getOffsetAtRange',
|
||||
'getParent',
|
||||
'getPreviousBlock',
|
||||
'getPreviousSibling',
|
||||
'getPreviousText',
|
||||
'getPreviousBlock',
|
||||
'getTextAtOffset',
|
||||
'getTextDirection',
|
||||
'getTexts',
|
||||
|
@@ -155,6 +155,27 @@ class Schema extends new Record(DEFAULTS) {
|
||||
return match.component
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the decorators for an `object`.
|
||||
*
|
||||
* This method is private, because it should always be called on one of the
|
||||
* often-changing immutable objects instead, since it will be memoized for
|
||||
* much better performance.
|
||||
*
|
||||
* @param {Mixed} object
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
__getDecorators(object) {
|
||||
return this.rules
|
||||
.filter(rule => rule.match(object) && rule.decorator)
|
||||
.map((rule) => {
|
||||
return (text) => {
|
||||
return rule.decorator(object, text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an `object` against the schema, returning the failing rule and
|
||||
* reason if the object is invalid, or void if it's valid.
|
||||
|
@@ -99,84 +99,85 @@ class Text extends new Record(DEFAULTS) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decorated characters.
|
||||
* Derive a set of decorated characters with `decorators`.
|
||||
*
|
||||
* @param {Block} block
|
||||
* @param {Function} decorator
|
||||
* @return {List} characters
|
||||
* @param {Array} decorators
|
||||
* @return {List}
|
||||
*/
|
||||
|
||||
getDecoratedCharacters(block, decorator) {
|
||||
return decorator(this, block)
|
||||
getDecorations(decorators) {
|
||||
const node = this
|
||||
let { characters } = node
|
||||
if (characters.size == 0) return characters
|
||||
|
||||
for (const decorator of decorators) {
|
||||
const decorateds = decorator(node)
|
||||
characters = characters.merge(decorateds)
|
||||
}
|
||||
|
||||
return characters
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decorated characters grouped by marks.
|
||||
* Get the decorations for the node from a `schema`.
|
||||
*
|
||||
* @param {Block} block
|
||||
* @param {Function} decorator
|
||||
* @return {List} ranges
|
||||
* @param {Schema} schema
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
getDecoratedRanges(block, decorator) {
|
||||
const decorations = this.getDecoratedCharacters(block, decorator)
|
||||
return this.getRangesForCharacters(decorations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the characters grouped by marks.
|
||||
*
|
||||
* @return {List} ranges
|
||||
*/
|
||||
|
||||
getRanges() {
|
||||
return this.getRangesForCharacters(this.characters)
|
||||
getDecorators(schema) {
|
||||
return schema.__getDecorators(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the ranges for a list of `characters`.
|
||||
*
|
||||
* @param {List} characters
|
||||
* @param {Array || Void} decorators (optional)
|
||||
* @return {List}
|
||||
*/
|
||||
|
||||
getRangesForCharacters(characters) {
|
||||
getRanges(decorators = []) {
|
||||
const node = this
|
||||
const list = new List()
|
||||
let characters = this.getDecorations(decorators)
|
||||
|
||||
// If there are no characters, return one empty range.
|
||||
if (characters.size == 0) {
|
||||
let ranges = new List()
|
||||
ranges = ranges.push(new Range())
|
||||
return ranges
|
||||
return list.push(new Range())
|
||||
}
|
||||
|
||||
return characters
|
||||
.toList()
|
||||
.reduce((ranges, char, i) => {
|
||||
const { marks, text } = char
|
||||
// Convert the now-decorated characters into ranges.
|
||||
const ranges = characters.reduce((memo, char, i) => {
|
||||
const { marks, text } = char
|
||||
|
||||
// The first one can always just be created.
|
||||
if (i == 0) {
|
||||
return ranges.push(new Range({ text, marks }))
|
||||
}
|
||||
// The first one can always just be created.
|
||||
if (i == 0) {
|
||||
return memo.push(new Range({ text, marks }))
|
||||
}
|
||||
|
||||
// Otherwise, compare to the previous and see if a new range should be
|
||||
// created, or whether the text should be added to the previous range.
|
||||
const previous = characters.get(i - 1)
|
||||
const prevMarks = previous.marks
|
||||
const added = marks.filterNot(mark => prevMarks.includes(mark))
|
||||
const removed = prevMarks.filterNot(mark => marks.includes(mark))
|
||||
const isSame = !added.size && !removed.size
|
||||
// Otherwise, compare to the previous and see if a new range should be
|
||||
// created, or whether the text should be added to the previous range.
|
||||
const previous = characters.get(i - 1)
|
||||
const prevMarks = previous.marks
|
||||
const added = marks.filterNot(mark => prevMarks.includes(mark))
|
||||
const removed = prevMarks.filterNot(mark => marks.includes(mark))
|
||||
const isSame = !added.size && !removed.size
|
||||
|
||||
// If the marks are the same, add the text to the previous range.
|
||||
if (isSame) {
|
||||
const index = ranges.size - 1
|
||||
let prevRange = ranges.get(index)
|
||||
let prevText = prevRange.get('text')
|
||||
prevRange = prevRange.set('text', prevText += text)
|
||||
return ranges.set(index, prevRange)
|
||||
}
|
||||
// If the marks are the same, add the text to the previous range.
|
||||
if (isSame) {
|
||||
const index = memo.size - 1
|
||||
let prevRange = memo.get(index)
|
||||
let prevText = prevRange.get('text')
|
||||
prevRange = prevRange.set('text', prevText += text)
|
||||
return memo.set(index, prevRange)
|
||||
}
|
||||
|
||||
// Otherwise, create a new range.
|
||||
return ranges.push(new Range({ text, marks }))
|
||||
}, new List())
|
||||
// Otherwise, create a new range.
|
||||
return memo.push(new Range({ text, marks }))
|
||||
}, list)
|
||||
|
||||
// Return the ranges.
|
||||
return ranges
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,11 +247,10 @@ class Text extends new Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
memoize(Text.prototype, [
|
||||
'getDecoratedCharacters',
|
||||
'getDecoratedRanges',
|
||||
'getDecorations',
|
||||
'getDecorators',
|
||||
'getRanges',
|
||||
'getRangesForCharacters',
|
||||
'validate'
|
||||
'validate',
|
||||
])
|
||||
|
||||
/**
|
||||
|
@@ -160,11 +160,12 @@ function Plugin(options = {}) {
|
||||
*/
|
||||
|
||||
function onBeforeInput(e, data, state, editor) {
|
||||
const { renderDecorations } = editor
|
||||
const { startOffset, startText, startBlock } = state
|
||||
const { document, startKey, startOffset, startText } = state
|
||||
|
||||
// Determine what the characters would be if natively inserted.
|
||||
const prevChars = startText.getDecoratedCharacters(startBlock, renderDecorations)
|
||||
const schema = editor.getSchema()
|
||||
const decorators = document.getDescendantDecorators(startKey, schema)
|
||||
const prevChars = startText.getDecorations(decorators)
|
||||
const prevChar = prevChars.get(startOffset - 1)
|
||||
const char = Character.create({
|
||||
text: e.data,
|
||||
@@ -183,8 +184,7 @@ function Plugin(options = {}) {
|
||||
.apply()
|
||||
|
||||
const nextText = next.startText
|
||||
const nextBlock = next.startBlock
|
||||
const nextChars = nextText.getDecoratedCharacters(nextBlock, renderDecorations)
|
||||
const nextChars = nextText.getDecorations(decorators)
|
||||
|
||||
// We do not have to re-render if the current selection is collapsed, the
|
||||
// current node is not empty, there are no marks on the cursor, and the
|
||||
|
Reference in New Issue
Block a user