diff --git a/docs/reference/models/transform.md b/docs/reference/models/transform.md index 5b23d18bd..b13108e51 100644 --- a/docs/reference/models/transform.md +++ b/docs/reference/models/transform.md @@ -99,11 +99,24 @@ Delete forward `n` characters at the current cursor. If the selection is expande Delete everything in the current selection. +### `insertBlock` +`insertBlock(block: Block) => Transform` +`insertBlock(properties: Object) => Transform` +`insertBlock(type: String) => Transform` + +Insert a new block at the same level as the current block, splitting the current block to make room if it is non-empty. If the selection is expanded, it will be deleted first. + ### `insertFragment` `insertFragment(fragment: Document) => Transform` Insert a `fragment` at the current selection. If the selection is expanded, it will be deleted first. +### `insertInline` +`insertInline(inline: Inline) => Transform` +`insertInline(properties: Object) => Transform` + +Insert a new inline at the current cursor position, splitting the text to make room if it is non-empty. If the selection is expanded, it will be deleted first. + ### `insertText` `insertText(text: String) => Transform` @@ -267,11 +280,24 @@ Delete forward `n` characters at a `range`. If the `range` is expanded, this met Delete everything in a `range`. +### `insertBlockAtRange` +`insertBlockAtRange(range: Selection, block: Block) => Transform` +`insertBlockAtRange(range: Selection, properties: Object) => Transform` +`insertBlockAtRange(range: Selection, type: String) => Transform` + +Insert a new block at the same level as the leaf block at a `range`, splitting the current block to make room if it is non-empty. If the selection is expanded, it will be deleted first. + ### `insertFragmentAtRange` `insertFragmentAtRange(range: Selection, fragment: Document) => Transform` Insert a `fragment` at a `range`. If the selection is expanded, it will be deleted first. +### `insertInlineAtRange` +`insertInlineAtRange(range: Selection, inline: Inline) => Transform` +`insertInlineAtRange(range: Selection, properties: Object) => Transform` + +Insert a new inline at a `range`, splitting the text to make room if it is non-empty. If the selection is expanded, it will be deleted first. + ### `insertTextAtRange` `insertTextAtRange(range: Selection, text: String) => Transform` diff --git a/lib/models/state.js b/lib/models/state.js index 787f683f1..908c1f45a 100644 --- a/lib/models/state.js +++ b/lib/models/state.js @@ -827,6 +827,38 @@ class State extends new Record(DEFAULTS) { return state } + /** + * Insert a `inline` at the current selection. + * + * @param {String || Object || Block} inline + * @return {State} state + */ + + insertInline(inline) { + let state = this + let { document, selection, startText } = state + let after = selection + const hasVoid = document.hasVoidParent(startText) + + // Insert the inline + document = document.insertInlineAtRange(selection, inline) + + // Determine what the selection should be after inserting. + if (hasVoid) { + selection = selection + } + + else { + const keys = state.document.getTexts().map(text => text.key) + const text = document.getTexts().find(n => !keys.includes(n.key)) + selection = selection.collapseToEndOf(text) + } + + // Update the document and selection. + state = state.merge({ document, selection }) + return state + } + /** * Insert a `text` string at the current selection. * diff --git a/test/transforms/fixtures/insert-inline-at-range/inside-inline/index.js b/test/transforms/fixtures/insert-inline-at-range/inside-inline/index.js new file mode 100644 index 000000000..3cee371eb --- /dev/null +++ b/test/transforms/fixtures/insert-inline-at-range/inside-inline/index.js @@ -0,0 +1,20 @@ + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 2, + focusKey: first.key, + focusOffset: 2 + }) + + return state + .transform() + .insertInlineAtRange(range, { + type: 'image', + isVoid: true + }) + .apply() +} diff --git a/test/transforms/fixtures/insert-inline-at-range/inside-inline/input.yaml b/test/transforms/fixtures/insert-inline-at-range/inside-inline/input.yaml new file mode 100644 index 000000000..f752cee89 --- /dev/null +++ b/test/transforms/fixtures/insert-inline-at-range/inside-inline/input.yaml @@ -0,0 +1,10 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: link + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline-at-range/inside-inline/output.yaml b/test/transforms/fixtures/insert-inline-at-range/inside-inline/output.yaml new file mode 100644 index 000000000..43b2b1a37 --- /dev/null +++ b/test/transforms/fixtures/insert-inline-at-range/inside-inline/output.yaml @@ -0,0 +1,15 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: link + nodes: + - kind: text + text: wo + - kind: inline + type: image + isVoid: true + - kind: text + text: rd diff --git a/test/transforms/fixtures/insert-inline/block-end/index.js b/test/transforms/fixtures/insert-inline/block-end/index.js new file mode 100644 index 000000000..6b17f8dda --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-end/index.js @@ -0,0 +1,32 @@ + +import assert from 'assert' + +export default function (state) { + 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) + .insertInline({ + type: 'hashtag', + isVoid: true + }) + .apply() + + const updated = next.document.getTexts().last() + + assert.deepEqual( + next.selection.toJS(), + range.collapseToEndOf(updated).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/block-end/input.yaml b/test/transforms/fixtures/insert-inline/block-end/input.yaml new file mode 100644 index 000000000..27f668fe2 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-end/input.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline/block-end/output.yaml b/test/transforms/fixtures/insert-inline/block-end/output.yaml new file mode 100644 index 000000000..f333e4656 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-end/output.yaml @@ -0,0 +1,10 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word + - kind: inline + type: hashtag + isVoid: true diff --git a/test/transforms/fixtures/insert-inline/block-middle/index.js b/test/transforms/fixtures/insert-inline/block-middle/index.js new file mode 100644 index 000000000..6de5d1d73 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-middle/index.js @@ -0,0 +1,32 @@ + +import assert from 'assert' + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 2, + focusKey: first.key, + focusOffset: 2 + }) + + const next = state + .transform() + .moveTo(range) + .insertInline({ + type: 'hashtag', + isVoid: true + }) + .apply() + + const updated = next.document.getTexts().get(1) + + assert.deepEqual( + next.selection.toJS(), + range.collapseToEndOf(updated).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/block-middle/input.yaml b/test/transforms/fixtures/insert-inline/block-middle/input.yaml new file mode 100644 index 000000000..27f668fe2 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-middle/input.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline/block-middle/output.yaml b/test/transforms/fixtures/insert-inline/block-middle/output.yaml new file mode 100644 index 000000000..09ad0bf68 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-middle/output.yaml @@ -0,0 +1,12 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: wo + - kind: inline + type: hashtag + isVoid: true + - kind: text + text: rd diff --git a/test/transforms/fixtures/insert-inline/block-start/index.js b/test/transforms/fixtures/insert-inline/block-start/index.js new file mode 100644 index 000000000..092bcf45b --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-start/index.js @@ -0,0 +1,32 @@ + +import assert from 'assert' + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 0, + focusKey: first.key, + focusOffset: 0 + }) + + const next = state + .transform() + .moveTo(range) + .insertInline({ + type: 'hashtag', + isVoid: true + }) + .apply() + + const updated = next.document.getTexts().first() + + assert.deepEqual( + next.selection.toJS(), + range.collapseToEndOf(updated).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/block-start/input.yaml b/test/transforms/fixtures/insert-inline/block-start/input.yaml new file mode 100644 index 000000000..27f668fe2 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-start/input.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline/block-start/output.yaml b/test/transforms/fixtures/insert-inline/block-start/output.yaml new file mode 100644 index 000000000..b1b784956 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/block-start/output.yaml @@ -0,0 +1,10 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: hashtag + isVoid: true + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline/inside-inline/index.js b/test/transforms/fixtures/insert-inline/inside-inline/index.js new file mode 100644 index 000000000..7ef8145ba --- /dev/null +++ b/test/transforms/fixtures/insert-inline/inside-inline/index.js @@ -0,0 +1,32 @@ + +import assert from 'assert' + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 2, + focusKey: first.key, + focusOffset: 2 + }) + + const next = state + .transform() + .moveTo(range) + .insertInline({ + type: 'image', + isVoid: true + }) + .apply() + + const updated = next.document.getTexts().get(1) + + assert.deepEqual( + next.selection.toJS(), + range.collapseToEndOf(updated).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/inside-inline/input.yaml b/test/transforms/fixtures/insert-inline/inside-inline/input.yaml new file mode 100644 index 000000000..f752cee89 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/inside-inline/input.yaml @@ -0,0 +1,10 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: link + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline/inside-inline/output.yaml b/test/transforms/fixtures/insert-inline/inside-inline/output.yaml new file mode 100644 index 000000000..43b2b1a37 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/inside-inline/output.yaml @@ -0,0 +1,15 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: link + nodes: + - kind: text + text: wo + - kind: inline + type: image + isVoid: true + - kind: text + text: rd diff --git a/test/transforms/fixtures/insert-inline/is-empty/index.js b/test/transforms/fixtures/insert-inline/is-empty/index.js new file mode 100644 index 000000000..048799136 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/is-empty/index.js @@ -0,0 +1,32 @@ + +import assert from 'assert' + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 0, + focusKey: first.key, + focusOffset: 0 + }) + + const next = state + .transform() + .moveTo(range) + .insertInline({ + type: 'hashtag', + isVoid: true + }) + .apply() + + const updated = next.document.getTexts().last() + + assert.deepEqual( + next.selection.toJS(), + range.collapseToEndOf(updated).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/is-empty/input.yaml b/test/transforms/fixtures/insert-inline/is-empty/input.yaml new file mode 100644 index 000000000..f6de4d08a --- /dev/null +++ b/test/transforms/fixtures/insert-inline/is-empty/input.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: "" diff --git a/test/transforms/fixtures/insert-inline/is-empty/output.yaml b/test/transforms/fixtures/insert-inline/is-empty/output.yaml new file mode 100644 index 000000000..936e672db --- /dev/null +++ b/test/transforms/fixtures/insert-inline/is-empty/output.yaml @@ -0,0 +1,8 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: hashtag + isVoid: true diff --git a/test/transforms/fixtures/insert-inline/is-void/index.js b/test/transforms/fixtures/insert-inline/is-void/index.js new file mode 100644 index 000000000..b152b8b4c --- /dev/null +++ b/test/transforms/fixtures/insert-inline/is-void/index.js @@ -0,0 +1,30 @@ + +import assert from 'assert' + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 0, + focusKey: first.key, + focusOffset: 0 + }) + + const next = state + .transform() + .moveTo(range) + .insertInline({ + type: 'hashtag', + isVoid: true + }) + .apply() + + assert.deepEqual( + next.selection.toJS(), + range.toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/is-void/input.yaml b/test/transforms/fixtures/insert-inline/is-void/input.yaml new file mode 100644 index 000000000..8246cfbed --- /dev/null +++ b/test/transforms/fixtures/insert-inline/is-void/input.yaml @@ -0,0 +1,5 @@ + +nodes: + - kind: block + type: image + isVoid: true diff --git a/test/transforms/fixtures/insert-inline/is-void/output.yaml b/test/transforms/fixtures/insert-inline/is-void/output.yaml new file mode 100644 index 000000000..8246cfbed --- /dev/null +++ b/test/transforms/fixtures/insert-inline/is-void/output.yaml @@ -0,0 +1,5 @@ + +nodes: + - kind: block + type: image + isVoid: true diff --git a/test/transforms/fixtures/insert-inline/with-inline/index.js b/test/transforms/fixtures/insert-inline/with-inline/index.js new file mode 100644 index 000000000..18deb6df5 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/with-inline/index.js @@ -0,0 +1,33 @@ + +import { Inline } from '../../../../..' +import assert from 'assert' + +export default function (state) { + const { document, selection } = state + const texts = document.getTexts() + const first = texts.first() + const range = selection.merge({ + anchorKey: first.key, + anchorOffset: 0, + focusKey: first.key, + focusOffset: 0 + }) + + const next = state + .transform() + .moveTo(range) + .insertInline(Inline.create({ + type: 'image', + isVoid: true + })) + .apply() + + const updated = next.document.getTexts().first() + + assert.deepEqual( + next.selection.toJS(), + range.collapseToEndOf(updated).toJS() + ) + + return next +} diff --git a/test/transforms/fixtures/insert-inline/with-inline/input.yaml b/test/transforms/fixtures/insert-inline/with-inline/input.yaml new file mode 100644 index 000000000..27f668fe2 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/with-inline/input.yaml @@ -0,0 +1,7 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: text + text: word diff --git a/test/transforms/fixtures/insert-inline/with-inline/output.yaml b/test/transforms/fixtures/insert-inline/with-inline/output.yaml new file mode 100644 index 000000000..3e75f2f14 --- /dev/null +++ b/test/transforms/fixtures/insert-inline/with-inline/output.yaml @@ -0,0 +1,10 @@ + +nodes: + - kind: block + type: paragraph + nodes: + - kind: inline + type: image + isVoid: true + - kind: text + text: word