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