1
0
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:
Ian Storm Taylor
2016-06-20 17:38:56 -07:00
parent 201609a655
commit 8bf081d26f
7 changed files with 222 additions and 107 deletions

View File

@@ -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;
}

View File

@@ -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'
}
}
}
}

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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
*/
}
/**

View File

@@ -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.
*

View File

@@ -46,6 +46,8 @@ const TRANSFORM_TYPES = [
'insertTextAtRange',
'mark',
'markAtRange',
'setType',
'setTypeAtRange',
'split',
'splitAtRange',
'unmark',