├── .babelrc
├── .editorconfig
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── demo
├── demo.css
├── demo.js
├── img.png
├── index.html
├── test.html
└── test.js
├── package.json
└── src
├── index.js
└── util.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | indent_style = space
8 | indent_size = 2
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | *.log
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # prosemirror-find-replace
2 | Find & Replace plugin for ProseMirror
3 |
4 |
5 | ## Usage
6 |
7 | Open your project in command prompt and run:
8 |
9 | `npm install prosemirror --save`
10 |
11 | `npm install prosemirror-find-replace --save`
12 |
13 |
14 | In your ProseMirror initialization script:
15 |
16 | ```
17 | import { ProseMirror } from "prosemirror"
18 | import "prosemirror-find-replace"
19 |
20 | let pm = new ProseMirror({
21 | place: document.querySelector("#target"),
22 | find: {}
23 | })
24 | ```
25 |
26 |
27 | ### Options
28 |
29 | The following options can be passed in the find object when ProseMirror is initialized:
30 |
31 | **atuoSelectNext** *(Boolean, default: true)*: Moves user selection to the next find match after and find or replace
32 |
33 | **findClass** *(String, default:"find")*: Class to apply to find matches
34 |
35 |
36 | ### Commands
37 |
38 | A simple, default set of commands is included, if you choose to use them:
39 |
40 | ```
41 | import { ProseMirror, CommandSet } from "prosemirror/dist/edit"
42 | import { findCommands } from "prosemirror-find-replace"
43 |
44 | let pm = new ProseMirror({
45 | place: document.querySelector("#target"),
46 | find: {},
47 | commands: CommandSet.default.add(findCommands)
48 | })
49 | ```
50 |
51 | **find** *(Meta + F)* - Highlights all matches of find term, selects next one if `autoSelectNext` option is true
52 |
53 | **findNext** *(Alt + Meta + F)* - Moves selection to next match of previous find
54 |
55 | **replace** *(Shift + Meta + F)* - Finds next match and replaces it
56 |
57 | **replaceAll** *(Shift + Alt + Meta + F)* - Finds and replaces all matches
58 |
59 | **clearFind** *(No shortcut)* - Clears highlighted find results
60 |
61 |
62 |
63 | ## Demo
64 |
65 | To run a quick demo (based on ProseMirror demo) run the following from the `prosemirror-find-replace` directory in command prompt:
66 |
67 | `npm install`
68 |
69 | `npm install prosemirror` (npm > 3 will not install peerDependencies for you)
70 |
71 | `npm run demo`
72 |
73 | Then connect to `http://localhost:8080` in your browser
74 |
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Georgia;
3 | margin: 0 1em
4 | }
5 |
6 | textarea {
7 | width: 100%;
8 | border: 1px solid silver;
9 | min-height: 40em;
10 | padding: 4px 8px;
11 | }
12 |
13 | .left, .right {
14 | width: 50%;
15 | float: left;
16 | }
17 |
18 | .full {
19 | max-width: 50em;
20 | }
21 |
22 | .marked {
23 | background: #ff6
24 | }
25 |
26 | .find {
27 | background: #ffd700
28 | }
29 |
30 | .activeFind{
31 | background: #00ff00
32 | }
33 |
--------------------------------------------------------------------------------
/demo/demo.js:
--------------------------------------------------------------------------------
1 | import {ProseMirror} from "prosemirror/dist/edit/main"
2 | import {fromDOM} from "prosemirror/dist/format"
3 | import {defaultSchema as schema} from "prosemirror/dist/model"
4 | import {updateCommands, CommandSet} from "prosemirror/dist/edit/command"
5 |
6 | import "prosemirror/dist/inputrules/autoinput"
7 | import "prosemirror/dist/menu/tooltipmenu"
8 | import "prosemirror/dist/menu/menubar"
9 | import "prosemirror/dist/collab"
10 | import {findCommands} from "../src"
11 |
12 | let te = document.querySelector("#content")
13 | te.style.display = "none"
14 |
15 | let dummy = document.createElement("div")
16 | dummy.innerHTML = te.value
17 | let doc = fromDOM(schema, dummy)
18 |
19 | class DummyServer {
20 | constructor() {
21 | this.version = 0
22 | this.pms = []
23 | }
24 |
25 | attach(pm) {
26 | pm.mod.collab.on("mustSend", () => this.mustSend(pm))
27 | this.pms.push(pm)
28 | }
29 |
30 | mustSend(pm) {
31 | let toSend = pm.mod.collab.sendableSteps()
32 | this.send(pm, toSend.version, toSend.steps)
33 | pm.mod.collab.confirmSteps(toSend)
34 | }
35 |
36 | send(pm, version, steps) {
37 | this.version += steps.length
38 | for (let i = 0; i < this.pms.length; i++)
39 | if (this.pms[i] != pm) this.pms[i].mod.collab.receive(steps)
40 | }
41 | }
42 |
43 | function makeEditor(where, collab) {
44 | let pm = new ProseMirror({
45 | place: document.querySelector(where),
46 | autoInput: true,
47 | tooltipMenu: {selectedBlockMenu: true},
48 | menuBar: {float: true},
49 | doc: doc,
50 | collab: collab,
51 | commands: CommandSet.default.add(findCommands),
52 | find: {
53 | highlightAll: true
54 | }
55 | })
56 | return pm
57 | }
58 |
59 | window.pm = window.pm2 = null
60 | function createCollab() {
61 | let server = new DummyServer
62 | pm = makeEditor(".left", {version: server.version})
63 | server.attach(pm)
64 | pm2 = makeEditor(".right", {version: server.version})
65 | server.attach(pm2)
66 | }
67 |
68 | let collab = document.location.hash == "#collab"
69 | let button = document.querySelector("#switch")
70 | function choose(collab) {
71 | if (pm) { pm.wrapper.parentNode.removeChild(pm.wrapper); pm = null }
72 | if (pm2) { pm2.wrapper.parentNode.removeChild(pm2.wrapper); pm2 = null }
73 |
74 | if (collab) {
75 | createCollab()
76 | button.textContent = "try single editor"
77 | document.location.hash = "#collab"
78 | } else {
79 | pm = makeEditor(".full", false)
80 | button.textContent = "try collaborative editor"
81 | document.location.hash = "#single"
82 | }
83 | }
84 | button.addEventListener("click", () => choose(collab = !collab))
85 |
86 | choose(collab)
87 |
88 | addEventListener("hashchange", () => {
89 | let newVal = document.location.hash != "#single"
90 | if (newVal != collab) choose(collab = newVal)
91 | })
92 |
93 | document.querySelector("#mark").addEventListener("mousedown", e => {
94 | pm.markRange(pm.selection.from, pm.selection.to, {className: "marked"})
95 | e.preventDefault()
96 | })
97 |
--------------------------------------------------------------------------------
/demo/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattberkowitz/prosemirror-find-replace/84926efdf060ca09cd582e292f01ff31fa4ab3d6/demo/img.png
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
ProseMirror demo page
5 |
6 |
7 | ProseMirror demo page
8 |
9 |
10 |
11 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/demo/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ProseMirror tests
5 |
6 |
16 |
17 | ProseMirror Tests
18 |
19 |
20 |
21 |
Starting...
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/demo/test.js:
--------------------------------------------------------------------------------
1 | import {tests, filter} from "../test/tests"
2 | import {Failure} from "../test/failure"
3 | import "../test/all"
4 | import "../test/browser/all"
5 |
6 | let gen = 0
7 |
8 | function runTests() {
9 | let filters = document.location.hash.slice(1).split(",")
10 | let myGen = ++gen
11 | let runnable = []
12 | for (let name in tests) if (filter(name, filters)) runnable.push(name)
13 |
14 | document.querySelector("#output").textContent = ""
15 |
16 | function run(i) {
17 | let t0 = Date.now()
18 | for (;; i++) {
19 | if (gen != myGen) return
20 | if (i == runnable.length) return finish()
21 | let name = runnable[i]
22 | document.querySelector("#info").textContent = (i + 1) + " of " + runnable.length + " tests"
23 | document.querySelector("#status").textContent = "Running " + name
24 | document.querySelector("#measure").style.width = (((i + 1) / runnable.length) * 100) + "%"
25 |
26 | try {
27 | tests[name]()
28 | } catch(e) {
29 | logFailure(name, e)
30 | }
31 | if (Date.now() > t0 + 200) {
32 | setTimeout(() => run(i + 1), 50)
33 | return
34 | }
35 | }
36 | }
37 |
38 | let failed = 0
39 |
40 | function finish() {
41 | document.querySelector("#info").textContent = "Ran " + runnable.length + " tests"
42 | let status = document.querySelector("#status")
43 | status.textContent = failed ? failed + " failed" : "All passed"
44 | status.className = failed ? "bad" : "good"
45 | }
46 |
47 | function logFailure(name, err) {
48 | ++failed
49 | let elt = document.querySelector("#output").appendChild(document.createElement("pre"))
50 | let nm = elt.appendChild(document.createElement("a"))
51 | nm.className = "bad"
52 | nm.href= "#" + name
53 | nm.textContent = name
54 | elt.appendChild(document.createTextNode(": " + err))
55 | if (!(err instanceof Failure))
56 | console.log(name + ": " + (err.stack || err))
57 | }
58 |
59 | setTimeout(() => run(0), 50)
60 | }
61 |
62 | runTests()
63 |
64 | addEventListener("hashchange", runTests)
65 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prosemirror-find-replace",
3 | "version": "0.9.0",
4 | "description": "Find and Replace plugin for ProseMirror",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "test": "node test/start.js",
8 | "demo": "moduleserve --transform babel demo",
9 | "dist": "babel -d dist src",
10 | "dist-watch": "babel -w -d dist src",
11 | "prepublish": "rimraf dist/* && babel -d dist src"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/mattberkowitz/prosemirror-find-replace.git"
16 | },
17 | "author": "Matthew Berkowitz",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/mattberkowitz/prosemirror-find-replace/issues"
21 | },
22 | "homepage": "https://github.com/mattberkowitz/prosemirror-find-replace#readme",
23 | "peerDependencies": {
24 | "prosemirror": ">= 0.7.0"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.4.5",
28 | "babel-preset-es2015": "^6.3.13",
29 | "moduleserve": "^0.6.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {defineOption, ProseMirror} from "prosemirror/dist/edit"
2 | import {updateCommands, Command, CommandSet} from "prosemirror/dist/edit/command"
3 | import {TextSelection} from "prosemirror/dist/edit/selection"
4 | import {Textblock, Pos} from "prosemirror/dist/model"
5 | import {getNodeEndpoints} from "./util"
6 |
7 | window.ProseMirror = ProseMirror
8 |
9 | defineOption("find", false, (pm, value) => {
10 | if (pm.mod.find) {
11 | pm.mod.find.detach()
12 | pm.mod.find = null
13 | }
14 | if (value) {
15 | pm.mod.find = new Find(pm, value)
16 | }
17 | })
18 |
19 |
20 | //Recursively finds matches within a given node
21 | function findInNode(node, findResult) {
22 | let ret = []
23 |
24 | if(node.isTextblock) {
25 | let index = 0, foundAt, ep = getNodeEndpoints(pm.doc, node)
26 | while((foundAt = node.textContent.slice(index).search(findResult.findRegExp)) > -1) {
27 | let sel = new TextSelection(ep.from + index + foundAt + 1, ep.from + index + foundAt + findResult.findTerm.length + 1)
28 | ret.push(sel)
29 | index = index + foundAt + findResult.findTerm.length
30 | }
31 | } else {
32 | node.content.forEach((child, i) => ret = ret.concat(findInNode(child, findResult)))
33 | }
34 | return ret
35 | }
36 |
37 |
38 | //Finds the selection that comes after the end of the current selection
39 | function selectNext(pm, selections) {
40 | if(selections.length === 0) {
41 | return null
42 | }
43 |
44 | for(let i=0;i {
64 | var matchingRanges = pm.ranges.ranges.filter(function(range){return range.from === selection.from && range.to === selection.to})
65 | if(matchingRanges.length === 0){
66 | pm.markRange(selection.from, selection.to, {className: pm.mod.find.options.findClass})
67 | }
68 | })
69 | }
70 |
71 | //Removes MarkedRanges that reside within a given node
72 | function removeFinds(pm, node = pm.doc) {
73 | pm.ranges.ranges.filter(r => r.options.className === pm.mod.find.options.findClass && pm.doc.resolve(r.from).path.indexOf(node) > -1).forEach(r => pm.ranges.removeRange(r))
74 | }
75 |
76 | function markActiveFind(pm,selection){
77 | pm.markRange(selection.from, selection.to, {className: pm.mod.find.options.activeFindClass})
78 | }
79 | function removeActiveFind(pm, node = pm.doc){
80 | pm.ranges.ranges.filter(r => r.options.className === pm.mod.find.options.activeFindClass && pm.doc.resolve(r.from).path.indexOf(node) > -1).forEach(r => pm.ranges.removeRange(r))
81 | }
82 |
83 | //Calculates the start and end nodes of a transform
84 | function rangeFromTransform(tr) {
85 | let from, to
86 | for (let i = 0; i < tr.steps.length; i++) {
87 | let step = tr.steps[i], map = tr.maps[i]
88 | let stepFrom = map.map(step.from || step.pos, -1).pos
89 | let stepTo = map.map(step.to || step.pos, 1).pos
90 | from = from ? map.map(from, -1).pos.min(stepFrom) : stepFrom
91 | to = to ? map.map(to, 1).pos.max(stepTo) : stepTo
92 | }
93 | return {from, to}
94 | }
95 |
96 |
97 | //Removes and recalcualtes finds between a start and end point
98 | function processNodes (pm, from, to, findResult) {
99 | if(!findResult) return
100 | let processed = []
101 | function processNode (node, path) {
102 | if(node.isTextblock && processed.indexOf(node) === -1) {
103 | removeFinds(pm, node)
104 | removeActiveFind(pm, node)
105 | let matches = findInNode(node, findResult, [].concat(path))
106 | markFinds(pm, matches)
107 | processed.push(node)
108 | }
109 | }
110 | pm.doc.nodesBetween(from, to, (node, path, parent) => processNode(node, path))
111 | }
112 |
113 |
114 | //Calculates default value for find input
115 | //Selected text > Last search > Empty
116 | function defaultFindTerm(pm) {
117 | if(!pm.selection.empty) {
118 | return pm.doc.slice(pm.selection.from, pm.selection.to).content.textContent
119 | }
120 | if(pm.mod.find.findOptions) {
121 | return pm.mod.find.findOptions.findTerm
122 | }
123 | return null
124 | }
125 |
126 | //Calculates default value for replace input
127 | //Last search > Empty
128 | function defaultReplaceWith(pm) {
129 | if(pm.mod.find.findOptions) {
130 | return pm.mod.find.findOptions.replaceWith
131 | }
132 | return null
133 | }
134 |
135 |
136 | //A default set of commands
137 | export var findCommands = {
138 | find: {
139 | label: "Find occurances of a string",
140 | run: function(pm, findTerm) {
141 | pm.mod.find.find(findTerm)
142 | },
143 | params: [
144 | {label: "Find", type: "text", prefill: defaultFindTerm}
145 | ],
146 | keys: ["Mod-F"]
147 | },
148 | findNext: {
149 | label: "Find next occurance of last searched string",
150 | run: function(pm,findTerm) {
151 | pm.mod.find.findNext()
152 | },
153 | keys: ["Alt-Mod-F"]
154 | },
155 | clearFind: {
156 | label: "Clear highlighted finds",
157 | run: function(pm) {
158 | pm.mod.find.clearFind()
159 | }
160 | },
161 | replace: {
162 | label: "Replaces selected/next occurance of a string",
163 | run: function(pm, findTerm, replaceWith) {
164 | pm.mod.find.replace(findTerm, replaceWith)
165 | },
166 | params: [
167 | {label: "Find", type: "text", prefill: defaultFindTerm},
168 | {label: "Replace", type: "text", prefill: defaultReplaceWith}
169 | ],
170 | keys: ["Shift-Mod-F"]
171 | },
172 | replaceAll: {
173 | label: "Replaces all occurances of a string",
174 | run: function(pm, findTerm, replaceWith) {
175 | pm.mod.find.replaceAll(findTerm, replaceWith)
176 | },
177 | params: [
178 | {label: "Find", type: "text", prefill: defaultFindTerm},
179 | {label: "Replace", type: "text", prefill: defaultReplaceWith}
180 | ],
181 | keys: ["Shift-Alt-Mod-F"]
182 | }
183 | }
184 |
185 |
186 | //Class to handle a set of find/replace terms and results
187 | class FindOptions {
188 | constructor(pm, findTerm, replaceWith, caseSensitive = true) {
189 | this.pm = pm
190 | this.findTerm = findTerm
191 | this.replaceWith = replaceWith
192 | this.caseSensitive = caseSensitive
193 | }
194 |
195 | //Constructs a regex based on find term and case sensitivity
196 | get findRegExp() {
197 | return RegExp(this.findTerm, !this.caseSensitive ? "i" : "")
198 | }
199 |
200 | //Calculates results for a set of terms
201 | results() {
202 | return findInNode(this.pm.doc, this)
203 | }
204 | }
205 |
206 |
207 |
208 | class Find {
209 | constructor(pm, options) {
210 | this.pm = pm
211 | this.findOptions = null
212 |
213 | this.options = Object.create(this.defaultOptions)
214 | for(let option in options){
215 | this.options[option] = options[option]
216 | }
217 |
218 | pm.mod.find = this
219 |
220 | //Recalculate changed blocks on transform
221 | this.onTransform = function(transform) {
222 | //If there was a find
223 | if(pm.mod.find.findOptions) {
224 | let {from, to} = rangeFromTransform(transform)
225 | processNodes(pm, from, to, pm.mod.find.findOptions)
226 | }
227 | }
228 |
229 | pm.on("transform", this.onTransform)
230 | }
231 |
232 | detach() {
233 | this.clearFind()
234 | pm.off("transform", this.onTransform)
235 | }
236 |
237 | //Default set of options
238 | get defaultOptions() {
239 | return {
240 | autoSelectNext: true, //move selection to next find after 'find' or 'replace'
241 | findClass: "find", //class to add to MarkedRanges
242 | activeFindClass: "activeFind"
243 | }
244 | }
245 |
246 | //Gets last find options
247 | get findOptions() {
248 | return this._findOptions
249 | }
250 |
251 | //Clears last find display and sets new find options
252 | set findOptions(val) {
253 | if(this._findOptions) this.clearFind() //clear out existing results if there are any
254 | this._findOptions = val
255 | }
256 |
257 | //Find and mark instnaces of a find term, optionally case insensitive
258 | //Will move selection to the next match, if autoSelectNext option is true
259 | find(findTerm, caseSensitive = true) {
260 | this.findOptions = new FindOptions(this.pm, findTerm, null, caseSensitive)
261 |
262 | let selections = this.findOptions.results()
263 |
264 | markFinds(this.pm, selections)
265 |
266 | if(this.options.autoSelectNext) {
267 | selectNext(this.pm, selections)
268 | }
269 |
270 |
271 |
272 | return selections
273 | }
274 |
275 | //Moves the selection to the next instance of the find term, optionall case insensitive
276 | findNext(findTerm, caseSensitive = true) {
277 | if(findTerm) {
278 | this.findOptions = new FindOptions(this.pm, findTerm, null, caseSensitive)
279 | }
280 | if(this.findOptions) {
281 | let selections = this.findOptions.results()
282 | markFinds(this.pm, selections)
283 | return selectNext(this.pm, selections)
284 | }
285 | return null
286 | }
287 |
288 | //Clears find display and nulls out stored find options
289 | clearFind() {
290 | removeFinds(this.pm)
291 | removeActiveFind(this.pm)
292 | this._findOptions = null
293 | }
294 |
295 | //Replaces next match of a findTerm with the repalceWith string, optionally case insensitive
296 | //If current selection matches the find term it will be replaced
297 | //Otherwise, selection will be moved to the next match and that will be replaced
298 | //If options.autoFindNext is true the match that proceeds replaced on will be selected
299 | replace(findTerm, replaceWith, caseSensitive = true) {
300 | this.findOptions = new FindOptions(this.pm, findTerm, replaceWith, caseSensitive)
301 |
302 | if(!this.pm.doc.slice(this.pm.selection.from, this.pm.selection.to).content.textContent.match(this.findOptions.findRegExp)) {
303 | if(!selectNext(this.pm, this.findOptions.results())) {
304 | return null
305 | }
306 | }
307 |
308 | let transform = this.pm.tr.typeText(replaceWith).apply({scrollIntoView: true})
309 |
310 | if(this.options.autoSelectNext) {
311 |
312 | let otherResults = this.findOptions.results()
313 | if(otherResults.length) {
314 | removeFinds(this.pm)
315 | removeActiveFind(this.pm)
316 | markFinds(this.pm, otherResults)
317 | }
318 | selectNext(this.pm, otherResults)
319 |
320 | }
321 |
322 | return transform
323 | }
324 |
325 |
326 | //Replaces all occurances of a findTerm with the replaceWith string, optionally case insensitive
327 | replaceAll(findTerm, replaceWith, caseSensitive = true) {
328 | this.findOptions = new FindOptions(this.pm, findTerm, replaceWith, caseSensitive)
329 |
330 | let selections = this.findOptions.results(),
331 | selection, transform,
332 | transforms = [];
333 |
334 | while(selection = selections.shift()) {
335 | this.pm.setSelection(selection)
336 | transform = this.pm.tr.typeText(replaceWith).apply({scrollIntoView: true})
337 | transforms.push(transform)
338 | selections = selections.map(s => s.map(this.pm.doc, transform.maps[0]))
339 | }
340 | return transforms
341 | }
342 |
343 |
344 | }
345 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | export function getNodeEndpoints(context, node) {
2 | let offset = 0
3 |
4 | if(context === node) return { from: offset, to: offset + node.nodeSize }
5 |
6 | if(node.isBlock) {
7 | for(let i=0; i