From e53cee3942680cc8e7d644cbfd6a47a777fd221f Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Fri, 13 Oct 2017 12:04:22 -0700 Subject: [PATCH] refactor decorations to use selections (#1221) * refactor decorations to use selections * update docs * cleanup * add Selection.createList * fix tests * fix for nested blocks * fix lint * actually merge * revert small change * add state.decorations, with search example --- docs/reference/slate/schema.md | 23 +-- examples/code-highlighting/index.js | 73 +++++--- examples/code-highlighting/state.json | 166 +++++++++++++++++- examples/index.css | 22 ++- examples/index.js | 2 + examples/search-highlighting/Readme.md | 8 + examples/search-highlighting/index.js | 149 ++++++++++++++++ examples/search-highlighting/state.json | 34 ++++ packages/slate-react/package.json | 1 + .../slate-react/src/components/content.js | 79 +++++---- packages/slate-react/src/components/leaf.js | 62 +++---- packages/slate-react/src/components/node.js | 9 +- packages/slate-react/src/components/text.js | 32 ++-- packages/slate-react/src/plugins/core.js | 9 +- .../slate-react/src/utils/find-dom-node.js | 16 +- .../{get-drop-point.js => find-drop-point.js} | 14 +- .../src/utils/find-native-point.js | 45 +++++ packages/slate-react/src/utils/find-point.js | 84 +++++++++ .../src/utils/get-caret-position.js | 46 ----- packages/slate-react/src/utils/get-point.js | 41 ----- packages/slate-react/src/utils/offset-key.js | 117 ------------ .../rendering/fixtures/custom-decorator.js | 18 +- packages/slate/src/changes/on-state.js | 14 +- packages/slate/src/models/node.js | 68 +++---- packages/slate/src/models/schema.js | 28 ++- packages/slate/src/models/selection.js | 21 ++- packages/slate/src/models/state.js | 46 ++++- packages/slate/src/models/text.js | 57 ++++-- packages/slate/src/operations/apply.js | 57 +++--- .../test/changes/on-state/set-data/simple.js | 2 +- .../raw/serialize/preserve-state-data.js | 2 +- yarn.lock | 4 + 32 files changed, 881 insertions(+), 468 deletions(-) create mode 100644 examples/search-highlighting/Readme.md create mode 100644 examples/search-highlighting/index.js create mode 100644 examples/search-highlighting/state.json rename packages/slate-react/src/utils/{get-drop-point.js => find-drop-point.js} (90%) create mode 100644 packages/slate-react/src/utils/find-native-point.js create mode 100644 packages/slate-react/src/utils/find-point.js delete mode 100644 packages/slate-react/src/utils/get-caret-position.js delete mode 100644 packages/slate-react/src/utils/get-point.js diff --git a/docs/reference/slate/schema.md b/docs/reference/slate/schema.md index 1e26ceaa6..ba6a341ec 100644 --- a/docs/reference/slate/schema.md +++ b/docs/reference/slate/schema.md @@ -103,24 +103,25 @@ Slate schemas are built up of a set of rules. Each of the properties will add ce The `match` property is the only required property of a rule. It determines which objects the rule applies to. ### `decorate` -`Function decorate(text: Node, object: Node) => List` +`Function decorate(node: Node) => List|Array` ```js { - decorate: (text, node) => { - let { characters } = text - let first = characters.get(0) - let { marks } = first - let mark = Mark.create({ type: 'bold' }) - marks = marks.add(mark) - first = first.merge({ marks }) - characters = characters.set(0, first) - return characters + decorate: (node) => { + const text = node.getFirstText() + + return [{ + anchorKey: text.key, + anchorOffset: 0, + focusKey: text.key, + focusOffset: 1, + marks: [{ type: 'bold' }] + }] } } ``` -The `decorate` property allows you define a function that will apply extra marks to all of the ranges of text inside a node. It is called with a [`Text`](./text.md) node and the matched node. It should return a list of characters with the desired marks, which will then be added to the text before rendering. +The `decorate` property allows you define a function that will apply extra marks to ranges of text inside a node. It is called with a [`Node`](./node.md). It should return a list of [`Selection`](./selection.md) objects with the desired marks, which will then be added to the text before rendering. ### `normalize` `Function normalize(change: Change, object: Node, failure: Any) => Change` diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js index e89934a17..f8ef827a8 100644 --- a/examples/code-highlighting/index.js +++ b/examples/code-highlighting/index.js @@ -1,13 +1,13 @@ import { Editor } from 'slate-react' -import { Mark, State } from 'slate' +import { State } from 'slate' import Prism from 'prismjs' import React from 'react' import initialState from './state.json' /** - * Define a code block component. + * Define our code components. * * @param {Object} props * @return {Element} @@ -40,43 +40,69 @@ function CodeBlock(props) { ) } +function CodeBlockLine(props) { + return ( +
{props.children}
+ ) +} + /** * Define a Prism.js decorator for code blocks. * - * @param {Text} text * @param {Block} block + * @return {Array} */ -function codeBlockDecorator(text, block) { - const characters = text.characters.asMutable() +function codeBlockDecorator(block) { const language = block.data.get('language') - const string = text.text + const texts = block.getTexts().toArray() + const string = texts.map(t => t.text).join('\n') const grammar = Prism.languages[language] const tokens = Prism.tokenize(string, grammar) - let offset = 0 + const decorations = [] + let startText = texts.shift() + let endText = startText + let startOffset = 0 + let endOffset = 0 + let start = 0 for (const token of tokens) { - if (typeof token == 'string') { - offset += token.length - continue + startText = endText + startOffset = endOffset + + const content = typeof token == 'string' ? token : token.content + const newlines = content.split('\n').length - 1 + const length = content.length - newlines + const end = start + length + + let available = startText.text.length - startOffset + let remaining = length + + endOffset = startOffset + remaining + + while (available < remaining) { + endText = texts.shift() + remaining = length - available + available = endText.text.length + endOffset = remaining } - const length = offset + token.content.length - const type = `highlight-${token.type}` - const mark = Mark.create({ type }) + if (typeof token != 'string') { + const range = { + anchorKey: startText.key, + anchorOffset: startOffset, + focusKey: endText.key, + focusOffset: endOffset, + marks: [{ type: `highlight-${token.type}` }], + } - for (let i = offset; i < length; i++) { - let char = characters.get(i) - let { marks } = char - marks = marks.add(mark) - char = char.set('marks', marks) - characters.set(i, char) + decorations.push(range) } - offset = length + start = end } - return characters.asImmutable() + return decorations } /** @@ -90,7 +116,10 @@ const schema = { code: { render: CodeBlock, decorate: codeBlockDecorator, - } + }, + code_line: { + render: CodeBlockLine, + }, }, marks: { 'highlight-comment': { diff --git a/examples/code-highlighting/state.json b/examples/code-highlighting/state.json index dca13108c..683b6eab2 100644 --- a/examples/code-highlighting/state.json +++ b/examples/code-highlighting/state.json @@ -23,10 +23,170 @@ }, "nodes": [ { - "kind": "text", - "ranges": [ + "kind": "block", + "type": "code_line", + "nodes": [ { - "text": "// A simple FizzBuzz implementation.\nfor (var i = 1; i <= 100; i++) {\n if (i % 15 == 0) {\n console.log('Fizz Buzz');\n } else if (i % 5 == 0) {\n console.log('Buzz');\n } else if (i % 3 == 0) {\n console.log('Fizz');\n } else {\n console.log(i);\n }\n}" + "kind": "text", + "ranges": [ + { + "text": "// A simple FizzBuzz implementation." + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "for (var i = 1; i <= 100; i++) {" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " if (i % 15 == 0) {" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " console.log('Fizz Buzz');" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " } else if (i % 5 == 0) {" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " console.log('Buzz');" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " } else if (i % 3 == 0) {" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " console.log('Fizz');" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " } else {" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " console.log(i);" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": " }" + } + ] + } + ] + }, + { + "kind": "block", + "type": "code_line", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "}" + } + ] } ] } diff --git a/examples/index.css b/examples/index.css index e347c70a3..f55ac7c37 100644 --- a/examples/index.css +++ b/examples/index.css @@ -61,6 +61,7 @@ td { } input { + box-sizing: border-box; font-size: .85em; width: 100%; padding: .5em; @@ -173,12 +174,29 @@ input:focus { } .toolbar-menu { - padding: 1px 0 17px 18px; + position: relative; + padding: 1px 18px 17px; margin: 0 -20px; border-bottom: 2px solid #eee; margin-bottom: 20px; } +.toolbar-menu .search { + position: relative; +} + +.toolbar-menu .search-icon { + position: absolute; + top: 0.5em; + left: 0.5em; + color: #ccc; +} + +.toolbar-menu .search-box { + padding-left: 2em; + width: 100%; +} + .hover-menu { padding: 8px 7px 6px; position: absolute; @@ -236,4 +254,4 @@ input:focus { padding: 12px; background-color: #EBEBEB; display: inline-block; -} \ No newline at end of file +} diff --git a/examples/index.js b/examples/index.js index 8a47d799b..21fcc3380 100644 --- a/examples/index.js +++ b/examples/index.js @@ -20,6 +20,7 @@ import Plugins from './plugins' import RTL from './rtl' import ReadOnly from './read-only' import RichText from './rich-text' +import SearchHighlighting from './search-highlighting' import Tables from './tables' import DevHugeDocument from './dev/huge-document' @@ -54,6 +55,7 @@ const EXAMPLES = [ ['Code Highlighting', CodeHighlighting, '/code-highlighting'], ['Tables', Tables, '/tables'], ['Paste HTML', PasteHtml, '/paste-html'], + ['Search Highlighting', SearchHighlighting, '/search-highlighting'], ['Read-only', ReadOnly, '/read-only'], ['RTL', RTL, '/rtl'], ['Plugins', Plugins, '/plugins'], diff --git a/examples/search-highlighting/Readme.md b/examples/search-highlighting/Readme.md new file mode 100644 index 000000000..d4aa063d3 --- /dev/null +++ b/examples/search-highlighting/Readme.md @@ -0,0 +1,8 @@ + +# Rich Text Example + +![](../../docs/images/rich-text-example.png) + +This example shows you can add a very different concepts together: key commands, toolbars, and custom formatting, to get the functionality you'd expect from a rich text editor. Of course this is just the beginning, you can layer in whatever other behaviors you want! + +Check out the [Examples readme](..) to see how to run it! diff --git a/examples/search-highlighting/index.js b/examples/search-highlighting/index.js new file mode 100644 index 000000000..50c0850ca --- /dev/null +++ b/examples/search-highlighting/index.js @@ -0,0 +1,149 @@ + +import { Editor } from 'slate-react' +import { State } from 'slate' + +import React from 'react' +import initialState from './state.json' + +/** + * Define a schema. + * + * @type {Object} + */ + +const schema = { + marks: { + highlight: { + backgroundColor: '#ffeeba' + } + } +} + +/** + * The rich text example. + * + * @type {Component} + */ + +class SearchHighlighting extends React.Component { + + /** + * Deserialize the initial editor state. + * + * @type {Object} + */ + + state = { + state: State.fromJSON(initialState), + } + + /** + * On change, save the new `state`. + * + * @param {Change} change + */ + + onChange = ({ state }) => { + this.setState({ state }) + } + + /** + * On input change, update the decorations. + * + * @param {Event} e + */ + + onInputChange = (e) => { + const { state } = this.state + const string = e.target.value + const texts = state.document.getTexts() + const decorations = [] + + texts.forEach((node) => { + const { key, text } = node + const parts = text.split(string) + let offset = 0 + + parts.forEach((part, i) => { + if (i != 0) { + decorations.push({ + anchorKey: key, + anchorOffset: offset - string.length, + focusKey: key, + focusOffset: offset, + marks: [{ type: 'highlight' }], + }) + } + + offset = offset + part.length + string.length + }) + }) + + const change = state.change().setState({ decorations }) + this.onChange(change) + } + + /** + * Render. + * + * @return {Element} + */ + + render() { + return ( +
+ {this.renderToolbar()} + {this.renderEditor()} +
+ ) + } + + /** + * Render the toolbar. + * + * @return {Element} + */ + + renderToolbar = () => { + return ( +
+
+ search + +
+
+ ) + } + + /** + * Render the Slate editor. + * + * @return {Element} + */ + + renderEditor = () => { + return ( +
+ +
+ ) + } + +} + +/** + * Export. + */ + +export default SearchHighlighting diff --git a/examples/search-highlighting/state.json b/examples/search-highlighting/state.json new file mode 100644 index 000000000..7c6c5785a --- /dev/null +++ b/examples/search-highlighting/state.json @@ -0,0 +1,34 @@ +{ + "document": { + "nodes": [ + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "This is editable text that you can search. As you search, it looks for matching strings of text, and adds \"decoration\" marks to them in realtime." + } + ] + } + ] + }, + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "ranges": [ + { + "text": "Try it out for yourself by typing in the search box above!" + } + ] + } + ] + } + ] + } +} diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index 27cb130f0..828d746a8 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -13,6 +13,7 @@ "keycode": "^2.1.2", "prop-types": "^15.5.8", "react-portal": "^3.1.0", + "react-immutable-proptypes": "^2.1.0", "selection-is-backward": "^1.0.0", "slate-base64-serializer": "^0.1.11", "slate-dev-logger": "^0.1.12", diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index a172068cb..97a08c14e 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -12,13 +12,13 @@ import TRANSFER_TYPES from '../constants/transfer-types' import Node from './node' import extendSelection from '../utils/extend-selection' import findClosestNode from '../utils/find-closest-node' -import getCaretPosition from '../utils/get-caret-position' +import findDropPoint from '../utils/find-drop-point' +import findNativePoint from '../utils/find-native-point' +import findPoint from '../utils/find-point' import getHtmlFromNativePaste from '../utils/get-html-from-native-paste' -import getPoint from '../utils/get-point' -import getDropPoint from '../utils/get-drop-point' import getTransferData from '../utils/get-transfer-data' -import setTransferData from '../utils/set-transfer-data' import scrollToSelection from '../utils/scroll-to-selection' +import setTransferData from '../utils/set-transfer-data' import { IS_FIREFOX, IS_MAC, IS_IE } from '../constants/environment' /** @@ -121,7 +121,7 @@ class Content extends React.Component { */ updateSelection = () => { - const { editor, state } = this.props + const { state } = this.props const { selection } = state const window = getWindow(this.element) const native = window.getSelection() @@ -144,10 +144,8 @@ class Content extends React.Component { // Otherwise, figure out which DOM nodes should be selected... const { anchorKey, anchorOffset, focusKey, focusOffset, isCollapsed } = selection - const anchor = getCaretPosition(anchorKey, anchorOffset, state, editor, this.element) - const focus = isCollapsed - ? anchor - : getCaretPosition(focusKey, focusOffset, state, editor, this.element) + const anchor = findNativePoint(anchorKey, anchorOffset) + const focus = isCollapsed ? anchor : findNativePoint(focusKey, focusOffset) // If they are already selected, do nothing. if ( @@ -432,12 +430,11 @@ class Content extends React.Component { if (this.props.readOnly) return - const { editor, state } = this.props + const { state } = this.props const { nativeEvent } = event const { dataTransfer } = nativeEvent const data = getTransferData(dataTransfer) - const point = getDropPoint(event, state, editor) - + const point = findDropPoint(event, state) if (!point) return // Add drop-specific information to the data. @@ -484,26 +481,33 @@ class Content extends React.Component { // Get the selection point. const native = window.getSelection() const { anchorNode, anchorOffset } = native - const point = getPoint(anchorNode, anchorOffset, state, editor) + const point = findPoint(anchorNode, anchorOffset, state) if (!point) return - // Get the range in question. - const { key, index, start, end } = point + // Get the text node and range in question. const { document, selection } = state - const schema = editor.getSchema() - const decorators = document.getDescendantDecorators(key, schema) - const node = document.getDescendant(key) - const block = document.getClosestBlock(node.key) - const ranges = node.getRanges(decorators) - const lastText = block.getLastText() + const node = document.getDescendant(point.key) + const ranges = node.getRanges() + let start = 0 + let end = 0 + + const range = ranges.find((r) => { + end += r.text.length + if (end >= point.offset) return true + start = end + }) // Get the text information. + const { text } = range let { textContent } = anchorNode + const block = document.getClosestBlock(node.key) + const lastText = block.getLastText() + const lastRange = ranges.last() const lastChar = textContent.charAt(textContent.length - 1) const isLastText = node == lastText - const isLastRange = index == ranges.size - 1 + const isLastRange = range == lastRange - // If we're dealing with the last leaf, and the DOM text ends in a new line, + // COMPAT: If this is the last range, and the DOM text ends in a new line, // we will have added another new line in 's render method to account // for browsers collapsing a single trailing new lines, so remove it. if (isLastText && isLastRange && lastChar == '\n') { @@ -511,26 +515,20 @@ class Content extends React.Component { } // If the text is no different, abort. - const range = ranges.get(index) - const { text, marks } = range if (textContent == text) return // Determine what the selection should be after changing the text. const delta = textContent.length - text.length - const after = selection.collapseToEnd().move(delta) + const corrected = selection.collapseToEnd().move(delta) + const entire = selection.moveAnchorTo(point.key, start).moveFocusTo(point.key, end) - // Change the current state to have the text replaced. + // Change the current state to have the range's text replaced. editor.change((change) => { change - .select({ - anchorKey: key, - anchorOffset: start, - focusKey: key, - focusOffset: end - }) + .select(entire) .delete() - .insertText(textContent, marks) - .select(after) + .insertText(textContent, range.marks) + .select(corrected) }) } @@ -677,7 +675,7 @@ class Content extends React.Component { if (!this.isInEditor(event.target)) return const window = getWindow(event.target) - const { state, editor } = this.props + const { state } = this.props const { document, selection } = state const native = window.getSelection() const data = {} @@ -690,8 +688,8 @@ class Content extends React.Component { // Otherwise, determine the Slate selection from the native one. else { const { anchorNode, anchorOffset, focusNode, focusOffset } = native - const anchor = getPoint(anchorNode, anchorOffset, state, editor) - const focus = getPoint(focusNode, focusOffset, state, editor) + const anchor = findPoint(anchorNode, anchorOffset, state) + const focus = findPoint(focusNode, focusOffset, state) if (!anchor || !focus) return // There are situations where a select event will fire with a new native @@ -872,11 +870,14 @@ class Content extends React.Component { renderNode = (child, isSelected) => { const { editor, readOnly, schema, state } = this.props - const { document } = state + const { document, decorations } = state + let decs = document.getDecorations(schema) + if (decorations) decs = decorations.concat(decs) return ( { + const Component = mark.getComponent(schema) + if (!Component) return memo + return ( + + {memo} + + ) + }, children) + } + /** * Render the text content of the leaf, accounting for browsers. * @@ -136,37 +167,6 @@ class Leaf extends React.Component { return text } - /** - * Render all of the leaf's mark components. - * - * @param {Object} props - * @return {Element} - */ - - renderMarks(props) { - const { marks, schema, node, offset, text, state, editor } = props - const children = this.renderText(props) - - return marks.reduce((memo, mark) => { - const Component = mark.getComponent(schema) - if (!Component) return memo - return ( - - {memo} - - ) - }, children) - } - } /** diff --git a/packages/slate-react/src/components/node.js b/packages/slate-react/src/components/node.js index 4c0df2856..447e88d37 100644 --- a/packages/slate-react/src/components/node.js +++ b/packages/slate-react/src/components/node.js @@ -1,6 +1,7 @@ import Base64 from 'slate-base64-serializer' import Debug from 'debug' +import ImmutableTypes from 'react-immutable-proptypes' import React from 'react' import SlateTypes from 'slate-prop-types' import logger from 'slate-dev-logger' @@ -35,6 +36,7 @@ class Node extends React.Component { static propTypes = { block: SlateTypes.block, + decorations: ImmutableTypes.list.isRequired, editor: Types.object.isRequired, isSelected: Types.bool.isRequired, node: SlateTypes.node.isRequired, @@ -136,6 +138,9 @@ class Node extends React.Component { // need to be rendered again. if (n.isSelected || p.isSelected) return true + // If the decorations have changed, update. + if (!n.decorations.equals(p.decorations)) return true + // Otherwise, don't update. return false } @@ -225,11 +230,13 @@ class Node extends React.Component { */ renderNode = (child, isSelected) => { - const { block, editor, node, readOnly, schema, state } = this.props + const { block, decorations, editor, node, readOnly, schema, state } = this.props const Component = child.kind === 'text' ? Text : Node + const decs = decorations.concat(node.getDecorations(schema)) return ( ` or `\n`. if (n.parent.kind == 'block') { @@ -81,6 +73,9 @@ class Text extends React.Component { if (p.node == pLast && n.node != nLast) return true } + // Re-render if the current decorations have changed. + if (!n.decorations.equals(p.decorations)) return true + // Otherwise, don't update. return false } @@ -95,10 +90,19 @@ class Text extends React.Component { const { props } = this this.debug('render', { props }) - const { node, schema, state } = props + const { decorations, node, state } = props const { document } = state - const decorators = schema.hasDecorators ? document.getDescendantDecorators(node.key, schema) : [] - const ranges = node.getRanges(decorators) + const { key } = node + + const decs = decorations.filter((d) => { + const { startKey, endKey } = d + if (startKey == key || endKey == key) return true + const startsBefore = document.areDescendantsSorted(startKey, key) + const endsAfter = document.areDescendantsSorted(key, endKey) + return startsBefore && endsAfter + }) + + const ranges = node.getRanges(decs) let offset = 0 const leaves = ranges.map((range, i) => { @@ -108,7 +112,7 @@ class Text extends React.Component { }) return ( - + {leaves} ) diff --git a/packages/slate-react/src/plugins/core.js b/packages/slate-react/src/plugins/core.js index 48b5397ca..afa729f3e 100644 --- a/packages/slate-react/src/plugins/core.js +++ b/packages/slate-react/src/plugins/core.js @@ -8,8 +8,8 @@ import { Block, Inline, coreSchema } from 'slate' import Content from '../components/content' import Placeholder from '../components/placeholder' -import getPoint from '../utils/get-point' import findDOMNode from '../utils/find-dom-node' +import findPoint from '../utils/find-point' import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment' /** @@ -64,10 +64,9 @@ function Plugin(options = {}) { * @param {Event} e * @param {Object} data * @param {Change} change - * @param {Editor} editor */ - function onBeforeInput(e, data, change, editor) { + function onBeforeInput(e, data, change) { debug('onBeforeInput', { data }) e.preventDefault() @@ -82,8 +81,8 @@ function Plugin(options = {}) { // the selection has gotten out of sync, and adjust it if so. (03/18/2017) const window = getWindow(e.target) const native = window.getSelection() - const a = getPoint(native.anchorNode, native.anchorOffset, state, editor) - const f = getPoint(native.focusNode, native.focusOffset, state, editor) + const a = findPoint(native.anchorNode, native.anchorOffset, state) + const f = findPoint(native.focusNode, native.focusOffset, state) const hasMismatch = a && f && ( anchorKey != a.key || anchorOffset != a.offset || diff --git a/packages/slate-react/src/utils/find-dom-node.js b/packages/slate-react/src/utils/find-dom-node.js index 2702bee8d..b86c75afb 100644 --- a/packages/slate-react/src/utils/find-dom-node.js +++ b/packages/slate-react/src/utils/find-dom-node.js @@ -1,16 +1,22 @@ +import { Node } from 'slate' + /** - * Find the DOM node for a `node`. + * Find the DOM node for a `key`. * - * @param {Node} node + * @param {String|Node} key * @return {Element} */ -function findDOMNode(node) { - const el = window.document.querySelector(`[data-key="${node.key}"]`) +function findDOMNode(key) { + if (Node.isNode(key)) { + key = key.key + } + + const el = window.document.querySelector(`[data-key="${key}"]`) if (!el) { - throw new Error(`Unable to find a DOM node for "${node.key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`) + throw new Error(`Unable to find a DOM node for "${key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`) } return el diff --git a/packages/slate-react/src/utils/get-drop-point.js b/packages/slate-react/src/utils/find-drop-point.js similarity index 90% rename from packages/slate-react/src/utils/get-drop-point.js rename to packages/slate-react/src/utils/find-drop-point.js index 96671d927..c87d2e4ac 100644 --- a/packages/slate-react/src/utils/get-drop-point.js +++ b/packages/slate-react/src/utils/find-drop-point.js @@ -2,18 +2,17 @@ import getWindow from 'get-window' import findClosestNode from './find-closest-node' -import getPoint from './get-point' +import findPoint from './find-point' /** - * Get the target point for a drop event. + * Find the target point for a drop `event`. * * @param {Event} event * @param {State} state - * @param {Editor} editor * @return {Object} */ -function getDropPoint(event, state, editor) { +function findDropPoint(event, state) { const { document } = state const { nativeEvent, target } = event const { x, y } = nativeEvent @@ -48,7 +47,6 @@ function getDropPoint(event, state, editor) { document.getNextSibling(nodeKey) const key = text.key const offset = previous ? text.characters.size : 0 - return { key, offset } } @@ -71,12 +69,10 @@ function getDropPoint(event, state, editor) { const text = block.getLastText() const { key } = text const offset = 0 - return { key, offset } } - const point = getPoint(n, o, state, editor) - + const point = findPoint(n, o, state) return point } @@ -86,4 +82,4 @@ function getDropPoint(event, state, editor) { * @type {Function} */ -export default getDropPoint +export default findDropPoint diff --git a/packages/slate-react/src/utils/find-native-point.js b/packages/slate-react/src/utils/find-native-point.js new file mode 100644 index 000000000..639b4a6aa --- /dev/null +++ b/packages/slate-react/src/utils/find-native-point.js @@ -0,0 +1,45 @@ + +import getWindow from 'get-window' + +import findDOMNode from './find-dom-node' + +/** + * Find a native DOM selection point from a Slate `key` and `offset`. + * + * @param {Element} root + * @param {String} key + * @param {Number} offset + * @return {Object} + */ + +function findNativePoint(key, offset) { + const el = findDOMNode(key) + if (!el) return null + + const window = getWindow(el) + const iterator = window.document.createNodeIterator(el, NodeFilter.SHOW_TEXT) + let start = 0 + let n + + while (n = iterator.nextNode()) { + const { length } = n.textContent + const end = start + length + + if (offset <= end) { + const o = offset - start + return { node: n, offset: o } + } + + start = end + } + + return null +} + +/** + * Export. + * + * @type {Function} + */ + +export default findNativePoint diff --git a/packages/slate-react/src/utils/find-point.js b/packages/slate-react/src/utils/find-point.js new file mode 100644 index 000000000..6bf16e0b7 --- /dev/null +++ b/packages/slate-react/src/utils/find-point.js @@ -0,0 +1,84 @@ + +import getWindow from 'get-window' + +import OffsetKey from './offset-key' +import normalizeNodeAndOffset from './normalize-node-and-offset' +import findClosestNode from './find-closest-node' + +/** + * Constants. + * + * @type {String} + */ + +const OFFSET_KEY_ATTRIBUTE = 'data-offset-key' +const RANGE_SELECTOR = `[${OFFSET_KEY_ATTRIBUTE}]` +const TEXT_SELECTOR = `[data-key]` +const VOID_SELECTOR = '[data-slate-void]' + +/** + * Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`. + * + * @param {Element} nativeNode + * @param {Number} nativeOffset + * @param {State} state + * @return {Object} + */ + +function findPoint(nativeNode, nativeOffset, state) { + const { + node: nearestNode, + offset: nearestOffset, + } = normalizeNodeAndOffset(nativeNode, nativeOffset) + + const window = getWindow(nativeNode) + const { parentNode } = nearestNode + let rangeNode = findClosestNode(parentNode, RANGE_SELECTOR) + let offset + let node + + // Calculate how far into the text node the `nearestNode` is, so that we can + // determine what the offset relative to the text node is. + if (rangeNode) { + const range = window.document.createRange() + const textNode = findClosestNode(rangeNode, TEXT_SELECTOR) + range.setStart(textNode, 0) + range.setEnd(nearestNode, nearestOffset) + node = textNode + offset = range.toString().length + } + + // For void nodes, the element with the offset key will be a cousin, not an + // ancestor, so find it by going down from the nearest void parent. + else { + const voidNode = findClosestNode(parentNode, VOID_SELECTOR) + if (!voidNode) return null + rangeNode = voidNode.querySelector(RANGE_SELECTOR) + node = rangeNode + offset = node.textContent.length + } + + // Get the string value of the offset key attribute. + const offsetKey = rangeNode.getAttribute(OFFSET_KEY_ATTRIBUTE) + if (!offsetKey) return null + + const { key } = OffsetKey.parse(offsetKey) + + // COMPAT: If someone is clicking from one Slate editor into another, the + // select event fires twice, once for the old editor's `element` first, and + // then afterwards for the correct `element`. (2017/03/03) + if (!state.document.hasDescendant(key)) return null + + return { + key, + offset, + } +} + +/** + * Export. + * + * @type {Function} + */ + +export default findPoint diff --git a/packages/slate-react/src/utils/get-caret-position.js b/packages/slate-react/src/utils/get-caret-position.js deleted file mode 100644 index 432a8cf1d..000000000 --- a/packages/slate-react/src/utils/get-caret-position.js +++ /dev/null @@ -1,46 +0,0 @@ - -import findDeepestNode from './find-deepest-node' - -/** - * Get caret position from selection point. - * - * @param {String} key - * @param {Number} offset - * @param {State} state - * @param {Editor} editor - * @param {Element} el - * @return {Object} - */ - -function getCaretPosition(key, offset, state, editor, el) { - const { document } = state - const text = document.getDescendant(key) - const schema = editor.getSchema() - const decorators = document.getDescendantDecorators(key, schema) - const ranges = text.getRanges(decorators) - - let a = 0 - let index - let off - - ranges.forEach((range, i) => { - const { length } = range.text - a += length - if (a < offset) return - index = i - off = offset - (a - length) - return false - }) - - const span = el.querySelector(`[data-offset-key="${key}-${index}"]`) - const node = findDeepestNode(span) - return { node, offset: off } -} - -/** - * Export. - * - * @type {Function} - */ - -export default getCaretPosition diff --git a/packages/slate-react/src/utils/get-point.js b/packages/slate-react/src/utils/get-point.js deleted file mode 100644 index 4a848f96c..000000000 --- a/packages/slate-react/src/utils/get-point.js +++ /dev/null @@ -1,41 +0,0 @@ - -import OffsetKey from './offset-key' - -/** - * Get a point from a native selection's DOM `element` and `offset`. - * - * @param {Element} element - * @param {Number} offset - * @param {State} state - * @param {Editor} editor - * @return {Object} - */ - -function getPoint(element, offset, state, editor) { - const { document } = state - const schema = editor.getSchema() - - // If we can't find an offset key, we can't get a point. - const offsetKey = OffsetKey.findKey(element, offset) - if (!offsetKey) return null - - // COMPAT: If someone is clicking from one Slate editor into another, the - // select event fires two, once for the old editor's `element` first, and - // then afterwards for the correct `element`. (2017/03/03) - const { key } = offsetKey - const node = document.getDescendant(key) - if (!node) return null - - const decorators = document.getDescendantDecorators(key, schema) - const ranges = node.getRanges(decorators) - const point = OffsetKey.findPoint(offsetKey, ranges) - return point -} - -/** - * Export. - * - * @type {Function} - */ - -export default getPoint diff --git a/packages/slate-react/src/utils/offset-key.js b/packages/slate-react/src/utils/offset-key.js index 4fbc5ec0d..fdc665743 100644 --- a/packages/slate-react/src/utils/offset-key.js +++ b/packages/slate-react/src/utils/offset-key.js @@ -1,7 +1,4 @@ -import normalizeNodeAndOffset from './normalize-node-and-offset' -import findClosestNode from './find-closest-node' - /** * Offset key parser regex. * @@ -10,117 +7,6 @@ import findClosestNode from './find-closest-node' const PARSER = /^(\w+)(?:-(\d+))?$/ -/** - * Offset key attribute name. - * - * @type {String} - */ - -const ATTRIBUTE = 'data-offset-key' - -/** - * Offset key attribute selector. - * - * @type {String} - */ - -const SELECTOR = `[${ATTRIBUTE}]` - -/** - * Void node selection. - * - * @type {String} - */ - -const VOID_SELECTOR = '[data-slate-void]' - -/** - * Find the start and end bounds from an `offsetKey` and `ranges`. - * - * @param {Number} index - * @param {List} ranges - * @return {Object} - */ - -function findBounds(index, ranges) { - const range = ranges.get(index) - const start = ranges - .slice(0, index) - .reduce((memo, r) => { - return memo += r.text.length - }, 0) - - return { - start, - end: start + range.text.length - } -} - -/** - * From a DOM node, find the closest parent's offset key. - * - * @param {Element} rawNode - * @param {Number} rawOffset - * @return {Object} - */ - -function findKey(rawNode, rawOffset) { - let { node, offset } = normalizeNodeAndOffset(rawNode, rawOffset) - const { parentNode } = node - - // Find the closest parent with an offset key attribute. - let closest = findClosestNode(parentNode, SELECTOR) - - // For void nodes, the element with the offset key will be a cousin, not an - // ancestor, so find it by going down from the nearest void parent. - if (!closest) { - const closestVoid = findClosestNode(parentNode, VOID_SELECTOR) - if (!closestVoid) return null - closest = closestVoid.querySelector(SELECTOR) - offset = closest.textContent.length - } - - // Get the string value of the offset key attribute. - const offsetKey = closest.getAttribute(ATTRIBUTE) - - // If we still didn't find an offset key, abort. - if (!offsetKey) return null - - // Return the parsed the offset key. - const parsed = parse(offsetKey) - return { - key: parsed.key, - index: parsed.index, - offset - } -} - -/** - * Find the selection point from an `offsetKey` and `ranges`. - * - * @param {Object} offsetKey - * @param {List} ranges - * @return {Object} - */ - -function findPoint(offsetKey, ranges) { - let { key, index, offset } = offsetKey - const { start, end } = findBounds(index, ranges) - - // Don't let the offset be outside of the start and end bounds. - offset = start + offset - offset = Math.max(offset, start) - offset = Math.min(offset, end) - - return { - key, - index, - start, - end, - offset - } -} - /** * Parse an offset key `string`. * @@ -158,9 +44,6 @@ function stringify(object) { */ export default { - findBounds, - findKey, - findPoint, parse, stringify } diff --git a/packages/slate-react/test/rendering/fixtures/custom-decorator.js b/packages/slate-react/test/rendering/fixtures/custom-decorator.js index 78d0d83ab..73f1b0540 100644 --- a/packages/slate-react/test/rendering/fixtures/custom-decorator.js +++ b/packages/slate-react/test/rendering/fixtures/custom-decorator.js @@ -1,19 +1,19 @@ /** @jsx h */ import h from '../../helpers/h' -import { Mark } from 'slate' export const schema = { nodes: { paragraph: { - decorate(text, block) { - let { characters } = text - let second = characters.get(1) - const mark = Mark.create({ type: 'bold' }) - const marks = second.marks.add(mark) - second = second.merge({ marks }) - characters = characters.set(1, second) - return characters + decorate(block) { + const text = block.getFirstText() + return [{ + anchorKey: text.key, + anchorOffset: 1, + focusKey: text.key, + focusOffset: 2, + marks: [{ type: 'bold' }] + }] } } }, diff --git a/packages/slate/src/changes/on-state.js b/packages/slate/src/changes/on-state.js index 3bd8a06cb..434f2d26c 100644 --- a/packages/slate/src/changes/on-state.js +++ b/packages/slate/src/changes/on-state.js @@ -1,4 +1,6 @@ +import State from '../models/state' + /** * Changes. * @@ -8,20 +10,20 @@ const Changes = {} /** - * Set `properties` on the top-level state's data. + * Set `properties` on the state. * * @param {Change} change - * @param {Object} properties + * @param {Object|State} properties */ -Changes.setData = (change, properties) => { +Changes.setState = (change, properties) => { + properties = State.createProperties(properties) const { state } = change - const { data } = state change.applyOperation({ - type: 'set_data', + type: 'set_state', properties, - data, + state, }) } diff --git a/packages/slate/src/models/node.js b/packages/slate/src/models/node.js index 03af04e61..fe907a62d 100644 --- a/packages/slate/src/models/node.js +++ b/packages/slate/src/models/node.js @@ -165,19 +165,12 @@ class Node { first = normalizeKey(first) second = normalizeKey(second) - let sorted + const keys = this.getKeysAsArray() + const firstIndex = keys.indexOf(first) + const secondIndex = keys.indexOf(second) + if (firstIndex == -1 || secondIndex == -1) return null - this.forEachDescendant((n) => { - if (n.key === first) { - sorted = true - return false - } else if (n.key === second) { - sorted = false - return false - } - }) - - return sorted + return firstIndex < secondIndex } /** @@ -609,8 +602,8 @@ class Node { * @return {Array} */ - getDecorators(schema) { - return schema.__getDecorators(this) + getDecorations(schema) { + return schema.__getDecorations(this) } /** @@ -674,32 +667,6 @@ class Node { return descendant } - /** - * Get the decorators for a descendant by `key` given a `schema`. - * - * @param {String} key - * @param {Schema} schema - * @return {Array} - */ - - getDescendantDecorators(key, schema) { - if (!schema.hasDecorators) { - return [] - } - - const descendant = this.assertDescendant(key) - let child = this.getFurthestAncestor(key) - let decorators = [] - - while (child != descendant) { - decorators = decorators.concat(child.getDecorators(schema)) - child = child.getFurthestAncestor(key) - } - - decorators = decorators.concat(descendant.getDecorators(schema)) - return decorators - } - /** * Get the first child text node. * @@ -958,18 +925,29 @@ class Node { } /** - * Return a set of all keys in the node. + * Return a set of all keys in the node as an array. * - * @return {Set} + * @return {Array} */ - getKeys() { + getKeysAsArray() { const keys = [] this.forEachDescendant((desc) => { keys.push(desc.key) }) + return keys + } + + /** + * Return a set of all keys in the node. + * + * @return {Set} + */ + + getKeys() { + const keys = this.getKeysAsArray() return new Set(keys) } @@ -2102,6 +2080,7 @@ memoize(Node.prototype, [ 'getInlines', 'getInlinesAsArray', 'getKeys', + 'getKeysAsArray', 'getLastText', 'getMarks', 'getOrderedMarks', @@ -2135,11 +2114,10 @@ memoize(Node.prototype, [ 'getClosestVoid', 'getCommonAncestor', 'getComponent', - 'getDecorators', + 'getDecorations', 'getDepth', 'getDescendant', 'getDescendantAtPath', - 'getDescendantDecorators', 'getFragmentAtRange', 'getFurthestBlock', 'getFurthestInline', diff --git a/packages/slate/src/models/schema.js b/packages/slate/src/models/schema.js index e46f19f77..919f10c5f 100644 --- a/packages/slate/src/models/schema.js +++ b/packages/slate/src/models/schema.js @@ -7,6 +7,7 @@ import typeOf from 'type-of' import { Record } from 'immutable' import MODEL_TYPES from '../constants/model-types' +import Selection from '../models/selection' import isReactComponent from '../utils/is-react-component' /** @@ -126,24 +127,33 @@ class Schema extends Record(DEFAULTS) { } /** - * Return the decorators for an `object`. + * Return the decorations 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} + * @return {List} */ - __getDecorators(object) { - return this.rules - .filter(rule => rule.decorate && rule.match(object)) - .map((rule) => { - return (text) => { - return rule.decorate(text, object) - } + __getDecorations(object) { + const array = [] + + this.rules.forEach((rule) => { + if (!rule.decorate) return + if (!rule.match(object)) return + + const decorations = rule.decorate(object) + if (!decorations.length) return + + decorations.forEach((dec) => { + array.push(dec) }) + }) + + const list = Selection.createList(array) + return list } /** diff --git a/packages/slate/src/models/selection.js b/packages/slate/src/models/selection.js index 49ed4360b..d17ac06a7 100644 --- a/packages/slate/src/models/selection.js +++ b/packages/slate/src/models/selection.js @@ -1,9 +1,10 @@ import isPlainObject from 'is-plain-object' import logger from 'slate-dev-logger' -import { Record } from 'immutable' +import { List, Record, Set } from 'immutable' import MODEL_TYPES from '../constants/model-types' +import Mark from './mark' /** * Default properties. @@ -48,6 +49,22 @@ class Selection extends Record(DEFAULTS) { throw new Error(`\`Selection.create\` only accepts objects or selections, but you passed it: ${attrs}`) } + /** + * Create a list of `Selections` from a `value`. + * + * @param {Array|List} value + * @return {List} + */ + + static createList(value = []) { + if (List.isList(value) || Array.isArray(value)) { + const list = new List(value.map(Selection.create)) + return list + } + + throw new Error(`\`Selection.createList\` only accepts arrays or lists, but you passed it: ${value}`) + } + /** * Create a dictionary of settable selection properties from `attrs`. * @@ -108,7 +125,7 @@ class Selection extends Record(DEFAULTS) { focusOffset, isBackward, isFocused, - marks, + marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)), }) return selection diff --git a/packages/slate/src/models/state.js b/packages/slate/src/models/state.js index 445a73743..98cc4dc4d 100644 --- a/packages/slate/src/models/state.js +++ b/packages/slate/src/models/state.js @@ -5,7 +5,7 @@ import { Record, Set, List, Map } from 'immutable' import MODEL_TYPES from '../constants/model-types' import SCHEMA from '../schemas/core' -import Change from './change' +import Data from './data' import Document from './document' import History from './history' import Selection from './selection' @@ -21,6 +21,7 @@ const DEFAULTS = { selection: Selection.create(), history: History.create(), data: new Map(), + decorations: null, } /** @@ -51,6 +52,31 @@ class State extends Record(DEFAULTS) { throw new Error(`\`State.create\` only accepts objects or states, but you passed it: ${attrs}`) } + /** + * Create a dictionary of settable state properties from `attrs`. + * + * @param {Object|State} attrs + * @return {Object} + */ + + static createProperties(attrs = {}) { + if (State.isState(attrs)) { + return { + data: attrs.data, + decorations: attrs.decorations, + } + } + + if (isPlainObject(attrs)) { + const props = {} + if ('data' in attrs) props.data = Data.create(attrs.data) + if ('decorations' in attrs) props.decorations = Selection.createList(attrs.decorations) + return props + } + + throw new Error(`\`State.createProperties\` only accepts objects or states, but you passed it: ${attrs}`) + } + /** * Create a `State` from a JSON `object`. * @@ -549,6 +575,7 @@ class State extends Record(DEFAULTS) { */ change(attrs = {}) { + const Change = require('./change').default return new Change({ ...attrs, state: this }) } @@ -572,11 +599,20 @@ class State extends Record(DEFAULTS) { toJSON(options = {}) { const object = { + kind: this.kind, data: this.data.toJSON(), document: this.document.toJSON(options), - kind: this.kind, - history: this.history.toJSON(), selection: this.selection.toJSON(), + decorations: this.decorations ? this.decorations.toArray().map(d => d.toJSON()) : null, + history: this.history.toJSON(), + } + + if (!options.preserveData) { + delete object.data + } + + if (!options.preserveDecorations) { + delete object.decorations } if (!options.preserveHistory) { @@ -587,10 +623,6 @@ class State extends Record(DEFAULTS) { delete object.selection } - if (!options.preserveStateData) { - delete object.data - } - if (options.preserveSelection && !options.preserveKeys) { const { document, selection } = this object.selection.anchorPath = selection.isSet ? document.getPath(selection.anchorKey) : null diff --git a/packages/slate/src/models/text.js b/packages/slate/src/models/text.js index d2528f47a..57476f69f 100644 --- a/packages/slate/src/models/text.js +++ b/packages/slate/src/models/text.js @@ -1,7 +1,7 @@ import isPlainObject from 'is-plain-object' import logger from 'slate-dev-logger' -import { List, Record, OrderedSet, is } from 'immutable' +import { List, OrderedSet, Record, Set, is } from 'immutable' import Character from './character' import Mark from './mark' @@ -193,11 +193,25 @@ class Text extends Record(DEFAULTS) { */ addMark(index, length, mark) { + const marks = new Set([mark]) + return this.addMarks(index, length, marks) + } + + /** + * Add a `set` of marks at `index` and `length`. + * + * @param {Number} index + * @param {Number} length + * @param {Set} set + * @return {Text} + */ + + addMarks(index, length, set) { const characters = this.characters.map((char, i) => { if (i < index) return char if (i >= index + length) return char let { marks } = char - marks = marks.add(mark) + marks = marks.union(set) char = char.set('marks', marks) return char }) @@ -206,24 +220,29 @@ class Text extends Record(DEFAULTS) { } /** - * Derive a set of decorated characters with `decorators`. + * Derive a set of decorated characters with `decorations`. * - * @param {Array} decorators + * @param {List} decorations * @return {List} */ - getDecorations(decorators) { - const node = this - let { characters } = node + getDecoratedCharacters(decorations) { + let node = this + const { key, characters } = node + + // PERF: Exit early if there are no characters to be decorated. if (characters.size == 0) return characters - for (let i = 0; i < decorators.length; i++) { - const decorator = decorators[i] - const decorateds = decorator(node) - characters = characters.merge(decorateds) - } + decorations.forEach((range) => { + const { startKey, endKey, startOffset, endOffset, marks } = range + const hasStart = startKey == key + const hasEnd = endKey == key + const index = hasStart ? startOffset : 0 + const length = hasEnd ? endOffset - index : characters.size + node = node.addMarks(index, length, marks) + }) - return characters + return node.characters } /** @@ -233,8 +252,8 @@ class Text extends Record(DEFAULTS) { * @return {Array} */ - getDecorators(schema) { - return schema.__getDecorators(this) + getDecorations(schema) { + return schema.__getDecorations(this) } /** @@ -291,12 +310,12 @@ class Text extends Record(DEFAULTS) { /** * Derive the ranges for a list of `characters`. * - * @param {Array|Void} decorators (optional) + * @param {Array|Void} decorations (optional) * @return {List} */ - getRanges(decorators = []) { - const characters = this.getDecorations(decorators) + getRanges(decorations = []) { + const characters = this.getDecoratedCharacters(decorations) let ranges = [] // PERF: cache previous values for faster lookup. @@ -513,8 +532,8 @@ memoize(Text.prototype, [ }) memoize(Text.prototype, [ + 'getDecoratedCharacters', 'getDecorations', - 'getDecorators', 'getMarksAtIndex', 'getRanges', 'validate' diff --git a/packages/slate/src/operations/apply.js b/packages/slate/src/operations/apply.js index 8df5b1028..783441393 100644 --- a/packages/slate/src/operations/apply.js +++ b/packages/slate/src/operations/apply.js @@ -310,23 +310,6 @@ const APPLIERS = { return state }, - /** - * Set `data` on `state`. - * - * @param {State} state - * @param {Object} operation - * @return {State} - */ - - set_data(state, operation) { - const { properties } = operation - let { data } = state - - data = data.merge(properties) - state = state.set('data', data) - return state - }, - /** * Set `properties` on mark on text at `offset` and `length` in node by `path`. * @@ -359,15 +342,13 @@ const APPLIERS = { let { document } = state let node = document.assertPath(path) - // Warn when trying to overwite a node's children. - if (properties.nodes && properties.nodes != node.nodes) { - logger.warn('Updating a Node\'s `nodes` property via `setNode()` is not allowed. Use the appropriate insertion and removal operations instead. The opeartion in question was:', operation) + if ('nodes' in properties) { + logger.warn('Updating a Node\'s `nodes` property via `setNode()` is not allowed. Use the appropriate insertion and removal methods instead. The operation in question was:', operation) delete properties.nodes } - // Warn when trying to change a node's key. - if (properties.key && properties.key != node.key) { - logger.warn('Updating a Node\'s `key` property via `setNode()` is not allowed. There should be no reason to do this. The opeartion in question was:', operation) + if ('key' in properties) { + logger.warn('Updating a Node\'s `key` property via `setNode()` is not allowed. There should be no reason to do this. The operation in question was:', operation) delete properties.key } @@ -413,6 +394,36 @@ const APPLIERS = { return state }, + /** + * Set `properties` on `state`. + * + * @param {State} state + * @param {Object} operation + * @return {State} + */ + + set_state(state, operation) { + const { properties } = operation + + if ('document' in properties) { + logger.warn('Updating `state.document` property via `setState()` is not allowed. Use the appropriate document updating methods instead. The operation in question was:', operation) + delete properties.document + } + + if ('selection' in properties) { + logger.warn('Updating `state.selection` property via `setState()` is not allowed. Use the appropriate selection updating methods instead. The operation in question was:', operation) + delete properties.selection + } + + if ('history' in properties) { + logger.warn('Updating `state.history` property via `setState()` is not allowed. Use the appropriate history updating methods instead. The operation in question was:', operation) + delete properties.history + } + + state = state.merge(properties) + return state + }, + /** * Split a node by `path` at `offset`. * diff --git a/packages/slate/test/changes/on-state/set-data/simple.js b/packages/slate/test/changes/on-state/set-data/simple.js index fc6e3ea09..db8d1e37a 100644 --- a/packages/slate/test/changes/on-state/set-data/simple.js +++ b/packages/slate/test/changes/on-state/set-data/simple.js @@ -3,7 +3,7 @@ import h from '../../../helpers/h' export default function (change) { - change.setData({ thing: 'value' }) + change.setState({ data: { thing: 'value' }}) } export const input = ( diff --git a/packages/slate/test/serializers/raw/serialize/preserve-state-data.js b/packages/slate/test/serializers/raw/serialize/preserve-state-data.js index d8366985c..00bab32ce 100644 --- a/packages/slate/test/serializers/raw/serialize/preserve-state-data.js +++ b/packages/slate/test/serializers/raw/serialize/preserve-state-data.js @@ -42,5 +42,5 @@ export const output = { } export const options = { - preserveStateData: true, + preserveData: true, } diff --git a/yarn.lock b/yarn.lock index f3ca7ff8c..a7711bfe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5016,6 +5016,10 @@ react-frame-component@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-1.1.1.tgz#05b7f5689a2d373f25baf0c9adb0e59d78103388" +react-immutable-proptypes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" + react-portal@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.1.0.tgz#865c44fb72a1da106c649206936559ce891ee899"