Newer
Older
import { operation, runInOp } from "../display/operations"
import { prepareSelection } from "../display/selection"
import { regChange } from "../display/view_tracking"
import { applyTextInput, copyableRanges, disableBrowserMagic, handlePaste, hiddenTextarea, lastCopied, setLastCopied } from "./input"
import { cmp, maxPos, minPos, Pos } from "../line/pos"
import { getBetween, getLine, lineNo } from "../line/utils_line"
import { findViewForLine, findViewIndex, mapFromLineView, nodeAndOffsetInLineMap } from "../measurement/position_measurement"
import { replaceRange } from "../model/changes"
import { simpleSelection } from "../model/selection"
import { setSelection } from "../model/selection_updates"
import { getBidiPartAt, getOrder } from "../util/bidi"
import { gecko, ie_version } from "../util/browser"
import { contains, range, removeChildrenAndAdd, selectInput } from "../util/dom"
import { on, signalDOMEvent } from "../util/event"
import { Delayed, lst, sel_dontScroll } from "../util/misc"
export default class ContentEditableInput {
constructor(cm) {
this.cm = cm
this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null
this.polling = new Delayed()
this.composing = null
this.gracePeriod = false
this.readDOMTimeout = null
}
let input = this, cm = input.cm
let div = input.div = display.lineDiv
if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
// IE doesn't fire input events, so we schedule a read for the pasted content in this way
if (ie_version <= 11) setTimeout(operation(cm, () => {
on(div, "compositionstart", e => {
this.composing = {data: e.data, done: false}
on(div, "compositionupdate", e => {
if (!this.composing) this.composing = {data: e.data, done: false}
if (this.composing) {
if (e.data != this.composing.data) this.readFromDOMSoon()
this.composing.done = true
on(div, "touchstart", () => input.forceCompositionEnd())
if (!this.composing) this.readFromDOMSoon()
function onCopyCut(e) {
if (signalDOMEvent(cm, e)) return
if (cm.somethingSelected()) {
setLastCopied({lineWise: false, text: cm.getSelections()})
if (e.type == "cut") cm.replaceSelection("", null, "cut")
} else if (!cm.options.lineWiseCopyCut) {
cm.setSelections(ranges.ranges, 0, sel_dontScroll)
cm.replaceSelection("", null, "cut")
})
// iOS exposes the clipboard API, but seems to discard content inserted into it
if (e.clipboardData.getData("Text") == content) {
return
}
}
// Old-fashioned briefly-focus-a-textarea hack
let kludge = hiddenTextarea(), te = kludge.firstChild
cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild)
te.value = lastCopied.text.join("\n")
cm.display.lineSpace.removeChild(kludge)
hadFocus.focus()
if (hadFocus == div) input.showPrimarySelection()
on(div, "copy", onCopyCut)
on(div, "cut", onCopyCut)
if (!info || !this.cm.display.view.length) return
if (info.focus || takeFocus) this.showPrimarySelection()
this.showMultipleSelections(info)
let sel = window.getSelection(), prim = this.cm.doc.sel.primary()
let curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset)
let curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset)
if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
let start = posToDOM(this.cm, prim.from())
let end = posToDOM(this.cm, prim.to())
let view = this.cm.display.view
let old = sel.rangeCount && sel.getRangeAt(0)
} else if (!end) { // FIXME dangerously hacky
let measure = view[view.length - 1].measure
let map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map
end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}
let rng
try { rng = range(start.node, start.offset, end.offset, end.node) }
catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
if (rng) {
if (!gecko && this.cm.state.focused) {
if (!rng.collapsed) {
sel.removeAllRanges()
sel.addRange(rng)
}
if (old && sel.anchorNode == null) sel.addRange(old)
else if (gecko) this.startGracePeriod()
this.gracePeriod = setTimeout(() => {
this.gracePeriod = false
if (this.selectionChanged())
this.cm.operation(() => this.cm.curOp.selectionChanged = true)
removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors)
removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection)
this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
let node = sel.getRangeAt(0).commonAncestorContainer
if (this.cm.options.readOnly != "nocursor") {
if (!this.selectionInEditor())
this.showSelection(this.prepareSelection(), true)
this.div.focus()
}
}
blur() { this.div.blur() }
getField() { return this.div }
runInOp(this.cm, () => input.cm.curOp.selectionChanged = true)
function poll() {
if (input.cm.state.focused) {
input.pollSelection()
input.polling.set(input.cm.options.pollInterval, poll)
return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
if (!this.composing && this.readDOMTimeout == null && !this.gracePeriod && this.selectionChanged()) {
let anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset)
let head = domToPos(cm, sel.focusNode, sel.focusOffset)
if (anchor && head) runInOp(cm, () => {
setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll)
if (anchor.bad || head.bad) cm.curOp.selectionChanged = true
})
if (this.readDOMTimeout != null) {
clearTimeout(this.readDOMTimeout)
this.readDOMTimeout = null
}
let cm = this.cm, display = cm.display, sel = cm.doc.sel.primary()
let from = sel.from(), to = sel.to()
Marijn Haverbeke
committed
if (from.ch == 0 && from.line > cm.firstLine())
from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length)
if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine())
to = Pos(to.line + 1, 0)
if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false
if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
fromLine = lineNo(display.view[0].line)
fromNode = display.view[0].node
fromLine = lineNo(display.view[fromIndex].line)
fromNode = display.view[fromIndex - 1].node.nextSibling
let toIndex = findViewIndex(cm, to.line)
let toLine, toNode
if (toIndex == display.view.length - 1) {
toLine = display.viewTo - 1
toNode = display.lineDiv.lastChild
toLine = lineNo(display.view[toIndex + 1].line) - 1
toNode = display.view[toIndex + 1].node.previousSibling
Marijn Haverbeke
committed
if (!fromNode) return false
let newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine))
let oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length))
while (newText.length > 1 && oldText.length > 1) {
if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine-- }
else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++ }
else break
let cutFront = 0, cutEnd = 0
let newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length)
while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
let newBot = lst(newText), oldBot = lst(oldText)
let maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
while (cutEnd < maxCutEnd &&
newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, "")
newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, "")
let chFrom = Pos(fromLine, cutFront)
let chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0)
if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
replaceRange(cm.doc, newText, chFrom, chTo, "+input")
return true
if (!this.composing) return
clearTimeout(this.readDOMTimeout)
this.composing = null
if (!this.pollContent()) regChange(this.cm)
if (this.readDOMTimeout != null) return
this.readDOMTimeout = setTimeout(() => {
this.readDOMTimeout = null
if (this.composing) {
if (this.composing.done) this.composing = null
else return
}
if (this.cm.isReadOnly() || !this.pollContent())
runInOp(this.cm, () => regChange(this.cm))
}, 80)
Marijn Haverbeke
committed
if (e.charCode == 0) return
operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0)
this.div.contentEditable = String(val != "nocursor")
onContextMenu() {}
resetPosition() {}
}
ContentEditableInput.prototype.needsContentAttribute = true
let line = getLine(cm.doc, pos.line)
let info = mapFromLineView(view, line, pos.line)
let order = getOrder(line, cm.doc.direction), side = "left"
let result = nodeAndOffsetInLineMap(info.map, pos.ch, side)
result.offset = result.collapse == "right" ? result.end : result.start
return result
function badPos(pos, bad) { if (bad) pos.bad = true; return pos }
function domTextBetween(cm, from, to, fromLine, toLine) {
let text = "", closing = false, lineSep = cm.doc.lineSeparator()
function recognizeMarker(id) { return marker => marker.id == id }
function walk(node) {
if (node.nodeType == 1) {
if (cmText == "") text += node.textContent.replace(/\u200b/g, "")
else text += cmText
let markerID = node.getAttribute("cm-marker"), range
let found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID))
if (found.length && (range = found[0].find()))
text += getBetween(cm.doc, range.from, range.to).join(lineSep)
return
if (node.getAttribute("contenteditable") == "false") return
if (/^(pre|div|p)$/i.test(node.nodeName))
walk(from)
if (from == to) break
from = from.nextSibling
}
function domToPos(cm, node, offset) {
lineNode = cm.display.lineDiv.childNodes[offset]
if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true)
node = null; offset = 0
} else {
for (lineNode = node;; lineNode = lineNode.parentNode) {
if (!lineNode || lineNode == cm.display.lineDiv) return null
if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break
for (let i = 0; i < cm.display.view.length; i++) {
let lineView = cm.display.view[i]
}
}
function locateNodeInLineView(lineView, node, offset) {
if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true)
bad = true
node = wrapper.childNodes[offset]
offset = 0
let line = lineView.rest ? lst(lineView.rest) : lineView.line
return badPos(Pos(lineNo(line), line.text.length), bad)
let textNode = node.nodeType == 3 ? node : null, topNode = node
if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
textNode = node.firstChild
if (offset) offset = textNode.nodeValue.length
while (topNode.parentNode != wrapper) topNode = topNode.parentNode
function find(textNode, topNode, offset) {
for (let i = -1; i < (maps ? maps.length : 0); i++) {
let map = i < 0 ? measure.map : maps[i]
for (let j = 0; j < map.length; j += 3) {
let curNode = map[j + 2]
if (curNode == textNode || curNode == topNode) {
let line = lineNo(i < 0 ? lineView.line : lineView.rest[i])
let ch = map[j] + offset
if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]
return Pos(line, ch)
// FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems
for (let after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {
for (let before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {