From 8bf081d26f53651c89299b1c7840a32ec8a9e78a Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Mon, 20 Jun 2016 17:38:56 -0700 Subject: [PATCH] add set type, bug fixes --- examples/richtext/index.css | 10 ++- examples/richtext/index.js | 113 ++++++++++++++++++------- lib/components/leaf.js | 3 +- lib/components/text.js | 6 +- lib/models/node.js | 161 +++++++++++++++++++++--------------- lib/models/state.js | 34 +++++++- lib/models/transform.js | 2 + 7 files changed, 222 insertions(+), 107 deletions(-) diff --git a/examples/richtext/index.css b/examples/richtext/index.css index e38de6985..0c9a20b69 100644 --- a/examples/richtext/index.css +++ b/examples/richtext/index.css @@ -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; } diff --git a/examples/richtext/index.js b/examples/richtext/index.js index dd5b6267b..a97bbd999 100644 --- a/examples/richtext/index.js +++ b/examples/richtext/index.js @@ -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 (
- this.onClickMark(e, 'bold')} data-active={isBold}> - format_bold - - this.onClickMark(e, 'italic')} data-active={isItalic}> - format_italic - - this.onClickMark(e, 'code')} data-active={isCode}> - code - + {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')}
) } + renderMarkButton(type, icon) { + const isActive = this.hasMark(type) + return ( + this.onClickMark(e, type)} data-active={isActive}> + {icon} + + ) + } + + renderBlockButton(type, icon) { + const isActive = this.hasBlock(type) + return ( + this.onClickBlock(e, type)} data-active={isActive}> + {icon} + + ) + } + renderEditor() { return (
@@ -142,10 +176,26 @@ class App extends React.Component { renderNode(node) { switch (node.type) { + case 'block-quote': { + return (props) =>
{props.children}
+ } + case 'bulleted-list': { + return (props) => + } + case 'heading-one': { + return (props) =>

{props.children}

+ } + case 'heading-two': { + return (props) =>

{props.children}

+ } + case 'list-item': { + return (props) =>
  • {props.chidlren}
  • + } + case 'numbered-list': { + return (props) =>
      {props.children}
    + } case 'paragraph': { - return (props) => { - return

    {props.children}

    - } + return (props) =>

    {props.children}

    } } } @@ -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' + } + } } } diff --git a/lib/components/leaf.js b/lib/components/leaf.js index cd5ec8c11..669644d3e 100644 --- a/lib/components/leaf.js +++ b/lib/components/leaf.js @@ -110,9 +110,8 @@ class Leaf extends React.Component { return ( {text ||
    }
    diff --git a/lib/components/text.js b/lib/components/text.js index ad009821c..20a35c0a4 100644 --- a/lib/components/text.js +++ b/lib/components/text.js @@ -20,11 +20,7 @@ class Text extends React.Component { render() { const { node } = this.props return ( - + {this.renderLeaves()} ) diff --git a/lib/models/node.js b/lib/models/node.js index 1cfe18fed..4c7107166 100644 --- a/lib/models/node.js +++ b/lib/models/node.js @@ -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 + */ + } /** diff --git a/lib/models/state.js b/lib/models/state.js index 39d0ced1c..747d72aab 100644 --- a/lib/models/state.js +++ b/lib/models/state.js @@ -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. * diff --git a/lib/models/transform.js b/lib/models/transform.js index 5b8d2336a..906984a68 100644 --- a/lib/models/transform.js +++ b/lib/models/transform.js @@ -46,6 +46,8 @@ const TRANSFORM_TYPES = [ 'insertTextAtRange', 'mark', 'markAtRange', + 'setType', + 'setTypeAtRange', 'split', 'splitAtRange', 'unmark',