mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-09-03 12:12:39 +02:00
add set type, bug fixes
This commit is contained in:
@@ -15,7 +15,15 @@ p {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor p + p {
|
blockquote {
|
||||||
|
border-left: 2px solid #ddd;
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
color: #aaa;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor > * > * + * {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -68,24 +68,39 @@ class App extends React.Component {
|
|||||||
state: Raw.deserialize(state)
|
state: Raw.deserialize(state)
|
||||||
};
|
};
|
||||||
|
|
||||||
isMarkActive(type) {
|
hasMark(type) {
|
||||||
const { state } = this.state
|
const { state } = this.state
|
||||||
const { document, selection } = state
|
const { currentMarks } = state
|
||||||
const marks = document.getMarksAtRange(selection)
|
return currentMarks.some(mark => mark.type == type)
|
||||||
return marks.some(mark => mark.type == type)
|
}
|
||||||
|
|
||||||
|
hasBlock(type) {
|
||||||
|
const { state } = this.state
|
||||||
|
const { currentWrappingNodes } = state
|
||||||
|
return currentWrappingNodes.some(node => node.type == type)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickMark(e, type) {
|
onClickMark(e, type) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
const isActive = this.hasMark(type)
|
||||||
let { state } = this.state
|
let { state } = this.state
|
||||||
const { marks } = state
|
|
||||||
const isActive = this.isMarkActive(type)
|
|
||||||
const mark = Mark.create({ type })
|
|
||||||
|
|
||||||
state = state
|
state = state
|
||||||
.transform()
|
.transform()
|
||||||
[isActive ? 'unmark' : 'mark'](mark)
|
[isActive ? 'unmark' : 'mark'](type)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
this.setState({ state })
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickBlock(e, type) {
|
||||||
|
e.preventDefault()
|
||||||
|
const isActive = this.hasBlock(type)
|
||||||
|
let { state } = this.state
|
||||||
|
|
||||||
|
state = state
|
||||||
|
.transform()
|
||||||
|
.setType(isActive ? 'paragraph' : type)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
this.setState({ state })
|
this.setState({ state })
|
||||||
@@ -101,25 +116,44 @@ class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderToolbar() {
|
renderToolbar() {
|
||||||
const isBold = this.isMarkActive('bold')
|
const isBold = this.hasMark('bold')
|
||||||
const isItalic = this.isMarkActive('italic')
|
const isCode = this.hasMark('code')
|
||||||
const isCode = this.isMarkActive('code')
|
const isItalic = this.hasMark('italic')
|
||||||
|
const isUnderlined = this.hasMark('underlined')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="menu">
|
<div className="menu">
|
||||||
<span className="button" onClick={e => this.onClickMark(e, 'bold')} data-active={isBold}>
|
{this.renderMarkButton('bold', 'format_bold')}
|
||||||
<span className="material-icons">format_bold</span>
|
{this.renderMarkButton('italic', 'format_italic')}
|
||||||
</span>
|
{this.renderMarkButton('underlined', 'format_underlined')}
|
||||||
<span className="button" onClick={e => this.onClickMark(e, 'italic')} data-active={isItalic}>
|
{this.renderMarkButton('code', 'code')}
|
||||||
<span className="material-icons">format_italic</span>
|
{this.renderBlockButton('heading-one', 'looks_one')}
|
||||||
</span>
|
{this.renderBlockButton('heading-two', 'looks_two')}
|
||||||
<span className="button" onClick={e => this.onClickMark(e, 'code')} data-active={isCode}>
|
{this.renderBlockButton('block-quote', 'format_quote')}
|
||||||
<span className="material-icons">code</span>
|
{this.renderBlockButton('numbered-list', 'format_list_numbered')}
|
||||||
</span>
|
{this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderMarkButton(type, icon) {
|
||||||
|
const isActive = this.hasMark(type)
|
||||||
|
return (
|
||||||
|
<span className="button" onClick={e => this.onClickMark(e, type)} data-active={isActive}>
|
||||||
|
<span className="material-icons">{icon}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBlockButton(type, icon) {
|
||||||
|
const isActive = this.hasBlock(type)
|
||||||
|
return (
|
||||||
|
<span className="button" onClick={e => this.onClickBlock(e, type)} data-active={isActive}>
|
||||||
|
<span className="material-icons">{icon}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
renderEditor() {
|
renderEditor() {
|
||||||
return (
|
return (
|
||||||
<div className="editor">
|
<div className="editor">
|
||||||
@@ -142,10 +176,26 @@ class App extends React.Component {
|
|||||||
|
|
||||||
renderNode(node) {
|
renderNode(node) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'paragraph': {
|
case 'block-quote': {
|
||||||
return (props) => {
|
return (props) => <blockquote>{props.children}</blockquote>
|
||||||
return <p>{props.children}</p>
|
|
||||||
}
|
}
|
||||||
|
case 'bulleted-list': {
|
||||||
|
return (props) => <ul>{props.chidlren}</ul>
|
||||||
|
}
|
||||||
|
case 'heading-one': {
|
||||||
|
return (props) => <h1>{props.children}</h1>
|
||||||
|
}
|
||||||
|
case 'heading-two': {
|
||||||
|
return (props) => <h2>{props.children}</h2>
|
||||||
|
}
|
||||||
|
case 'list-item': {
|
||||||
|
return (props) => <li>{props.chidlren}</li>
|
||||||
|
}
|
||||||
|
case 'numbered-list': {
|
||||||
|
return (props) => <ol>{props.children}</ol>
|
||||||
|
}
|
||||||
|
case 'paragraph': {
|
||||||
|
return (props) => <p>{props.children}</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,11 +207,6 @@ class App extends React.Component {
|
|||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'italic': {
|
|
||||||
return {
|
|
||||||
fontStyle: 'italic'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'code': {
|
case 'code': {
|
||||||
return {
|
return {
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
@@ -170,6 +215,16 @@ class App extends React.Component {
|
|||||||
borderRadius: '4px'
|
borderRadius: '4px'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'italic': {
|
||||||
|
return {
|
||||||
|
fontStyle: 'italic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'underlined': {
|
||||||
|
return {
|
||||||
|
textDecoration: 'underline'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -110,9 +110,8 @@ class Leaf extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={style}
|
|
||||||
data-offset-key={offsetKey}
|
data-offset-key={offsetKey}
|
||||||
data-type='leaf'
|
style={style}
|
||||||
>
|
>
|
||||||
{text || <br/>}
|
{text || <br/>}
|
||||||
</span>
|
</span>
|
||||||
|
@@ -20,11 +20,7 @@ class Text extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { node } = this.props
|
const { node } = this.props
|
||||||
return (
|
return (
|
||||||
<span
|
<span>
|
||||||
key={node.key}
|
|
||||||
data-key={node.key}
|
|
||||||
data-type='text'
|
|
||||||
>
|
|
||||||
{this.renderLeaves()}
|
{this.renderLeaves()}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
@@ -4,7 +4,7 @@ import Element from './element'
|
|||||||
import Mark from './mark'
|
import Mark from './mark'
|
||||||
import Selection from './selection'
|
import Selection from './selection'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import { List, OrderedMap, Set } from 'immutable'
|
import { List, OrderedMap, OrderedSet, Set } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node.
|
* Node.
|
||||||
@@ -260,8 +260,8 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getLastTextNode() {
|
getLastTextNode() {
|
||||||
const texts = this.findNode(node => node.type == 'text')
|
const texts = this.filterNodes(node => node.type == 'text')
|
||||||
return texts.size ? texts.get(texts.size - 1) : null
|
return texts.last() || null
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,8 +319,8 @@ const Node = {
|
|||||||
/**
|
/**
|
||||||
* Get the child text node at an `offset`.
|
* Get the child text node at an `offset`.
|
||||||
*
|
*
|
||||||
* @param {String} offset
|
* @param {String or Node} key
|
||||||
* @return {Node or Null}
|
* @return {Number} offset
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getNodeOffset(key) {
|
getNodeOffset(key) {
|
||||||
@@ -338,7 +338,9 @@ const Node = {
|
|||||||
const befores = this.nodes.takeUntil(node => node.key == child.key)
|
const befores = this.nodes.takeUntil(node => node.key == child.key)
|
||||||
|
|
||||||
// Calculate the offset of the nodes before the matching child.
|
// Calculate the offset of the nodes before the matching child.
|
||||||
const offset = befores.map(child => child.length)
|
const offset = befores.reduce((offset, child) => {
|
||||||
|
return offset + child.length
|
||||||
|
}, 0)
|
||||||
|
|
||||||
// If the child's parent is this node, return the offset of all of the nodes
|
// If the child's parent is this node, return the offset of all of the nodes
|
||||||
// before it, otherwise recurse.
|
// before it, otherwise recurse.
|
||||||
@@ -413,8 +415,8 @@ const Node = {
|
|||||||
focusOffset: 0
|
focusOffset: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const texts = this.getTextNodesAtRange()
|
const texts = this.getTextNodesAtRange(range)
|
||||||
const previous = texts.get(text.size - 2)
|
const previous = texts.get(texts.size - 2)
|
||||||
return previous
|
return previous
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -460,54 +462,6 @@ const Node = {
|
|||||||
return match
|
return match
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the child text nodes after an `offset`.
|
|
||||||
*
|
|
||||||
* @param {String} offset
|
|
||||||
* @return {OrderedMap} matches
|
|
||||||
*/
|
|
||||||
|
|
||||||
getTextNodesAfterOffset(offset) {
|
|
||||||
let matches = new OrderedMap()
|
|
||||||
let i
|
|
||||||
|
|
||||||
this.nodes.forEach((child) => {
|
|
||||||
if (child.length <= offset + i) return
|
|
||||||
|
|
||||||
matches = child.type == 'text'
|
|
||||||
? matches.set(child.key, child)
|
|
||||||
: matches.concat(child.getTextNodesAfterOffset(offset - i))
|
|
||||||
|
|
||||||
i += child.length
|
|
||||||
})
|
|
||||||
|
|
||||||
return matches
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the child text nodes before an `offset`.
|
|
||||||
*
|
|
||||||
* @param {String} offset
|
|
||||||
* @return {OrderedMap} matches
|
|
||||||
*/
|
|
||||||
|
|
||||||
getTextNodesBeforeOffset(offset) {
|
|
||||||
let matches = new OrderedMap()
|
|
||||||
let i
|
|
||||||
|
|
||||||
this.nodes.forEach((child) => {
|
|
||||||
if (child.length > offset + i) return
|
|
||||||
|
|
||||||
matches = child.type == 'text'
|
|
||||||
? matches.set(child.key, child)
|
|
||||||
: matches.concat(child.getTextNodesBeforeOffset(offset - i))
|
|
||||||
|
|
||||||
i += child.length
|
|
||||||
})
|
|
||||||
|
|
||||||
return matches
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all of the text nodes in a `range`.
|
* Get all of the text nodes in a `range`.
|
||||||
*
|
*
|
||||||
@@ -522,17 +476,30 @@ const Node = {
|
|||||||
this.assertHasNode(startKey)
|
this.assertHasNode(startKey)
|
||||||
this.assertHasNode(endKey)
|
this.assertHasNode(endKey)
|
||||||
|
|
||||||
// Convert the start and end nodes to offsets.
|
|
||||||
const startNode = this.getNode(startKey)
|
|
||||||
const endNode = this.getNode(endKey)
|
|
||||||
const startOffset = this.getNodeOffset(startNode)
|
|
||||||
const endOffset = this.getNodeOffset(endNode)
|
|
||||||
|
|
||||||
// Return the text nodes after the start offset and before the end offset.
|
// Return the text nodes after the start offset and before the end offset.
|
||||||
const afterStart = this.getTextNodesAfterOffset(startOffset)
|
const endNode = this.getNode(endKey)
|
||||||
const beforeEnd = this.getTextNodesBeforeOffset(endOffset)
|
const texts = this.filterNodes(node => node.type == 'text')
|
||||||
const between = afterStart.filter(node => beforeEnd.includes(node))
|
const afterStart = texts.skipUntil(node => node.key == startKey)
|
||||||
return between
|
const upToEnd = afterStart.takeUntil(node => node.key == endKey)
|
||||||
|
let matches = upToEnd.set(endNode.key, endNode)
|
||||||
|
return matches
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all of the wrapping nodes in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {OrderedMap} nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
getWrappingNodesAtRange(range) {
|
||||||
|
const node = this
|
||||||
|
const texts = node.getTextNodesAtRange(range)
|
||||||
|
const parents = texts.map((text) => {
|
||||||
|
return node.nodes.includes(text) ? node : node.getParentNode(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
return parents
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -718,6 +685,37 @@ const Node = {
|
|||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the direct parent of text nodes in a range to `type`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
setTypeAtRange(range, type) {
|
||||||
|
let node = this
|
||||||
|
const texts = node.getTextNodesAtRange(range)
|
||||||
|
let parents = new OrderedSet()
|
||||||
|
|
||||||
|
// Find the direct parent of each text node.
|
||||||
|
texts.forEach((text) => {
|
||||||
|
const parent = node.has(text.key) ? node : node.getParentNode(text)
|
||||||
|
parents = parents.add(parent)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the new type for each parent.
|
||||||
|
parents = parents.forEach((parent) => {
|
||||||
|
if (parent == node) {
|
||||||
|
node = node.merge({ type })
|
||||||
|
} else {
|
||||||
|
parent = parent.merge({ type })
|
||||||
|
node = node.updateNode(parent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split the nodes at a `range`.
|
* Split the nodes at a `range`.
|
||||||
*
|
*
|
||||||
@@ -844,8 +842,39 @@ const Node = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap all of the nodes in a `range` in a new `parent` node.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @param {Node or String} parent
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
wrapAtRange(range, parent) {
|
||||||
|
|
||||||
|
// Allow for the parent to by just a type.
|
||||||
|
if (typeof parent == 'string') {
|
||||||
|
parent = Element.create({ type: parent })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the child to the parent's nodes.
|
||||||
|
const child = this.findNode(key)
|
||||||
|
parent = node.nodes.set(child.key, child)
|
||||||
|
|
||||||
|
// Remove the child from this node.
|
||||||
|
node = node.removeNode(child)
|
||||||
|
|
||||||
|
// Add the parent to this node.
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwrap the node
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -59,7 +59,7 @@ class State extends Record(DEFAULTS) {
|
|||||||
* @return {List} characters
|
* @return {List} characters
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get characters() {
|
get currentCharacters() {
|
||||||
const { document, selection } = this
|
const { document, selection } = this
|
||||||
return document.getCharactersAtRange(selection)
|
return document.getCharactersAtRange(selection)
|
||||||
}
|
}
|
||||||
@@ -70,18 +70,29 @@ class State extends Record(DEFAULTS) {
|
|||||||
* @return {Set} marks
|
* @return {Set} marks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get marks() {
|
get currentMarks() {
|
||||||
const { document, selection } = this
|
const { document, selection } = this
|
||||||
return document.getMarksAtRange(selection)
|
return document.getMarksAtRange(selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the wrapping nodes in the current selection.
|
||||||
|
*
|
||||||
|
* @return {OrderedMap} nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
get currentWrappingNodes() {
|
||||||
|
const { document, selection, textNodes } = this
|
||||||
|
return document.getWrappingNodesAtRange(selection)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the text nodes in the current selection.
|
* Get the text nodes in the current selection.
|
||||||
*
|
*
|
||||||
* @return {OrderedMap} nodes
|
* @return {OrderedMap} nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get textNodes() {
|
get currentTextNodes() {
|
||||||
const { document, selection } = this
|
const { document, selection } = this
|
||||||
return document.getTextNodesAtRange(selection)
|
return document.getTextNodesAtRange(selection)
|
||||||
}
|
}
|
||||||
@@ -147,7 +158,7 @@ class State extends Record(DEFAULTS) {
|
|||||||
after = selection.moveToEndOf(previous)
|
after = selection.moveToEndOf(previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (!selection.isAtEndOf(document)) {
|
else {
|
||||||
after = selection.moveBackward(n)
|
after = selection.moveBackward(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +226,21 @@ class State extends Record(DEFAULTS) {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the nodes in the current selection to `type`.
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
setType(type) {
|
||||||
|
let state = this
|
||||||
|
let { document, selection } = state
|
||||||
|
document = document.setTypeAtRange(selection, type)
|
||||||
|
state = state.merge({ document })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split the node at the current selection.
|
* Split the node at the current selection.
|
||||||
*
|
*
|
||||||
|
@@ -46,6 +46,8 @@ const TRANSFORM_TYPES = [
|
|||||||
'insertTextAtRange',
|
'insertTextAtRange',
|
||||||
'mark',
|
'mark',
|
||||||
'markAtRange',
|
'markAtRange',
|
||||||
|
'setType',
|
||||||
|
'setTypeAtRange',
|
||||||
'split',
|
'split',
|
||||||
'splitAtRange',
|
'splitAtRange',
|
||||||
'unmark',
|
'unmark',
|
||||||
|
Reference in New Issue
Block a user