openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

ContentEditableInput.min.js (18356B)


      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 }