From 9499b9188bf66accc532806692dd2fccf5bf1e20 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Wed, 20 Jul 2016 14:13:29 -0700 Subject: [PATCH] change unwrapBlock to operate only on the siblings in a range --- History.md | 10 +++ examples/rich-text/index.js | 33 +++++--- lib/models/transforms.js | 77 +++++++++++++++---- .../join-nested-blocks/index.js | 17 ++++ .../join-nested-blocks/input.yaml | 17 ++++ .../join-nested-blocks/output.yaml | 11 +++ .../join-nested-blocks/index.js | 17 ++++ .../join-nested-blocks/input.yaml | 17 ++++ .../join-nested-blocks/output.yaml | 11 +++ .../ending-child-blocks/index.js | 18 +++++ .../ending-child-blocks/input.yaml | 41 ++++++++++ .../ending-child-blocks/output.yaml | 41 ++++++++++ .../middle-child-blocks/index.js | 18 +++++ .../middle-child-blocks/input.yaml | 41 ++++++++++ .../middle-child-blocks/output.yaml | 44 +++++++++++ .../starting-child-blocks/index.js | 18 +++++ .../starting-child-blocks/input.yaml | 41 ++++++++++ .../starting-child-blocks/output.yaml | 41 ++++++++++ 18 files changed, 486 insertions(+), 27 deletions(-) create mode 100644 test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/index.js create mode 100644 test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/input.yaml create mode 100644 test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/output.yaml create mode 100644 test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/index.js create mode 100644 test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/input.yaml create mode 100644 test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/output.yaml create mode 100644 test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/index.js create mode 100644 test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/input.yaml create mode 100644 test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/output.yaml create mode 100644 test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/index.js create mode 100644 test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/input.yaml create mode 100644 test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/output.yaml create mode 100644 test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/index.js create mode 100644 test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/input.yaml create mode 100644 test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/output.yaml diff --git a/History.md b/History.md index 949fa64b8..fc6a055d0 100644 --- a/History.md +++ b/History.md @@ -2,6 +2,16 @@ This document maintains a list of changes to Slate with each new version. Until `1.0.0` is released, breaking changes will be added as minor version bumps, and non-breaking changes won't be accounted for since the library is moving quickly. +## `0.3.0` +_July 20, 2016_ + +###### BREAKING CHANGES + +- Changed `unwrapBlock` to unwrap selectively. Previously, calling `unwrapBlock` with a range representing a middle sibling would unwrap _all_ of the siblings, removing the wrapping block entirely. Now, calling it with those same arguments will only move the middle sibling up a layer in the hierarchy, preserving the nesting on any of its siblings. + +This changes makes it much simpler to implement functionality like unwrapping a single list item, which previously would unwrap the entire list. + + ## `0.2.0` _July 18, 2016_ diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index bc5703504..9aa397338 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -99,6 +99,7 @@ class RichText extends React.Component { onChange = (state) => { this.setState({ state }) + console.log(state.document.toJS()) } /** @@ -169,19 +170,32 @@ class RichText extends React.Component { onClickBlock = (e, type) => { e.preventDefault() - const isActive = this.hasBlock(type) let { state } = this.state + let transform = state.transform() + const { document } = state - let transform = state - .transform() - .setBlock(isActive ? 'paragraph' : type) + // Handle everything but list buttons. + if (type != 'bulleted-list' && type != 'numbered-list') { + const isActive = this.hasBlock(type) + transform = transform.setBlock(isActive ? DEFAULT_NODE : type) + } // Handle the extra wrapping required for list buttons. - if (type == 'bulleted-list' || type == 'numbered-list') { - if (this.hasBlock('list-item')) { + else { + const isList = this.hasBlock('list-item') + const isType = state.blocks.some((block) => { + return !!document.getClosest(block, parent => parent.type == type) + }) + + if (isList && isType) { transform = transform .setBlock(DEFAULT_NODE) - .unwrapBlock(type) + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list') + } else if (isList) { + transform = transform + .unwrapBlock(type == 'bulleted-list' ? 'numbered-list' : 'bulleted-list') + .wrapBlock(type) } else { transform = transform .setBlock('list-item') @@ -189,11 +203,6 @@ class RichText extends React.Component { } } - // Handle everything but list buttons. - else { - transform = transform.setBlock(isActive ? DEFAULT_NODE : type) - } - state = transform.apply() this.setState({ state }) } diff --git a/lib/models/transforms.js b/lib/models/transforms.js index 9e106f263..5de183b45 100644 --- a/lib/models/transforms.js +++ b/lib/models/transforms.js @@ -66,7 +66,7 @@ const Transforms = { node = node.merge({ nodes }) // Take the end edge's split text and move it to the start edge. - let startBlock = node.getFurthestBlock(startText) + let startBlock = node.getClosestBlock(startText) let endChild = node.getFurthestInline(endText) || endText const startNodes = startBlock.nodes.push(endChild) @@ -426,12 +426,14 @@ const Transforms = { let firstChild = node.getFurthestInline(firstText) || firstText let secondChild = node.getFurthestInline(secondText) || secondText let parent = node.getClosestBlock(firstChild) - let firstChildren = parent.nodes.takeUntil(n => n == firstChild).push(firstChild) - let secondChildren = parent.nodes.skipUntil(n => n == secondChild) + let firstChildren + let secondChildren let d = 0 // While the parent is a block, split the block nodes. while (parent && d < depth) { + firstChildren = parent.nodes.takeUntil(n => n == firstChild).push(firstChild) + secondChildren = parent.nodes.skipUntil(n => n == secondChild) firstChild = parent.merge({ nodes: firstChildren }) secondChild = Block.create({ nodes: secondChildren, @@ -439,9 +441,6 @@ const Transforms = { data: parent.data }) - firstChildren = Block.createList([firstChild]) - secondChildren = Block.createList([secondChild]) - // Add the new children. const grandparent = node.getParent(parent) const nodes = grandparent.nodes @@ -622,9 +621,11 @@ const Transforms = { // Ensure that data is immutable. if (data) data = Data.create(data) - // Find the closest wrapping blocks of each text node. - const texts = node.getBlocksAtRange(range) - const wrappers = texts.reduce((memo, text) => { + // Get the deepest blocks in the range. + const blocks = node.getBlocksAtRange(range) + + // Get the matching wrapper blocks. + const wrappers = blocks.reduce((memo, text) => { const match = node.getClosest(text, (parent) => { if (parent.kind != 'block') return false if (type && parent.type != type) return false @@ -636,16 +637,62 @@ const Transforms = { return memo }, new Set()) - // Replace each of the wrappers with their child nodes. + // For each of the wrapper blocks... wrappers.forEach((wrapper) => { + const first = wrapper.nodes.first() + const last = wrapper.nodes.last() const parent = node.getParent(wrapper) - // Replace the wrapper in the parent's nodes with the block. - const nodes = parent.nodes.takeUntil(n => n == wrapper) - .concat(wrapper.nodes) - .concat(parent.nodes.skipUntil(n => n == wrapper).rest()) + // Get the wrapped direct children. + const children = wrapper.nodes.filter((child) => { + return blocks.some(block => child == block || child.hasDescendant(block)) + }) + + // Determine what the new nodes should be... + const firstMatch = children.first() + const lastMatch = children.last() + let nodes + + // If the first and last both match, remove the wrapper completely. + if (first == firstMatch && last == lastMatch) { + nodes = parent.nodes.takeUntil(n => n == wrapper) + .concat(wrapper.nodes) + .concat(parent.nodes.skipUntil(n => n == wrapper).rest()) + } + + // If only the last child matches, move the last nodes. + else if (last == lastMatch) { + const remain = wrapper.nodes.takeUntil(n => n == firstMatch) + const updated = wrapper.merge({ nodes: remain }) + nodes = parent.nodes.takeUntil(n => n == wrapper) + .push(updated) + .concat(children) + .concat(parent.nodes.skipUntil(n => n == wrapper).rest()) + } + + // If only the first child matches, move the first ones. + else if (first == firstMatch) { + const remain = wrapper.nodes.skipUntil(n => n == lastMatch).rest() + const updated = wrapper.merge({ nodes: remain }) + nodes = parent.nodes.takeUntil(n => n == wrapper) + .concat(children) + .push(updated) + .concat(parent.nodes.skipUntil(n => n == wrapper).rest()) + } + + // Otherwise, move the middle ones. + else { + const firsts = wrapper.nodes.takeUntil(n => n == firstMatch) + const lasts = wrapper.nodes.skipUntil(n => n == lastMatch).rest() + const updatedFirst = wrapper.merge({ nodes: firsts }) + const updatedLast = wrapper.merge({ nodes: lasts }) + nodes = parent.nodes.takeUntil(n => n == wrapper) + .push(updatedFirst) + .concat(children) + .push(updatedLast) + .concat(parent.nodes.skipUntil(n => n == wrapper).rest()) + } - // Update the parent. node = parent == node ? node.merge({ nodes }) : node.updateDescendant(parent.merge({ nodes })) diff --git a/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/index.js b/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/index.js new file mode 100644 index 000000000..6c314641d --- /dev/null +++ b/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/index.js @@ -0,0 +1,17 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTextNodes() + const second = texts.last() + const range = selection.merge({ + anchorKey: second.key, + anchorOffset: 0, + focusKey: second.key, + focusOffset: 0 + }) + + return state + .transform() + .deleteBackwardAtRange(range) + .apply() +} diff --git a/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/input.yaml b/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/input.yaml new file mode 100644 index 000000000..87b381f13 --- /dev/null +++ b/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/input.yaml @@ -0,0 +1,17 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: word + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: another diff --git a/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/output.yaml b/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/output.yaml new file mode 100644 index 000000000..45d48e74b --- /dev/null +++ b/test/transforms/fixtures/delete-backward-at-range/join-nested-blocks/output.yaml @@ -0,0 +1,11 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: wordanother diff --git a/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/index.js b/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/index.js new file mode 100644 index 000000000..a9306c233 --- /dev/null +++ b/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/index.js @@ -0,0 +1,17 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTextNodes() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: first.length, + focusKey: first.key, + focusOffset: first.length + }) + + return state + .transform() + .deleteForwardAtRange(range) + .apply() +} diff --git a/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/input.yaml b/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/input.yaml new file mode 100644 index 000000000..87b381f13 --- /dev/null +++ b/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/input.yaml @@ -0,0 +1,17 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: word + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: another diff --git a/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/output.yaml b/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/output.yaml new file mode 100644 index 000000000..45d48e74b --- /dev/null +++ b/test/transforms/fixtures/delete-forward-at-range/join-nested-blocks/output.yaml @@ -0,0 +1,11 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: wordanother diff --git a/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/index.js b/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/index.js new file mode 100644 index 000000000..2ae964f5e --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/index.js @@ -0,0 +1,18 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTextNodes() + const fifth = texts.get(4) + const sixth = texts.get(5) + const range = selection.merge({ + anchorKey: fifth.key, + anchorOffset: 0, + focusKey: sixth.key, + focusOffset: 0 + }) + + return state + .transform() + .unwrapBlockAtRange(range, 'quote') + .apply() +} diff --git a/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/input.yaml b/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/input.yaml new file mode 100644 index 000000000..1279141e4 --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/input.yaml @@ -0,0 +1,41 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: one + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: two + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: three + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: four + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: five + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: six diff --git a/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/output.yaml b/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/output.yaml new file mode 100644 index 000000000..395624841 --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/ending-child-blocks/output.yaml @@ -0,0 +1,41 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: one + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: two + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: three + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: four + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: five + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: six diff --git a/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/index.js b/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/index.js new file mode 100644 index 000000000..b516268d3 --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/index.js @@ -0,0 +1,18 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTextNodes() + const third = texts.get(2) + const fourth = texts.get(3) + const range = selection.merge({ + anchorKey: third.key, + anchorOffset: 0, + focusKey: fourth.key, + focusOffset: 0 + }) + + return state + .transform() + .unwrapBlockAtRange(range, 'quote') + .apply() +} diff --git a/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/input.yaml b/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/input.yaml new file mode 100644 index 000000000..1279141e4 --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/input.yaml @@ -0,0 +1,41 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: one + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: two + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: three + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: four + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: five + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: six diff --git a/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/output.yaml b/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/output.yaml new file mode 100644 index 000000000..edf758d4a --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/middle-child-blocks/output.yaml @@ -0,0 +1,44 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: one + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: two + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: three + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: four + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: five + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: six diff --git a/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/index.js b/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/index.js new file mode 100644 index 000000000..9d8ad610a --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/index.js @@ -0,0 +1,18 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTextNodes() + const first = texts.get(0) + const second = texts.get(1) + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 0, + focusKey: second.key, + focusOffset: 0 + }) + + return state + .transform() + .unwrapBlockAtRange(range, 'quote') + .apply() +} diff --git a/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/input.yaml b/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/input.yaml new file mode 100644 index 000000000..1279141e4 --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/input.yaml @@ -0,0 +1,41 @@ + +nodes: + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: one + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: two + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: three + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: four + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: five + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: six diff --git a/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/output.yaml b/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/output.yaml new file mode 100644 index 000000000..580c42ce3 --- /dev/null +++ b/test/transforms/fixtures/unwrap-block-at-range/starting-child-blocks/output.yaml @@ -0,0 +1,41 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: one + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: two + - kind: block + type: quote + nodes: + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: three + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: four + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: five + - kind: block + type: paragraph + nodes: + - kind: text + ranges: + - text: six