File modules/editor/codemirror/src/input/ContentEditableInput.min.js

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