├── .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