mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-30 18:39:51 +02:00
add set type, bug fixes
This commit is contained in:
@@ -15,7 +15,15 @@ p {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -68,24 +68,39 @@ class App extends React.Component {
|
||||
state: Raw.deserialize(state)
|
||||
};
|
||||
|
||||
isMarkActive(type) {
|
||||
hasMark(type) {
|
||||
const { state } = this.state
|
||||
const { document, selection } = state
|
||||
const marks = document.getMarksAtRange(selection)
|
||||
return marks.some(mark => mark.type == type)
|
||||
const { currentMarks } = state
|
||||
return currentMarks.some(mark => mark.type == type)
|
||||
}
|
||||
|
||||
hasBlock(type) {
|
||||
const { state } = this.state
|
||||
const { currentWrappingNodes } = state
|
||||
return currentWrappingNodes.some(node => node.type == type)
|
||||
}
|
||||
|
||||
onClickMark(e, type) {
|
||||
e.preventDefault()
|
||||
|
||||
const isActive = this.hasMark(type)
|
||||
let { state } = this.state
|
||||
const { marks } = state
|
||||
const isActive = this.isMarkActive(type)
|
||||
const mark = Mark.create({ type })
|
||||
|
||||
state = state
|
||||
.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()
|
||||
|
||||
this.setState({ state })
|
||||
@@ -101,25 +116,44 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
renderToolbar() {
|
||||
const isBold = this.isMarkActive('bold')
|
||||
const isItalic = this.isMarkActive('italic')
|
||||
const isCode = this.isMarkActive('code')
|
||||
const isBold = this.hasMark('bold')
|
||||
const isCode = this.hasMark('code')
|
||||
const isItalic = this.hasMark('italic')
|
||||
const isUnderlined = this.hasMark('underlined')
|
||||
|
||||
return (
|
||||
<div className="menu">
|
||||
<span className="button" onClick={e => this.onClickMark(e, 'bold')} data-active={isBold}>
|
||||
<span className="material-icons">format_bold</span>
|
||||
</span>
|
||||
<span className="button" onClick={e => this.onClickMark(e, 'italic')} data-active={isItalic}>
|
||||
<span className="material-icons">format_italic</span>
|
||||
</span>
|
||||
<span className="button" onClick={e => this.onClickMark(e, 'code')} data-active={isCode}>
|
||||
<span className="material-icons">code</span>
|
||||
</span>
|
||||
{this.renderMarkButton('bold', 'format_bold')}
|
||||
{this.renderMarkButton('italic', 'format_italic')}
|
||||
{this.renderMarkButton('underlined', 'format_underlined')}
|
||||
{this.renderMarkButton('code', 'code')}
|
||||
{this.renderBlockButton('heading-one', 'looks_one')}
|
||||
{this.renderBlockButton('heading-two', 'looks_two')}
|
||||
{this.renderBlockButton('block-quote', 'format_quote')}
|
||||
{this.renderBlockButton('numbered-list', 'format_list_numbered')}
|
||||
{this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
|
||||
</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() {
|
||||
return (
|
||||
<div className="editor">
|
||||
@@ -142,10 +176,26 @@ class App extends React.Component {
|
||||
|
||||
renderNode(node) {
|
||||
switch (node.type) {
|
||||
case 'block-quote': {
|
||||
return (props) => <blockquote>{props.children}</blockquote>
|
||||
}
|
||||
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) => {
|
||||
return <p>{props.children}</p>
|
||||
}
|
||||
return (props) => <p>{props.children}</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,11 +207,6 @@ class App extends React.Component {
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
case 'italic': {
|
||||
return {
|
||||
fontStyle: 'italic'
|
||||
}
|
||||
}
|
||||
case 'code': {
|
||||
return {
|
||||
fontFamily: 'monospace',
|
||||
@@ -170,6 +215,16 @@ class App extends React.Component {
|
||||
borderRadius: '4px'
|
||||
}
|
||||
}
|
||||
case 'italic': {
|
||||
return {
|
||||
fontStyle: 'italic'
|
||||
}
|
||||
}
|
||||
case 'underlined': {
|
||||
return {
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -110,9 +110,8 @@ class Leaf extends React.Component {
|
||||
|
||||
return (
|
||||
<span
|
||||
style={style}
|
||||
data-offset-key={offsetKey}
|
||||
data-type='leaf'
|
||||
style={style}
|
||||
>
|
||||
{text || <br/>}
|
||||
</span>
|
||||
|
@@ -20,11 +20,7 @@ class Text extends React.Component {
|
||||
render() {
|
||||
const { node } = this.props
|
||||
return (
|
||||
<span
|
||||
key={node.key}
|
||||
data-key={node.key}
|
||||
data-type='text'
|
||||
>
|
||||
<span>
|
||||
{this.renderLeaves()}
|
||||
</span>
|
||||
)
|
||||
|
@@ -4,7 +4,7 @@ import Element from './element'
|
||||
import Mark from './mark'
|
||||
import Selection from './selection'
|
||||
import Text from './text'
|
||||
import { List, OrderedMap, Set } from 'immutable'
|
||||
import { List, OrderedMap, OrderedSet, Set } from 'immutable'
|
||||
|
||||
/**
|
||||
* Node.
|
||||
@@ -260,8 +260,8 @@ const Node = {
|
||||
*/
|
||||
|
||||
getLastTextNode() {
|
||||
const texts = this.findNode(node => node.type == 'text')
|
||||
return texts.size ? texts.get(texts.size - 1) : null
|
||||
const texts = this.filterNodes(node => node.type == 'text')
|
||||
return texts.last() || null
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -319,8 +319,8 @@ const Node = {
|
||||
/**
|
||||
* Get the child text node at an `offset`.
|
||||
*
|
||||
* @param {String} offset
|
||||
* @return {Node or Null}
|
||||
* @param {String or Node} key
|
||||
* @return {Number} offset
|
||||
*/
|
||||
|
||||
getNodeOffset(key) {
|
||||
@@ -338,7 +338,9 @@ const Node = {
|
||||
const befores = this.nodes.takeUntil(node => node.key == child.key)
|
||||
|
||||
// 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
|
||||
// before it, otherwise recurse.
|
||||
@@ -413,8 +415,8 @@ const Node = {
|
||||
focusOffset: 0
|
||||
})
|
||||
|
||||
const texts = this.getTextNodesAtRange()
|
||||
const previous = texts.get(text.size - 2)
|
||||
const texts = this.getTextNodesAtRange(range)
|
||||
const previous = texts.get(texts.size - 2)
|
||||
return previous
|
||||
},
|
||||
|
||||
@@ -460,54 +462,6 @@ const Node = {
|
||||
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`.
|
||||
*
|
||||
@@ -522,17 +476,30 @@ const Node = {
|
||||
this.assertHasNode(startKey)
|
||||
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.
|
||||
const afterStart = this.getTextNodesAfterOffset(startOffset)
|
||||
const beforeEnd = this.getTextNodesBeforeOffset(endOffset)
|
||||
const between = afterStart.filter(node => beforeEnd.includes(node))
|
||||
return between
|
||||
const endNode = this.getNode(endKey)
|
||||
const texts = this.filterNodes(node => node.type == 'text')
|
||||
const afterStart = texts.skipUntil(node => node.key == startKey)
|
||||
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 })
|
||||
},
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*
|
||||
@@ -844,8 +842,39 @@ const Node = {
|
||||
})
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
get characters() {
|
||||
get currentCharacters() {
|
||||
const { document, selection } = this
|
||||
return document.getCharactersAtRange(selection)
|
||||
}
|
||||
@@ -70,18 +70,29 @@ class State extends Record(DEFAULTS) {
|
||||
* @return {Set} marks
|
||||
*/
|
||||
|
||||
get marks() {
|
||||
get currentMarks() {
|
||||
const { document, selection } = this
|
||||
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.
|
||||
*
|
||||
* @return {OrderedMap} nodes
|
||||
*/
|
||||
|
||||
get textNodes() {
|
||||
get currentTextNodes() {
|
||||
const { document, selection } = this
|
||||
return document.getTextNodesAtRange(selection)
|
||||
}
|
||||
@@ -147,7 +158,7 @@ class State extends Record(DEFAULTS) {
|
||||
after = selection.moveToEndOf(previous)
|
||||
}
|
||||
|
||||
else if (!selection.isAtEndOf(document)) {
|
||||
else {
|
||||
after = selection.moveBackward(n)
|
||||
}
|
||||
|
||||
@@ -215,6 +226,21 @@ class State extends Record(DEFAULTS) {
|
||||
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.
|
||||
*
|
||||
|
@@ -46,6 +46,8 @@ const TRANSFORM_TYPES = [
|
||||
'insertTextAtRange',
|
||||
'mark',
|
||||
'markAtRange',
|
||||
'setType',
|
||||
'setTypeAtRange',
|
||||
'split',
|
||||
'splitAtRange',
|
||||
'unmark',
|
||||
|
Reference in New Issue
Block a user