From 270c2ab2195c430413b3fbac39379811d41b0798 Mon Sep 17 00:00:00 2001 From: Nikita Zyulyaev Date: Wed, 26 Oct 2016 21:30:45 +0300 Subject: [PATCH 1/5] Fix inserting fragment at the end of current text node (#405) * fix insertFragment * insert multi-block fragment at the end of current text node test case --- src/transforms/at-current-range.js | 7 +--- .../end-block-multiple-blocks/fragment.yaml | 17 ++++++++ .../end-block-multiple-blocks/index.js | 41 +++++++++++++++++++ .../end-block-multiple-blocks/input.yaml | 7 ++++ .../end-block-multiple-blocks/output.yaml | 17 ++++++++ 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/fragment.yaml create mode 100644 test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/index.js create mode 100644 test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/input.yaml create mode 100644 test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/output.yaml diff --git a/src/transforms/at-current-range.js b/src/transforms/at-current-range.js index 767247091..fad058576 100644 --- a/src/transforms/at-current-range.js +++ b/src/transforms/at-current-range.js @@ -262,6 +262,7 @@ export function insertFragment(transform, fragment) { const lastText = fragment.getTexts().last() const lastInline = fragment.getClosestInline(lastText) const beforeTexts = document.getTexts() + const appending = selection.hasEdgeAtEndOf(document.getDescendant(selection.endKey)) transform.unsetSelection() transform.insertFragmentAtRange(selection, fragment) @@ -270,17 +271,13 @@ export function insertFragment(transform, fragment) { const keys = beforeTexts.map(text => text.key) const news = document.getTexts().filter(n => !keys.includes(n.key)) - const text = news.size ? news.takeLast(2).first() : null + const text = appending ? news.last() : news.takeLast(2).first() let after if (text && lastInline) { after = selection.collapseToEndOf(text) } - else if (text && lastInline) { - after = selection.collapseToStart() - } - else if (text) { after = selection .collapseToStartOf(text) diff --git a/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/fragment.yaml b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/fragment.yaml new file mode 100644 index 000000000..e1fea2c54 --- /dev/null +++ b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/fragment.yaml @@ -0,0 +1,17 @@ + +nodes: + - kind: block + type: list-item + nodes: + - kind: text + text: fragment + - kind: block + type: list-item + nodes: + - kind: text + text: second fragment + - kind: block + type: list-item + nodes: + - kind: text + text: third fragment diff --git a/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/index.js b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/index.js new file mode 100644 index 000000000..79217cfcb --- /dev/null +++ b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/index.js @@ -0,0 +1,41 @@ + +import assert from 'assert' +import path from 'path' +import readMetadata from 'read-metadata' +import { Raw } from '../../../../../..' + +export default function (state) { + const file = path.resolve(__dirname, 'fragment.yaml') + const raw = readMetadata.sync(file) + const fragment = Raw.deserialize(raw, { terse: true }).document + + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: first.length, + focusKey: first.key, + focusOffset: first.length + }) + + const next = state + .transform() + .moveTo(range) + .insertFragment(fragment) + .apply() + + const last = next.document.getTexts().last() + + assert.deepEqual( + next.selection.toJS(), + range.merge({ + anchorKey: last.key, + anchorOffset: last.length, + focusKey: last.key, + focusOffset: last.length + }).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/input.yaml b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/input.yaml new file mode 100644 index 000000000..27f668fe2 --- /dev/null +++ b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/input.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/output.yaml b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/output.yaml new file mode 100644 index 000000000..66415017e --- /dev/null +++ b/test/transforms/fixtures/at-current-range/insert-fragment/end-block-multiple-blocks/output.yaml @@ -0,0 +1,17 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: wordfragment + - kind: block + type: list-item + nodes: + - kind: text + text: second fragment + - kind: block + type: list-item + nodes: + - kind: text + text: third fragment \ No newline at end of file From dca60c42ce7d552915aff8bc61005ebd15ac66ed Mon Sep 17 00:00:00 2001 From: jasonphillips Date: Wed, 26 Oct 2016 14:46:24 -0500 Subject: [PATCH 2/5] Html serializer: optionally return React elements (#408) * Html serializer optionally returns React elements * update heredoc to indicate optional return value of array * update documentation for html serializer to include React return option * move returnElements argument to render:false --- docs/reference/serializers/html.md | 6 +++--- src/serializers/Readme.md | 5 +++-- src/serializers/html.js | 8 ++++++-- test/serializers/index.js | 11 +++++++++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/reference/serializers/html.md b/docs/reference/serializers/html.md index 6c73070a9..62f77167d 100644 --- a/docs/reference/serializers/html.md +++ b/docs/reference/serializers/html.md @@ -51,9 +51,9 @@ An array of rules to initialize the `Html` serializer with, defining your schema Deserialize an HTML `string` into a [`State`](../models/state.md). How the string is deserialized will be determined by the rules that the `Html` serializer was constructed with. ### `Html.serialize` -`Html.serialize(state: State) => String` +`Html.serialize(state: State, [options: Object]) => String || Array` -Serialize a `state` into an HTML string. How the string is serialized will be determined by the rules that the `Html` serializer was constructed with. +Serialize a `state` into an HTML string. How the string is serialized will be determined by the rules that the `Html` serializer was constructed with. If you pass `render: false` as an option, the return value will instead be an iterable list of the top-level React elements, to be rendered as children in your own React component. ## Rules @@ -73,7 +73,7 @@ Each rule must define two properties: #### `rule.deserialize` `rule.deserialize(el: CheerioElement, next: Function) => Object || Void` -The `deserialize` function should return a plain Javascript object representing the deserialized state, or nothing if the rule in question doesn't know how to deserialize the object, in which case the next rule in the stack will be attempted. +The `deserialize` function should return a plain Javascript object representing the deserialized state, or nothing if the rule in question doesn't know how to deserialize the object, in which case the next rule in the stack will be attempted. The returned object is almost exactly equivalent to the objects returned by the [`Raw`](./raw.md) serializer, except an extra `kind: 'mark'` is added to account for the ability to nest marks. diff --git a/src/serializers/Readme.md b/src/serializers/Readme.md index 7130a0ed9..2ade83fac 100644 --- a/src/serializers/Readme.md +++ b/src/serializers/Readme.md @@ -8,10 +8,12 @@ This directory contains the serializers that ship by default with Slate. They ar The `Html` serializer offers a simple way to serialize and deserialize an HTML schema of your choosing. -It doesn't hardcode any information about the schema itself (like which tag means "bold"), but allows you to build up a simple HTML serializer for your own use case. +It doesn't hardcode any information about the schema itself (like which tag means "bold"), but allows you to build up a simple HTML serializer for your own use case. It handles all of the heavy lifting of actually parsing the HTML, and iterating over the elements, and all you have to supply it is a `serialize()` and `deserialize()` function for each type of [`Node`](../models#node) or [`Mark`](../models/#mark) you want it to handle. +If called with `{render: false}` as the optional second argument, the serializer will return an iterable list of the top-level React elements generated, instead of automatically rendering these to a markup string. + #### Raw @@ -20,4 +22,3 @@ The `Raw` serializer is the simplest serializer, which translates a [`State`](.. It doesn't just use Immutable.js's [`.toJSON()`](https://facebook.github.io/immutable-js/docs/#/List/toJS) method. Instead, it performs a little bit of "minifying" logic to reduce unnecessary information from being in the raw output. It also transforms [`Text`](../models#text) nodes's content from being organized by [`Characters`](../models#character) into the concept of "ranges", which have a unique set of [`Marks`](../models#mark). - diff --git a/src/serializers/html.js b/src/serializers/html.js index 56980f528..6ddc946fd 100644 --- a/src/serializers/html.js +++ b/src/serializers/html.js @@ -231,12 +231,16 @@ class Html { * Serialize a `state` object into an HTML string. * * @param {State} state - * @return {String} html + * @param {Object} options + * @property {Boolean} render + * @return {String|Array} html */ - serialize = (state) => { + serialize = (state, options = {}) => { const { document } = state const elements = document.nodes.map(this.serializeNode) + if (options.render === false) return elements + const html = ReactDOMServer.renderToStaticMarkup({elements}) const inner = html.slice(6, -7) return inner diff --git a/test/serializers/index.js b/test/serializers/index.js index 95e08aafe..79a372611 100644 --- a/test/serializers/index.js +++ b/test/serializers/index.js @@ -1,11 +1,14 @@ import assert from 'assert' +import type from 'type-of' import fs from 'fs' import readMetadata from 'read-metadata' import strip from '../helpers/strip-dynamic' import { Html, Json, Plain, Raw } from '../..' import { equal, strictEqual } from '../helpers/assert-json' import { resolve } from 'path' +import React from 'react' +import { Iterable } from 'immutable' /** * Tests. @@ -46,6 +49,14 @@ describe('serializers', () => { strictEqual(serialized, expected.trim()) }) } + + it('optionally returns an iterable list of React elements', () => { + const html = new Html(require('./fixtures/html/serialize/block-nested').default) + const input = require('./fixtures/html/serialize/block-nested/input.js').default + const serialized = html.serialize(input, { render: false }) + assert(Iterable.isIterable(serialized), 'did not return an interable list') + assert(React.isValidElement(serialized.first()), 'did not return valid React elements') + }) }) }) From 8851855b5b666cc6b178427de2899f8e509ab81a Mon Sep 17 00:00:00 2001 From: Nicolas Gaborit Date: Wed, 26 Oct 2016 21:46:40 +0200 Subject: [PATCH 3/5] Node methods optimizations (#364) * Node.getParent exits earlier * Add Node.getAncestors method * Remove numerous getParent in Node.getClosest * Remove use of assertDescendant in getPath Still throws when not finding the descendant though * Remove assertDescendant from Node.updateDescendant * Remove assertDescendant from Node.removeDescendant * Fix Node.findDescendant, which always returned first level descendants * Add Node.findDescendantDeep * Memoize Node.getAncestors * Implement and use Node.get{First|Last}Text * Add jsdom devDepencency Required as peer dependency by mocha-jsdom --- package.json | 1 + src/components/void.js | 2 +- src/models/node.js | 195 ++++++++++++++++++++++------- src/models/selection.js | 12 +- src/models/state.js | 2 +- src/transforms/at-current-range.js | 10 +- 6 files changed, 164 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 1938cdedd..4bc976730 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "http-server": "^0.9.0", "is-image": "^1.0.1", "is-url": "^1.2.2", + "jsdom": "9.8.0", "jsdom": "9.6.0", "jsdom-global": "2.1.0", "microtime": "2.1.1", diff --git a/src/components/void.js b/src/components/void.js index 22a610d5f..b727c35be 100644 --- a/src/components/void.js +++ b/src/components/void.js @@ -130,7 +130,7 @@ class Void extends React.Component { renderLeaf = () => { const { node, schema, state } = this.props - const child = node.getTexts().first() + const child = node.getFirstText() const ranges = child.getRanges() const text = '' const marks = Mark.createSet() diff --git a/src/models/node.js b/src/models/node.js index 428b70260..5472c7a85 100644 --- a/src/models/node.js +++ b/src/models/node.js @@ -105,23 +105,53 @@ const Node = { }, /** - * Recursively find all ancestor nodes by `iterator`. + * Recursively find all descendant nodes by `iterator`. Breadth first. * * @param {Function} iterator - * @return {Node} node + * @return {Node or Null} node */ findDescendant(iterator) { - return ( - this.nodes.find(iterator) || - this.nodes - .map(node => node.kind == 'text' ? null : node.findDescendant(iterator)) - .find(exists => exists) - ) + const found = this.nodes.find(iterator) + if (found) return found + + let descendantFound = null + this.nodes.find(node => { + if (node.kind != 'text') { + descendantFound = node.findDescendant(iterator) + return descendantFound + } else { + return false + } + }) + + return descendantFound }, /** - * Recursively filter all ancestor nodes with `iterator`. + * Recursively find all descendant nodes by `iterator`. Depth first. + * + * @param {Function} iterator + * @return {Node or Null} node + */ + + findDescendantDeep(iterator) { + let descendantFound = null + + const found = this.nodes.find(node => { + if (node.kind != 'text') { + descendantFound = node.findDescendantDeep(iterator) + return descendantFound || iterator(node) + } + + return iterator(node) ? node : null + }) + + return descendantFound || found + }, + + /** + * Recursively filter all descendant nodes with `iterator`. * * @param {Function} iterator * @return {List} nodes @@ -136,7 +166,7 @@ const Node = { }, /** - * Recursively filter all ancestor nodes with `iterator`, depth-first. + * Recursively filter all descendant nodes with `iterator`, depth-first. * * @param {Function} iterator * @return {List} nodes @@ -286,14 +316,13 @@ const Node = { */ getClosest(key, iterator) { - let node = this.assertDescendant(key) - - while (node = this.getParent(node)) { - if (node == this) return null - if (iterator(node)) return node + let ancestors = this.getAncestors(key) + if (!ancestors) { + throw new Error(`Could not find a descendant node with key "${key}".`) } - return null + // Exclude this node itself + return ancestors.rest().findLast(iterator) }, /** @@ -410,16 +439,7 @@ const Node = { getDescendant(key) { key = Normalize.key(key) - let child = this.getChild(key) - if (child) return child - - this.nodes.find((node) => { - if (node.kind == 'text') return false - child = node.getDescendant(key) - return child - }) - - return child + return this.findDescendantDeep(node => node.key == key) }, /** @@ -654,10 +674,10 @@ const Node = { let last if (child.kind == 'block') { - last = child.getTexts().last() + last = child.getLastText() } else { const block = this.getClosestBlock(key) - last = block.getTexts().last() + last = block.getLastText() } const next = this.getNextText(last) @@ -748,10 +768,13 @@ const Node = { let node = null - this.nodes.forEach((child) => { - if (child.kind == 'text') return - const match = child.getParent(key) - if (match) node = match + this.nodes.find((child) => { + if (child.kind == 'text') { + return false + } else { + node = child.getParent(key) + return node + } }) return node @@ -760,7 +783,7 @@ const Node = { /** * Get the path of a descendant node by `key`. * - * @param {String || Node} node + * @param {String || Node} key * @return {Array} */ @@ -769,17 +792,50 @@ const Node = { if (key == this.key) return [] - let child = this.assertDescendant(key) let path = [] + let childKey = key let parent - while (parent = this.getParent(child)) { - const index = parent.nodes.indexOf(child) + // Efficient with getParent memoization + while (parent = this.getParent(childKey)) { + const index = parent.nodes.findIndex(n => n.key === childKey) path.unshift(index) - child = parent + childKey = parent.key } - return path + if (childKey === key) { + // Did not loop once, meaning we could not find the child + throw new Error(`Could not find a descendant node with key "${key}".`) + } else { + return path + } + }, + + /** + * Get the path of ancestors of a descendant node by `key`. + * + * @param {String || Node} node + * @return {List or Null} + */ + + getAncestors(key) { + key = Normalize.key(key) + + if (key == this.key) return List() + if (this.hasChild(key)) return List([this]) + + let ancestors + this.nodes.find((node) => { + if (node.kind == 'text') return false + ancestors = node.getAncestors(key) + return ancestors + }) + + if (ancestors) { + return ancestors.unshift(this) + } else { + return null + } }, /** @@ -824,10 +880,10 @@ const Node = { let first if (child.kind == 'block') { - first = child.getTexts().first() + first = child.getFirstText() } else { const block = this.getClosestBlock(key) - first = block.getTexts().first() + first = block.getFirstText() } const previous = this.getPreviousText(first) @@ -881,6 +937,34 @@ const Node = { }, Block.createList()) }, + /** + * Get the first child text node. + * + * @return {Node || Null} node + */ + + getFirstText() { + return this.findDescendantDeep(node => node.kind == 'text') + }, + + /** + * Get the last child text node. + * + * @return {Node} node + */ + + getLastText() { + let descendantFound = null + + const found = this.nodes.findLast((node) => { + if (node.kind == 'text') return true + descendantFound = node.getLastText() + return descendantFound + }) + + return descendantFound || found + }, + /** * Get all of the text nodes in a `range`. * @@ -1163,10 +1247,13 @@ const Node = { */ removeDescendant(key) { + key = Normalize.key(key) + let node = this - const desc = node.assertDescendant(key) - let parent = node.getParent(desc) - const index = parent.nodes.indexOf(desc) + let parent = node.getParent(key) + if (!parent) throw new Error(`Could not find a descendant node with key "${key}".`) + + const index = parent.nodes.findIndex(n => n.key === key) const isParent = node == parent const nodes = parent.nodes.splice(index, 1) @@ -1277,8 +1364,22 @@ const Node = { */ updateDescendant(node) { - this.assertDescendant(node) - return this.mapDescendants(d => d.key == node.key ? node : d) + let found = false + + const result = this.mapDescendants(d => { + if (d.key == node.key) { + found = true + return node + } else { + return d + } + }) + + if (!found) { + throw new Error(`Could not update descendant node with key "${node.key}".`) + } else { + return result + } }, /** @@ -1304,6 +1405,8 @@ memoize(Node, [ 'filterDescendants', 'filterDescendantsDeep', 'findDescendant', + 'findDescendantDeep', + 'getAncestors', 'getBlocks', 'getBlocksAtRange', 'getCharactersAtRange', @@ -1322,6 +1425,7 @@ memoize(Node, [ 'getDepth', 'getDescendant', 'getDescendantDecorators', + 'getFirstText', 'getFragmentAtRange', 'getFurthest', 'getFurthestBlock', @@ -1329,6 +1433,7 @@ memoize(Node, [ 'getHighestChild', 'getHighestOnlyChildParent', 'getInlinesAtRange', + 'getLastText', 'getMarksAtRange', 'getNextBlock', 'getNextSibling', diff --git a/src/models/selection.js b/src/models/selection.js index c16e1be79..560658043 100644 --- a/src/models/selection.js +++ b/src/models/selection.js @@ -148,7 +148,7 @@ class Selection extends new Record(DEFAULTS) { hasAnchorAtStartOf(node) { if (this.anchorOffset != 0) return false - const first = node.kind == 'text' ? node : node.getTexts().first() + const first = node.kind == 'text' ? node : node.getFirstText() return this.anchorKey == first.key } @@ -160,7 +160,7 @@ class Selection extends new Record(DEFAULTS) { */ hasAnchorAtEndOf(node) { - const last = node.kind == 'text' ? node : node.getTexts().last() + const last = node.kind == 'text' ? node : node.getLastText() return this.anchorKey == last.key && this.anchorOffset == last.length } @@ -202,7 +202,7 @@ class Selection extends new Record(DEFAULTS) { */ hasFocusAtEndOf(node) { - const last = node.kind == 'text' ? node : node.getTexts().last() + const last = node.kind == 'text' ? node : node.getLastText() return this.focusKey == last.key && this.focusOffset == last.length } @@ -215,7 +215,7 @@ class Selection extends new Record(DEFAULTS) { hasFocusAtStartOf(node) { if (this.focusOffset != 0) return false - const first = node.kind == 'text' ? node : node.getTexts().first() + const first = node.kind == 'text' ? node : node.getFirstText() return this.focusKey == first.key } @@ -260,7 +260,7 @@ class Selection extends new Record(DEFAULTS) { const { isExpanded, startKey, startOffset } = this if (isExpanded) return false if (startOffset != 0) return false - const first = node.kind == 'text' ? node : node.getTexts().first() + const first = node.kind == 'text' ? node : node.getFirstText() return startKey == first.key } @@ -274,7 +274,7 @@ class Selection extends new Record(DEFAULTS) { isAtEndOf(node) { const { endKey, endOffset, isExpanded } = this if (isExpanded) return false - const last = node.kind == 'text' ? node : node.getTexts().last() + const last = node.kind == 'text' ? node : node.getLastText() return endKey == last.key && endOffset == last.length } diff --git a/src/models/state.js b/src/models/state.js index 56ead2f7e..6e1725c1f 100644 --- a/src/models/state.js +++ b/src/models/state.js @@ -47,7 +47,7 @@ class State extends new Record(DEFAULTS) { let selection = Selection.create(properties.selection) if (selection.isUnset) { - const text = document.getTexts().first() + const text = document.getFirstText() selection = selection.collapseToStartOf(text) } diff --git a/src/transforms/at-current-range.js b/src/transforms/at-current-range.js index fad058576..4d376b703 100644 --- a/src/transforms/at-current-range.js +++ b/src/transforms/at-current-range.js @@ -70,7 +70,7 @@ export function _delete(transform) { after = selection.collapseToEndOf(previous) } } else { - const last = previous.getTexts().last() + const last = previous.getLastText() after = selection.collapseToEndOf(last) } } @@ -143,7 +143,7 @@ export function deleteBackward(transform, n = 1) { after = selection.collapseToEndOf(previous) } } else { - const last = previous.getTexts().last() + const last = previous.getLastText() after = selection.collapseToEndOf(last) } } else { @@ -206,7 +206,7 @@ export function deleteForward(transform, n = 1) { after = selection.collapseToEndOf(previous) } } else { - const last = previous.getTexts().last() + const last = previous.getLastText() after = selection.collapseToEndOf(last) } } @@ -259,7 +259,7 @@ export function insertFragment(transform, fragment) { if (!fragment.length) return transform - const lastText = fragment.getTexts().last() + const lastText = fragment.getLastText() const lastInline = fragment.getClosestInline(lastText) const beforeTexts = document.getTexts() const appending = selection.hasEdgeAtEndOf(document.getDescendant(selection.endKey)) @@ -575,7 +575,7 @@ export function wrapInline(transform, properties) { } else if (selection.startOffset == 0) { - const text = previous ? document.getNextText(previous) : document.getTexts().first() + const text = previous ? document.getNextText(previous) : document.getFirstText() after = selection.moveToRangeOf(text) } From db95f4bdec0c2d13d3b268c9788bbaa705583655 Mon Sep 17 00:00:00 2001 From: Tyler Johnson Date: Wed, 26 Oct 2016 13:59:03 -0600 Subject: [PATCH 4/5] fix for deleting across an inline (#384) * failing test for deleting across an inline * fix deleting across inlines (#384) * Add jsdom devDepencency Required as peer dependency by mocha-jsdom --- src/transforms/at-range.js | 33 +++++++++++-------- .../across-inline-texts/index.js | 18 ++++++++++ .../across-inline-texts/input.yaml | 14 ++++++++ .../across-inline-texts/output.yaml | 7 ++++ 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/index.js create mode 100644 test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/input.yaml create mode 100644 test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/output.yaml diff --git a/src/transforms/at-range.js b/src/transforms/at-range.js index 28f415c86..c3a5041fc 100644 --- a/src/transforms/at-range.js +++ b/src/transforms/at-range.js @@ -61,39 +61,46 @@ export function deleteAtRange(transform, range) { let { state } = transform let { document } = state + + // split the nodes at range, within the common ancestor let ancestor = document.getCommonAncestor(startKey, endKey) let startChild = ancestor.getHighestChild(startKey) let endChild = ancestor.getHighestChild(endKey) - const startOff = startChild.getOffset(startKey) + startOffset - const endOff = endChild.getOffset(endKey) + endOffset + const startOff = (startChild.key === startKey ? 0 : startChild.getOffset(startKey)) + startOffset + const endOff = (endChild.key === endKey ? 0 : endChild.getOffset(endKey)) + endOffset transform.splitNodeByKey(startChild.key, startOff) transform.splitNodeByKey(endChild.key, endOff) state = transform.state document = state.document - ancestor = document.getCommonAncestor(startKey, endKey) const startBlock = document.getClosestBlock(startKey) const endBlock = document.getClosestBlock(document.getNextText(endKey)) - startChild = ancestor.getHighestChild(startBlock) - endChild = ancestor.getHighestChild(endBlock) + // remove all of the nodes between range + ancestor = document.getCommonAncestor(startKey, endKey) + startChild = ancestor.getHighestChild(startKey) + endChild = ancestor.getHighestChild(endKey) const startIndex = ancestor.nodes.indexOf(startChild) const endIndex = ancestor.nodes.indexOf(endChild) - const middles = ancestor.nodes.slice(startIndex + 1, endIndex) + const middles = ancestor.nodes.slice(startIndex + 1, endIndex + 1) middles.forEach((child) => { transform.removeNodeByKey(child.key) }) - endBlock.nodes.forEach((child, i) => { - const newKey = startBlock.key - const newIndex = startBlock.nodes.size + i - transform.moveNodeByKey(child.key, newKey, newIndex) - }) + // "normalize" the document so blocks in the range are also removed + if (startBlock.key !== endBlock.key) { + endBlock.nodes.forEach((child, i) => { + const newKey = startBlock.key + const newIndex = startBlock.nodes.size + i + transform.moveNodeByKey(child.key, newKey, newIndex) + }) + + const lonely = document.getFurthest(endBlock, p => p.nodes.size == 1) || endBlock + transform.removeNodeByKey(lonely.key) + } - const lonely = document.getFurthest(endBlock, p => p.nodes.size == 1) || endBlock - transform.removeNodeByKey(lonely.key) transform.normalizeDocument() return transform } diff --git a/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/index.js b/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/index.js new file mode 100644 index 000000000..4fb524e62 --- /dev/null +++ b/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/index.js @@ -0,0 +1,18 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const last = texts.last() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 1, + focusKey: last.key, + focusOffset: last.length - 1 + }) + + return state + .transform() + .deleteAtRange(range) + .apply() +} diff --git a/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/input.yaml b/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/input.yaml new file mode 100644 index 000000000..1a7131b75 --- /dev/null +++ b/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/input.yaml @@ -0,0 +1,14 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: before + - kind: inline + type: link + nodes: + - kind: text + text: word + - kind: text + text: after diff --git a/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/output.yaml b/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/output.yaml new file mode 100644 index 000000000..33577c937 --- /dev/null +++ b/test/transforms/fixtures/at-range/delete-at-range/across-inline-texts/output.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: "br" From d4eb53fd6f62c4642ee24425ceebdf394eebde11 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Wed, 26 Oct 2016 13:02:01 -0700 Subject: [PATCH 5/5] 0.14.16 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 4bc976730..f9a5c6a42 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "slate", "description": "A completely customizable framework for building rich text editors.", - "version": "0.14.15", + "version": "0.14.16", "license": "MIT", "repository": "git://github.com/ianstormtaylor/slate.git", "main": "./lib/index.js", @@ -47,7 +47,6 @@ "http-server": "^0.9.0", "is-image": "^1.0.1", "is-url": "^1.2.2", - "jsdom": "9.8.0", "jsdom": "9.6.0", "jsdom-global": "2.1.0", "microtime": "2.1.1",