From fc617a4d320893b0bc9f2a0ae4625a4eb332e30b Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Thu, 23 May 2019 17:41:11 +0200 Subject: [PATCH 01/17] add text search --- examples/Components/Routes/Search/index.vue | 256 ++++++++++++++++++ examples/Components/Subnavigation/index.vue | 3 + examples/main.js | 7 + .../src/extensions/Search.js | 109 ++++++++ packages/tiptap-extensions/src/index.js | 1 + 5 files changed, 376 insertions(+) create mode 100644 examples/Components/Routes/Search/index.vue create mode 100644 packages/tiptap-extensions/src/extensions/Search.js diff --git a/examples/Components/Routes/Search/index.vue b/examples/Components/Routes/Search/index.vue new file mode 100644 index 00000000..379dac65 --- /dev/null +++ b/examples/Components/Routes/Search/index.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/examples/Components/Subnavigation/index.vue b/examples/Components/Subnavigation/index.vue index 953b8a6d..a871baf0 100644 --- a/examples/Components/Subnavigation/index.vue +++ b/examples/Components/Subnavigation/index.vue @@ -24,6 +24,9 @@ Tables + + Search + Suggestions diff --git a/examples/main.js b/examples/main.js index 933ff7c0..705007f7 100644 --- a/examples/main.js +++ b/examples/main.js @@ -68,6 +68,13 @@ const routes = [ githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/TodoList', }, }, + { + path: '/search', + component: () => import('Components/Routes/Search'), + meta: { + githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Search', + }, + }, { path: '/suggestions', component: () => import('Components/Routes/Suggestions'), diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js new file mode 100644 index 00000000..fad8cdfa --- /dev/null +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -0,0 +1,109 @@ +import { Extension, Plugin } from 'tiptap' +import { Decoration, DecorationSet } from 'prosemirror-view' + +export default class Search extends Extension { + + constructor(options = {}) { + super(options) + + this.results = [] + this.searchTerm = null + } + + get name() { + return 'search' + } + + get defaultOptions() { + return { + autoSelectNext: true, + findClass: 'find', + searching: false, + caseSensitive: false, + } + } + + toggleSearch() { + return () => { + this.options.searching = !this.options.searching + return true + } + } + + keys() { + return { + 'Mod-f': this.toggleSearch(), + } + } + + commands() { + return { + find: attrs => this.find(attrs), + toggleSearch: () => this.toggleSearch(), + } + } + + get findRegExp() { + return RegExp(this.searchTerm, !this.options.caseSensitive ? 'gi' : 'g') + } + + get decorations() { + return this.results.map(deco => ( + Decoration.inline(deco.from, deco.to, { class: this.options.findClass }) + )) + } + + _search(doc) { + this.results = [] + + if (!this.searchTerm) { + return + } + + const search = this.findRegExp + + doc.descendants((node, pos) => { + if (node.isText) { + let m + while (m = search.exec(node.text)) { + this.results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }) + } + } + }) + } + + find(searchTerm) { + return ({ doc, tr }, dispatch) => { + this.options.searching = true + this.searchTerm = searchTerm + + this._search(doc) + + dispatch(tr) + } + } + + createDeco(doc) { + return this.decorations ? DecorationSet.create(doc, this.decorations) : [] + } + + get plugins() { + return [ + new Plugin({ + state: { + init: (_, { doc }) => this.createDeco(doc), + apply: (tr, old) => ( + (tr.docChanged || this.options.searching) ? this.createDeco(tr.doc) : old + ), + }, + props: { + decorations(state) { return this.getState(state) }, + }, + }), + ] + } + +} diff --git a/packages/tiptap-extensions/src/index.js b/packages/tiptap-extensions/src/index.js index 4851bb51..b3817696 100644 --- a/packages/tiptap-extensions/src/index.js +++ b/packages/tiptap-extensions/src/index.js @@ -26,6 +26,7 @@ export { default as Underline } from './marks/Underline' export { default as Collaboration } from './extensions/Collaboration' export { default as History } from './extensions/History' export { default as Placeholder } from './extensions/Placeholder' +export { default as Search } from './extensions/Search' export { default as Suggestions } from './plugins/Suggestions' export { default as Highlight } from './plugins/Highlight' From 753fc7632418dd7066062488ef045cf7dc4902c6 Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Tue, 28 May 2019 13:51:37 +0200 Subject: [PATCH 02/17] fix search highlight on doc change --- packages/tiptap-extensions/src/extensions/Search.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index fad8cdfa..3279bbc5 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -95,9 +95,15 @@ export default class Search extends Extension { new Plugin({ state: { init: (_, { doc }) => this.createDeco(doc), - apply: (tr, old) => ( - (tr.docChanged || this.options.searching) ? this.createDeco(tr.doc) : old - ), + apply: (tr, old) => { + if (this.options.searching) { + return this.createDeco(tr.doc) + } + if (tr.docChanged) { + return old.map(tr.mapping, tr.doc) + } + return old + }, }, props: { decorations(state) { return this.getState(state) }, From 331ba1c36b738ece3eb3b03e31747afdb3c71a59 Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Tue, 28 May 2019 13:52:13 +0200 Subject: [PATCH 03/17] search across marks --- .../src/extensions/Search.js | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 3279bbc5..ba083119 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -55,6 +55,8 @@ export default class Search extends Extension { _search(doc) { this.results = [] + const mergedTextNodes = [] + let index = 0 if (!this.searchTerm) { return @@ -64,13 +66,29 @@ export default class Search extends Extension { doc.descendants((node, pos) => { if (node.isText) { - let m - while (m = search.exec(node.text)) { - this.results.push({ - from: pos + m.index, - to: pos + m.index + m[0].length, - }) + if (mergedTextNodes[index]) { + mergedTextNodes[index] = { + text: mergedTextNodes[index].text + node.text, + pos: mergedTextNodes[index].pos, + } + } else { + mergedTextNodes[index] = { + text: node.text, + pos, + } } + } else { + index += 1 + } + }) + + mergedTextNodes.forEach(({ text, pos }) => { + let m + while (m = search.exec(text)) { + this.results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }) } }) } From f19b5c8f343ed95d02a4be3870a3f1c7ff43223f Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Tue, 28 May 2019 13:52:40 +0200 Subject: [PATCH 04/17] add toggleSearch event --- packages/tiptap-extensions/src/extensions/Search.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index ba083119..2603968e 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -14,6 +14,10 @@ export default class Search extends Extension { return 'search' } + init() { + this.editor.events.push('toggleSearch') + } + get defaultOptions() { return { autoSelectNext: true, @@ -26,6 +30,7 @@ export default class Search extends Extension { toggleSearch() { return () => { this.options.searching = !this.options.searching + this.editor.emit('toggleSearch', this.options.searching) return true } } From 94b2846695b2bdd096d857f815e84682f833ec18 Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Tue, 28 May 2019 13:53:15 +0200 Subject: [PATCH 05/17] focus search input on toggleSearch event --- examples/Components/Routes/Search/index.vue | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/examples/Components/Routes/Search/index.vue b/examples/Components/Routes/Search/index.vue index 379dac65..85c113aa 100644 --- a/examples/Components/Routes/Search/index.vue +++ b/examples/Components/Routes/Search/index.vue @@ -107,7 +107,7 @@ - - + diff --git a/examples/assets/sass/menubar.scss b/examples/assets/sass/menubar.scss index 26c0781b..0f5537b1 100644 --- a/examples/assets/sass/menubar.scss +++ b/examples/assets/sass/menubar.scss @@ -33,4 +33,8 @@ background-color: rgba($color-black, 0.1); } } + + span#{&}__button { + font-size: 13.3333px; + } } From 87995cb93c46b358b589a16cccbba488c96dd05e Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 11:05:18 +0200 Subject: [PATCH 07/17] fix decoration not updating if searching and doc changes --- packages/tiptap-extensions/src/extensions/Search.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 2603968e..0ccbb9e3 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -99,17 +99,16 @@ export default class Search extends Extension { } find(searchTerm) { - return ({ doc, tr }, dispatch) => { + return ({ tr }, dispatch) => { this.options.searching = true this.searchTerm = searchTerm - this._search(doc) - dispatch(tr) } } createDeco(doc) { + this._search(doc) return this.decorations ? DecorationSet.create(doc, this.decorations) : [] } @@ -117,7 +116,7 @@ export default class Search extends Extension { return [ new Plugin({ state: { - init: (_, { doc }) => this.createDeco(doc), + init() { return DecorationSet.empty }, apply: (tr, old) => { if (this.options.searching) { return this.createDeco(tr.doc) From 0476d355995bacd96a1d2c72a39145148feb5b6c Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 11:21:40 +0200 Subject: [PATCH 08/17] fix infinite loop on some RegExp --- packages/tiptap-extensions/src/extensions/Search.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 0ccbb9e3..95e4fc9c 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -67,8 +67,6 @@ export default class Search extends Extension { return } - const search = this.findRegExp - doc.descendants((node, pos) => { if (node.isText) { if (mergedTextNodes[index]) { @@ -88,8 +86,12 @@ export default class Search extends Extension { }) mergedTextNodes.forEach(({ text, pos }) => { + const search = this.findRegExp let m while (m = search.exec(text)) { + if (m[0] === '') { + break + } this.results.push({ from: pos + m.index, to: pos + m.index + m[0].length, From 0e5aa7f116784b374fae18ba950010d531e6b49f Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 11:25:26 +0200 Subject: [PATCH 09/17] add option to disable regex searches --- examples/Components/Routes/Search/index.vue | 4 +++- packages/tiptap-extensions/src/extensions/Search.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/Components/Routes/Search/index.vue b/examples/Components/Routes/Search/index.vue index 1a3470d8..4974ad79 100644 --- a/examples/Components/Routes/Search/index.vue +++ b/examples/Components/Routes/Search/index.vue @@ -175,7 +175,9 @@ export default { new HardBreak(), new Heading({ levels: [1, 2, 3] }), new HorizontalRule(), - new Search(), + new Search({ + disableRegex: false, + }), new ListItem(), new OrderedList(), new TodoItem(), diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 95e4fc9c..624e8484 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -24,6 +24,7 @@ export default class Search extends Extension { findClass: 'find', searching: false, caseSensitive: false, + disableRegex: true, } } @@ -103,7 +104,7 @@ export default class Search extends Extension { find(searchTerm) { return ({ tr }, dispatch) => { this.options.searching = true - this.searchTerm = searchTerm + this.searchTerm = (this.options.disableRegex) ? searchTerm.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : searchTerm dispatch(tr) } From d3514c3d361090d83882d991b766471cc931038c Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 12:24:46 +0200 Subject: [PATCH 10/17] add option to always update search results --- packages/tiptap-extensions/src/extensions/Search.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 624e8484..e76c5e3b 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -25,6 +25,7 @@ export default class Search extends Extension { searching: false, caseSensitive: false, disableRegex: true, + alwaysSearch: false, } } @@ -121,7 +122,7 @@ export default class Search extends Extension { state: { init() { return DecorationSet.empty }, apply: (tr, old) => { - if (this.options.searching) { + if (this.options.searching || (tr.docChanged && this.options.alwaysSearch)) { return this.createDeco(tr.doc) } if (tr.docChanged) { From a42d0113e02b8bb4685fd9b00dcec82710d95638 Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 12:25:33 +0200 Subject: [PATCH 11/17] add clearSearch command --- examples/Components/Routes/Search/index.vue | 1 + packages/tiptap-extensions/src/extensions/Search.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/examples/Components/Routes/Search/index.vue b/examples/Components/Routes/Search/index.vue index 4974ad79..6c2bd4a7 100644 --- a/examples/Components/Routes/Search/index.vue +++ b/examples/Components/Routes/Search/index.vue @@ -123,6 +123,7 @@ v-model="searchTerm" > + diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index e76c5e3b..6b026767 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -46,6 +46,7 @@ export default class Search extends Extension { commands() { return { find: attrs => this.find(attrs), + clearSearch: () => this.clear(), toggleSearch: () => this.toggleSearch(), } } @@ -111,6 +112,14 @@ export default class Search extends Extension { } } + clear() { + return ({ tr }, dispatch) => { + this.searchTerm = null + + dispatch(tr) + } + } + createDeco(doc) { this._search(doc) return this.decorations ? DecorationSet.create(doc, this.decorations) : [] From 016ea8f86b0f3cea549701534dcf8533ca6fe62c Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 12:46:36 +0200 Subject: [PATCH 12/17] use internal variable for commands to determine if search should update Prior to this if search was closed (searching: false) the commands won't do anything. --- .../src/extensions/Search.js | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 6b026767..74985878 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -8,6 +8,7 @@ export default class Search extends Extension { this.results = [] this.searchTerm = null + this._updating = false } get name() { @@ -104,22 +105,27 @@ export default class Search extends Extension { } find(searchTerm) { - return ({ tr }, dispatch) => { - this.options.searching = true + return (state, dispatch) => { this.searchTerm = (this.options.disableRegex) ? searchTerm.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : searchTerm - dispatch(tr) + this.updateView(state, dispatch) } } clear() { - return ({ tr }, dispatch) => { + return (state, dispatch) => { this.searchTerm = null - dispatch(tr) + this.updateView(state, dispatch) } } + updateView({ tr }, dispatch) { + this._updating = true + dispatch(tr) + this._updating = false + } + createDeco(doc) { this._search(doc) return this.decorations ? DecorationSet.create(doc, this.decorations) : [] @@ -131,7 +137,9 @@ export default class Search extends Extension { state: { init() { return DecorationSet.empty }, apply: (tr, old) => { - if (this.options.searching || (tr.docChanged && this.options.alwaysSearch)) { + if (this._updating + || this.options.searching + || (tr.docChanged && this.options.alwaysSearch)) { return this.createDeco(tr.doc) } if (tr.docChanged) { From 3246b84229ea8492b8f596f216410543d57dbede Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Wed, 29 May 2019 14:14:08 +0200 Subject: [PATCH 13/17] fix emojis in search --- packages/tiptap-extensions/src/extensions/Search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 74985878..ab85ffec 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -53,7 +53,7 @@ export default class Search extends Extension { } get findRegExp() { - return RegExp(this.searchTerm, !this.options.caseSensitive ? 'gi' : 'g') + return RegExp(this.searchTerm, !this.options.caseSensitive ? 'gui' : 'gu') } get decorations() { From 525619ad267e52551bc5a8383ce4b097484f851b Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Mon, 3 Jun 2019 12:16:07 +0200 Subject: [PATCH 14/17] default callback for custom editor events added --- packages/tiptap/src/Editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tiptap/src/Editor.js b/packages/tiptap/src/Editor.js index 0405cd6a..09341c3a 100644 --- a/packages/tiptap/src/Editor.js +++ b/packages/tiptap/src/Editor.js @@ -82,7 +82,7 @@ export default class Editor extends Emitter { } this.events.forEach(name => { - this.on(name, this.options[camelCase(`on ${name}`)]) + this.on(name, this.options[camelCase(`on ${name}`)] || (() => {})) }) this.emit('init', { From 2544e40f993a31880e83e8385638dada5b8a3ccf Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Mon, 3 Jun 2019 12:18:59 +0200 Subject: [PATCH 15/17] added replace and replaceAll commands --- examples/Components/Routes/Search/index.vue | 10 +++++- .../src/extensions/Search.js | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/examples/Components/Routes/Search/index.vue b/examples/Components/Routes/Search/index.vue index 6c2bd4a7..2cb1f3c2 100644 --- a/examples/Components/Routes/Search/index.vue +++ b/examples/Components/Routes/Search/index.vue @@ -121,9 +121,16 @@ placeholder="Search..." type="text" v-model="searchTerm" - > + > + + @@ -168,6 +175,7 @@ export default { return { searching: false, searchTerm: null, + replaceWith: null, editor: new Editor({ extensions: [ new Blockquote(), diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index ab85ffec..2fe57f35 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -47,6 +47,8 @@ export default class Search extends Extension { commands() { return { find: attrs => this.find(attrs), + replace: attrs => this.replace(attrs), + replaceAll: attrs => this.replaceAll(attrs), clearSearch: () => this.clear(), toggleSearch: () => this.toggleSearch(), } @@ -104,6 +106,40 @@ export default class Search extends Extension { }) } + replace(replace) { + return (state, dispatch) => { + const { from, to } = this.results[0] + + dispatch(state.tr.insertText(replace, from, to)) + } + } + + rebaseNextResult(replace, index) { + const nextIndex = index + 1 + if (!this.results[nextIndex]) return + + const nextStep = this.results[nextIndex] + const { from, to } = nextStep + const offset = (to - from - replace.length) * nextIndex + + this.results[nextIndex] = { + to: to - offset, + from: from - offset, + } + } + + replaceAll(replace) { + return ({ tr }, dispatch) => { + this.results.forEach(({ from, to }, index) => { + tr.insertText(replace, from, to) + + this.rebaseNextResult(replace, index) + }) + + dispatch(tr) + } + } + find(searchTerm) { return (state, dispatch) => { this.searchTerm = (this.options.disableRegex) ? searchTerm.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : searchTerm From edd54f90b0fb3742689b86ee25b19f5f322da2a3 Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Mon, 3 Jun 2019 12:52:59 +0200 Subject: [PATCH 16/17] disable eslint for while loop --- packages/tiptap-extensions/src/extensions/Search.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 2fe57f35..57d48bd4 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -94,7 +94,8 @@ export default class Search extends Extension { mergedTextNodes.forEach(({ text, pos }) => { const search = this.findRegExp let m - while (m = search.exec(text)) { + // eslint-disable-next-line no-cond-assign + while ((m = search.exec(text))) { if (m[0] === '') { break } From c33d1bf38fc9081866580fa60b7e494a8a705747 Mon Sep 17 00:00:00 2001 From: Chrissi2812 Date: Mon, 3 Jun 2019 17:24:15 +0200 Subject: [PATCH 17/17] fix wrong offset calculation --- .../tiptap-extensions/src/extensions/Search.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js index 57d48bd4..dd63d60a 100644 --- a/packages/tiptap-extensions/src/extensions/Search.js +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -115,26 +115,29 @@ export default class Search extends Extension { } } - rebaseNextResult(replace, index) { + rebaseNextResult(replace, index, lastOffset = 0) { const nextIndex = index + 1 - if (!this.results[nextIndex]) return + if (!this.results[nextIndex]) return null - const nextStep = this.results[nextIndex] - const { from, to } = nextStep - const offset = (to - from - replace.length) * nextIndex + const { from: currentFrom, to: currentTo } = this.results[index] + const offset = (currentTo - currentFrom - replace.length) + lastOffset + const { from, to } = this.results[nextIndex] this.results[nextIndex] = { to: to - offset, from: from - offset, } + + return offset } replaceAll(replace) { return ({ tr }, dispatch) => { + let offset this.results.forEach(({ from, to }, index) => { tr.insertText(replace, from, to) - this.rebaseNextResult(replace, index) + offset = this.rebaseNextResult(replace, index, offset) }) dispatch(tr)