Mercurial
comparison .cms/lib/codemirror/src/input/ContentEditableInput.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
1 import { operation, runInOp } from "../display/operations.js" | |
2 import { prepareSelection } from "../display/selection.js" | |
3 import { regChange } from "../display/view_tracking.js" | |
4 import { applyTextInput, copyableRanges, disableBrowserMagic, handlePaste, hiddenTextarea, lastCopied, setLastCopied } from "./input.js" | |
5 import { cmp, maxPos, minPos, Pos } from "../line/pos.js" | |
6 import { getBetween, getLine, lineNo } from "../line/utils_line.js" | |
7 import { findViewForLine, findViewIndex, mapFromLineView, nodeAndOffsetInLineMap } from "../measurement/position_measurement.js" | |
8 import { replaceRange } from "../model/changes.js" | |
9 import { simpleSelection } from "../model/selection.js" | |
10 import { setSelection } from "../model/selection_updates.js" | |
11 import { getBidiPartAt, getOrder } from "../util/bidi.js" | |
12 import { android, chrome, gecko, ie_version } from "../util/browser.js" | |
13 import { activeElt, contains, range, removeChildrenAndAdd, selectInput, rootNode } from "../util/dom.js" | |
14 import { on, signalDOMEvent } from "../util/event.js" | |
15 import { Delayed, lst, sel_dontScroll } from "../util/misc.js" | |
16 | |
17 // CONTENTEDITABLE INPUT STYLE | |
18 | |
19 export default class ContentEditableInput { | |
20 constructor(cm) { | |
21 this.cm = cm | |
22 this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null | |
23 this.polling = new Delayed() | |
24 this.composing = null | |
25 this.gracePeriod = false | |
26 this.readDOMTimeout = null | |
27 } | |
28 | |
29 init(display) { | |
30 let input = this, cm = input.cm | |
31 let div = input.div = display.lineDiv | |
32 div.contentEditable = true | |
33 disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize) | |
34 | |
35 function belongsToInput(e) { | |
36 for (let t = e.target; t; t = t.parentNode) { | |
37 if (t == div) return true | |
38 if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) break | |
39 } | |
40 return false | |
41 } | |
42 | |
43 on(div, "paste", e => { | |
44 if (!belongsToInput(e) || signalDOMEvent(cm, e) || handlePaste(e, cm)) return | |
45 // IE doesn't fire input events, so we schedule a read for the pasted content in this way | |
46 if (ie_version <= 11) setTimeout(operation(cm, () => this.updateFromDOM()), 20) | |
47 }) | |
48 | |
49 on(div, "compositionstart", e => { | |
50 this.composing = {data: e.data, done: false} | |
51 }) | |
52 on(div, "compositionupdate", e => { | |
53 if (!this.composing) this.composing = {data: e.data, done: false} | |
54 }) | |
55 on(div, "compositionend", e => { | |
56 if (this.composing) { | |
57 if (e.data != this.composing.data) this.readFromDOMSoon() | |
58 this.composing.done = true | |
59 } | |
60 }) | |
61 | |
62 on(div, "touchstart", () => input.forceCompositionEnd()) | |
63 | |
64 on(div, "input", () => { | |
65 if (!this.composing) this.readFromDOMSoon() | |
66 }) | |
67 | |
68 function onCopyCut(e) { | |
69 if (!belongsToInput(e) || signalDOMEvent(cm, e)) return | |
70 if (cm.somethingSelected()) { | |
71 setLastCopied({lineWise: false, text: cm.getSelections()}) | |
72 if (e.type == "cut") cm.replaceSelection("", null, "cut") | |
73 } else if (!cm.options.lineWiseCopyCut) { | |
74 return | |
75 } else { | |
76 let ranges = copyableRanges(cm) | |
77 setLastCopied({lineWise: true, text: ranges.text}) | |
78 if (e.type == "cut") { | |
79 cm.operation(() => { | |
80 cm.setSelections(ranges.ranges, 0, sel_dontScroll) | |
81 cm.replaceSelection("", null, "cut") | |
82 }) | |
83 } | |
84 } | |
85 if (e.clipboardData) { | |
86 e.clipboardData.clearData() | |
87 let content = lastCopied.text.join("\n") | |
88 // iOS exposes the clipboard API, but seems to discard content inserted into it | |
89 e.clipboardData.setData("Text", content) | |
90 if (e.clipboardData.getData("Text") == content) { | |
91 e.preventDefault() | |
92 return | |
93 } | |
94 } | |
95 // Old-fashioned briefly-focus-a-textarea hack | |
96 let kludge = hiddenTextarea(), te = kludge.firstChild | |
97 disableBrowserMagic(te) | |
98 cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild) | |
99 te.value = lastCopied.text.join("\n") | |
100 let hadFocus = activeElt(rootNode(div)) | |
101 selectInput(te) | |
102 setTimeout(() => { | |
103 cm.display.lineSpace.removeChild(kludge) | |
104 hadFocus.focus() | |
105 if (hadFocus == div) input.showPrimarySelection() | |
106 }, 50) | |
107 } | |
108 on(div, "copy", onCopyCut) | |
109 on(div, "cut", onCopyCut) | |
110 } | |
111 | |
112 screenReaderLabelChanged(label) { | |
113 // Label for screenreaders, accessibility | |
114 if(label) { | |
115 this.div.setAttribute('aria-label', label) | |
116 } else { | |
117 this.div.removeAttribute('aria-label') | |
118 } | |
119 } | |
120 | |
121 prepareSelection() { | |
122 let result = prepareSelection(this.cm, false) | |
123 result.focus = activeElt(rootNode(this.div)) == this.div | |
124 return result | |
125 } | |
126 | |
127 showSelection(info, takeFocus) { | |
128 if (!info || !this.cm.display.view.length) return | |
129 if (info.focus || takeFocus) this.showPrimarySelection() | |
130 this.showMultipleSelections(info) | |
131 } | |
132 | |
133 getSelection() { | |
134 return this.cm.display.wrapper.ownerDocument.getSelection() | |
135 } | |
136 | |
137 showPrimarySelection() { | |
138 let sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary() | |
139 let from = prim.from(), to = prim.to() | |
140 | |
141 if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { | |
142 sel.removeAllRanges() | |
143 return | |
144 } | |
145 | |
146 let curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) | |
147 let curFocus = domToPos(cm, sel.focusNode, sel.focusOffset) | |
148 if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && | |
149 cmp(minPos(curAnchor, curFocus), from) == 0 && | |
150 cmp(maxPos(curAnchor, curFocus), to) == 0) | |
151 return | |
152 | |
153 let view = cm.display.view | |
154 let start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || | |
155 {node: view[0].measure.map[2], offset: 0} | |
156 let end = to.line < cm.display.viewTo && posToDOM(cm, to) | |
157 if (!end) { | |
158 let measure = view[view.length - 1].measure | |
159 let map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map | |
160 end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]} | |
161 } | |
162 | |
163 if (!start || !end) { | |
164 sel.removeAllRanges() | |
165 return | |
166 } | |
167 | |
168 let old = sel.rangeCount && sel.getRangeAt(0), rng | |
169 try { rng = range(start.node, start.offset, end.offset, end.node) } | |
170 catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible | |
171 if (rng) { | |
172 if (!gecko && cm.state.focused) { | |
173 sel.collapse(start.node, start.offset) | |
174 if (!rng.collapsed) { | |
175 sel.removeAllRanges() | |
176 sel.addRange(rng) | |
177 } | |
178 } else { | |
179 sel.removeAllRanges() | |
180 sel.addRange(rng) | |
181 } | |
182 if (old && sel.anchorNode == null) sel.addRange(old) | |
183 else if (gecko) this.startGracePeriod() | |
184 } | |
185 this.rememberSelection() | |
186 } | |
187 | |
188 startGracePeriod() { | |
189 clearTimeout(this.gracePeriod) | |
190 this.gracePeriod = setTimeout(() => { | |
191 this.gracePeriod = false | |
192 if (this.selectionChanged()) | |
193 this.cm.operation(() => this.cm.curOp.selectionChanged = true) | |
194 }, 20) | |
195 } | |
196 | |
197 showMultipleSelections(info) { | |
198 removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors) | |
199 removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection) | |
200 } | |
201 | |
202 rememberSelection() { | |
203 let sel = this.getSelection() | |
204 this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset | |
205 this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset | |
206 } | |
207 | |
208 selectionInEditor() { | |
209 let sel = this.getSelection() | |
210 if (!sel.rangeCount) return false | |
211 let node = sel.getRangeAt(0).commonAncestorContainer | |
212 return contains(this.div, node) | |
213 } | |
214 | |
215 focus() { | |
216 if (this.cm.options.readOnly != "nocursor") { | |
217 if (!this.selectionInEditor() || activeElt(rootNode(this.div)) != this.div) | |
218 this.showSelection(this.prepareSelection(), true) | |
219 this.div.focus() | |
220 } | |
221 } | |
222 blur() { this.div.blur() } | |
223 getField() { return this.div } | |
224 | |
225 supportsTouch() { return true } | |
226 | |
227 receivedFocus() { | |
228 let input = this | |
229 if (this.selectionInEditor()) | |
230 setTimeout(() => this.pollSelection(), 20) | |
231 else | |
232 runInOp(this.cm, () => input.cm.curOp.selectionChanged = true) | |
233 | |
234 function poll() { | |
235 if (input.cm.state.focused) { | |
236 input.pollSelection() | |
237 input.polling.set(input.cm.options.pollInterval, poll) | |
238 } | |
239 } | |
240 this.polling.set(this.cm.options.pollInterval, poll) | |
241 } | |
242 | |
243 selectionChanged() { | |
244 let sel = this.getSelection() | |
245 return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || | |
246 sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset | |
247 } | |
248 | |
249 pollSelection() { | |
250 if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) return | |
251 let sel = this.getSelection(), cm = this.cm | |
252 // On Android Chrome (version 56, at least), backspacing into an | |
253 // uneditable block element will put the cursor in that element, | |
254 // and then, because it's not editable, hide the virtual keyboard. | |
255 // Because Android doesn't allow us to actually detect backspace | |
256 // presses in a sane way, this code checks for when that happens | |
257 // and simulates a backspace press in this case. | |
258 if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) { | |
259 this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}) | |
260 this.blur() | |
261 this.focus() | |
262 return | |
263 } | |
264 if (this.composing) return | |
265 this.rememberSelection() | |
266 let anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) | |
267 let head = domToPos(cm, sel.focusNode, sel.focusOffset) | |
268 if (anchor && head) runInOp(cm, () => { | |
269 setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll) | |
270 if (anchor.bad || head.bad) cm.curOp.selectionChanged = true | |
271 }) | |
272 } | |
273 | |
274 pollContent() { | |
275 if (this.readDOMTimeout != null) { | |
276 clearTimeout(this.readDOMTimeout) | |
277 this.readDOMTimeout = null | |
278 } | |
279 | |
280 let cm = this.cm, display = cm.display, sel = cm.doc.sel.primary() | |
281 let from = sel.from(), to = sel.to() | |
282 if (from.ch == 0 && from.line > cm.firstLine()) | |
283 from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length) | |
284 if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) | |
285 to = Pos(to.line + 1, 0) | |
286 if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false | |
287 | |
288 let fromIndex, fromLine, fromNode | |
289 if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { | |
290 fromLine = lineNo(display.view[0].line) | |
291 fromNode = display.view[0].node | |
292 } else { | |
293 fromLine = lineNo(display.view[fromIndex].line) | |
294 fromNode = display.view[fromIndex - 1].node.nextSibling | |
295 } | |
296 let toIndex = findViewIndex(cm, to.line) | |
297 let toLine, toNode | |
298 if (toIndex == display.view.length - 1) { | |
299 toLine = display.viewTo - 1 | |
300 toNode = display.lineDiv.lastChild | |
301 } else { | |
302 toLine = lineNo(display.view[toIndex + 1].line) - 1 | |
303 toNode = display.view[toIndex + 1].node.previousSibling | |
304 } | |
305 | |
306 if (!fromNode) return false | |
307 let newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)) | |
308 let oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)) | |
309 while (newText.length > 1 && oldText.length > 1) { | |
310 if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine-- } | |
311 else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++ } | |
312 else break | |
313 } | |
314 | |
315 let cutFront = 0, cutEnd = 0 | |
316 let newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length) | |
317 while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) | |
318 ++cutFront | |
319 let newBot = lst(newText), oldBot = lst(oldText) | |
320 let maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), | |
321 oldBot.length - (oldText.length == 1 ? cutFront : 0)) | |
322 while (cutEnd < maxCutEnd && | |
323 newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) | |
324 ++cutEnd | |
325 // Try to move start of change to start of selection if ambiguous | |
326 if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { | |
327 while (cutFront && cutFront > from.ch && | |
328 newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { | |
329 cutFront-- | |
330 cutEnd++ | |
331 } | |
332 } | |
333 | |
334 newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, "") | |
335 newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, "") | |
336 | |
337 let chFrom = Pos(fromLine, cutFront) | |
338 let chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0) | |
339 if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { | |
340 replaceRange(cm.doc, newText, chFrom, chTo, "+input") | |
341 return true | |
342 } | |
343 } | |
344 | |
345 ensurePolled() { | |
346 this.forceCompositionEnd() | |
347 } | |
348 reset() { | |
349 this.forceCompositionEnd() | |
350 } | |
351 forceCompositionEnd() { | |
352 if (!this.composing) return | |
353 clearTimeout(this.readDOMTimeout) | |
354 this.composing = null | |
355 this.updateFromDOM() | |
356 this.div.blur() | |
357 this.div.focus() | |
358 } | |
359 readFromDOMSoon() { | |
360 if (this.readDOMTimeout != null) return | |
361 this.readDOMTimeout = setTimeout(() => { | |
362 this.readDOMTimeout = null | |
363 if (this.composing) { | |
364 if (this.composing.done) this.composing = null | |
365 else return | |
366 } | |
367 this.updateFromDOM() | |
368 }, 80) | |
369 } | |
370 | |
371 updateFromDOM() { | |
372 if (this.cm.isReadOnly() || !this.pollContent()) | |
373 runInOp(this.cm, () => regChange(this.cm)) | |
374 } | |
375 | |
376 setUneditable(node) { | |
377 node.contentEditable = "false" | |
378 } | |
379 | |
380 onKeyPress(e) { | |
381 if (e.charCode == 0 || this.composing) return | |
382 e.preventDefault() | |
383 if (!this.cm.isReadOnly()) | |
384 operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) | |
385 } | |
386 | |
387 readOnlyChanged(val) { | |
388 this.div.contentEditable = String(val != "nocursor") | |
389 } | |
390 | |
391 onContextMenu() {} | |
392 resetPosition() {} | |
393 } | |
394 | |
395 ContentEditableInput.prototype.needsContentAttribute = true | |
396 | |
397 function posToDOM(cm, pos) { | |
398 let view = findViewForLine(cm, pos.line) | |
399 if (!view || view.hidden) return null | |
400 let line = getLine(cm.doc, pos.line) | |
401 let info = mapFromLineView(view, line, pos.line) | |
402 | |
403 let order = getOrder(line, cm.doc.direction), side = "left" | |
404 if (order) { | |
405 let partPos = getBidiPartAt(order, pos.ch) | |
406 side = partPos % 2 ? "right" : "left" | |
407 } | |
408 let result = nodeAndOffsetInLineMap(info.map, pos.ch, side) | |
409 result.offset = result.collapse == "right" ? result.end : result.start | |
410 return result | |
411 } | |
412 | |
413 function isInGutter(node) { | |
414 for (let scan = node; scan; scan = scan.parentNode) | |
415 if (/CodeMirror-gutter-wrapper/.test(scan.className)) return true | |
416 return false | |
417 } | |
418 | |
419 function badPos(pos, bad) { if (bad) pos.bad = true; return pos } | |
420 | |
421 function domTextBetween(cm, from, to, fromLine, toLine) { | |
422 let text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false | |
423 function recognizeMarker(id) { return marker => marker.id == id } | |
424 function close() { | |
425 if (closing) { | |
426 text += lineSep | |
427 if (extraLinebreak) text += lineSep | |
428 closing = extraLinebreak = false | |
429 } | |
430 } | |
431 function addText(str) { | |
432 if (str) { | |
433 close() | |
434 text += str | |
435 } | |
436 } | |
437 function walk(node) { | |
438 if (node.nodeType == 1) { | |
439 let cmText = node.getAttribute("cm-text") | |
440 if (cmText) { | |
441 addText(cmText) | |
442 return | |
443 } | |
444 let markerID = node.getAttribute("cm-marker"), range | |
445 if (markerID) { | |
446 let found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)) | |
447 if (found.length && (range = found[0].find(0))) | |
448 addText(getBetween(cm.doc, range.from, range.to).join(lineSep)) | |
449 return | |
450 } | |
451 if (node.getAttribute("contenteditable") == "false") return | |
452 let isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName) | |
453 if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) return | |
454 | |
455 if (isBlock) close() | |
456 for (let i = 0; i < node.childNodes.length; i++) | |
457 walk(node.childNodes[i]) | |
458 | |
459 if (/^(pre|p)$/i.test(node.nodeName)) extraLinebreak = true | |
460 if (isBlock) closing = true | |
461 } else if (node.nodeType == 3) { | |
462 addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")) | |
463 } | |
464 } | |
465 for (;;) { | |
466 walk(from) | |
467 if (from == to) break | |
468 from = from.nextSibling | |
469 extraLinebreak = false | |
470 } | |
471 return text | |
472 } | |
473 | |
474 function domToPos(cm, node, offset) { | |
475 let lineNode | |
476 if (node == cm.display.lineDiv) { | |
477 lineNode = cm.display.lineDiv.childNodes[offset] | |
478 if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) | |
479 node = null; offset = 0 | |
480 } else { | |
481 for (lineNode = node;; lineNode = lineNode.parentNode) { | |
482 if (!lineNode || lineNode == cm.display.lineDiv) return null | |
483 if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break | |
484 } | |
485 } | |
486 for (let i = 0; i < cm.display.view.length; i++) { | |
487 let lineView = cm.display.view[i] | |
488 if (lineView.node == lineNode) | |
489 return locateNodeInLineView(lineView, node, offset) | |
490 } | |
491 } | |
492 | |
493 function locateNodeInLineView(lineView, node, offset) { | |
494 let wrapper = lineView.text.firstChild, bad = false | |
495 if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true) | |
496 if (node == wrapper) { | |
497 bad = true | |
498 node = wrapper.childNodes[offset] | |
499 offset = 0 | |
500 if (!node) { | |
501 let line = lineView.rest ? lst(lineView.rest) : lineView.line | |
502 return badPos(Pos(lineNo(line), line.text.length), bad) | |
503 } | |
504 } | |
505 | |
506 let textNode = node.nodeType == 3 ? node : null, topNode = node | |
507 if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { | |
508 textNode = node.firstChild | |
509 if (offset) offset = textNode.nodeValue.length | |
510 } | |
511 while (topNode.parentNode != wrapper) topNode = topNode.parentNode | |
512 let measure = lineView.measure, maps = measure.maps | |
513 | |
514 function find(textNode, topNode, offset) { | |
515 for (let i = -1; i < (maps ? maps.length : 0); i++) { | |
516 let map = i < 0 ? measure.map : maps[i] | |
517 for (let j = 0; j < map.length; j += 3) { | |
518 let curNode = map[j + 2] | |
519 if (curNode == textNode || curNode == topNode) { | |
520 let line = lineNo(i < 0 ? lineView.line : lineView.rest[i]) | |
521 let ch = map[j] + offset | |
522 if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)] | |
523 return Pos(line, ch) | |
524 } | |
525 } | |
526 } | |
527 } | |
528 let found = find(textNode, topNode, offset) | |
529 if (found) return badPos(found, bad) | |
530 | |
531 // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems | |
532 for (let after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { | |
533 found = find(after, after.firstChild, 0) | |
534 if (found) | |
535 return badPos(Pos(found.line, found.ch - dist), bad) | |
536 else | |
537 dist += after.textContent.length | |
538 } | |
539 for (let before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { | |
540 found = find(before, before.firstChild, -1) | |
541 if (found) | |
542 return badPos(Pos(found.line, found.ch + dist), bad) | |
543 else | |
544 dist += before.textContent.length | |
545 } | |
546 } |