This commit is contained in:
546
node_modules/codemirror/src/input/ContentEditableInput.js
generated
vendored
Normal file
546
node_modules/codemirror/src/input/ContentEditableInput.js
generated
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
import { operation, runInOp } from "../display/operations.js"
|
||||
import { prepareSelection } from "../display/selection.js"
|
||||
import { regChange } from "../display/view_tracking.js"
|
||||
import { applyTextInput, copyableRanges, disableBrowserMagic, handlePaste, hiddenTextarea, lastCopied, setLastCopied } from "./input.js"
|
||||
import { cmp, maxPos, minPos, Pos } from "../line/pos.js"
|
||||
import { getBetween, getLine, lineNo } from "../line/utils_line.js"
|
||||
import { findViewForLine, findViewIndex, mapFromLineView, nodeAndOffsetInLineMap } from "../measurement/position_measurement.js"
|
||||
import { replaceRange } from "../model/changes.js"
|
||||
import { simpleSelection } from "../model/selection.js"
|
||||
import { setSelection } from "../model/selection_updates.js"
|
||||
import { getBidiPartAt, getOrder } from "../util/bidi.js"
|
||||
import { android, chrome, gecko, ie_version } from "../util/browser.js"
|
||||
import { activeElt, contains, range, removeChildrenAndAdd, selectInput, rootNode } from "../util/dom.js"
|
||||
import { on, signalDOMEvent } from "../util/event.js"
|
||||
import { Delayed, lst, sel_dontScroll } from "../util/misc.js"
|
||||
|
||||
// CONTENTEDITABLE INPUT STYLE
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
init(display) {
|
||||
let input = this, cm = input.cm
|
||||
let div = input.div = display.lineDiv
|
||||
div.contentEditable = true
|
||||
disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize)
|
||||
|
||||
function belongsToInput(e) {
|
||||
for (let t = e.target; t; t = t.parentNode) {
|
||||
if (t == div) return true
|
||||
if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) break
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
on(div, "paste", e => {
|
||||
if (!belongsToInput(e) || 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, () => this.updateFromDOM()), 20)
|
||||
})
|
||||
|
||||
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}
|
||||
})
|
||||
on(div, "compositionend", e => {
|
||||
if (this.composing) {
|
||||
if (e.data != this.composing.data) this.readFromDOMSoon()
|
||||
this.composing.done = true
|
||||
}
|
||||
})
|
||||
|
||||
on(div, "touchstart", () => input.forceCompositionEnd())
|
||||
|
||||
on(div, "input", () => {
|
||||
if (!this.composing) this.readFromDOMSoon()
|
||||
})
|
||||
|
||||
function onCopyCut(e) {
|
||||
if (!belongsToInput(e) || 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) {
|
||||
return
|
||||
} else {
|
||||
let ranges = copyableRanges(cm)
|
||||
setLastCopied({lineWise: true, text: ranges.text})
|
||||
if (e.type == "cut") {
|
||||
cm.operation(() => {
|
||||
cm.setSelections(ranges.ranges, 0, sel_dontScroll)
|
||||
cm.replaceSelection("", null, "cut")
|
||||
})
|
||||
}
|
||||
}
|
||||
if (e.clipboardData) {
|
||||
e.clipboardData.clearData()
|
||||
let content = lastCopied.text.join("\n")
|
||||
// iOS exposes the clipboard API, but seems to discard content inserted into it
|
||||
e.clipboardData.setData("Text", content)
|
||||
if (e.clipboardData.getData("Text") == content) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Old-fashioned briefly-focus-a-textarea hack
|
||||
let kludge = hiddenTextarea(), te = kludge.firstChild
|
||||
disableBrowserMagic(te)
|
||||
cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild)
|
||||
te.value = lastCopied.text.join("\n")
|
||||
let hadFocus = activeElt(rootNode(div))
|
||||
selectInput(te)
|
||||
setTimeout(() => {
|
||||
cm.display.lineSpace.removeChild(kludge)
|
||||
hadFocus.focus()
|
||||
if (hadFocus == div) input.showPrimarySelection()
|
||||
}, 50)
|
||||
}
|
||||
on(div, "copy", onCopyCut)
|
||||
on(div, "cut", onCopyCut)
|
||||
}
|
||||
|
||||
screenReaderLabelChanged(label) {
|
||||
// Label for screenreaders, accessibility
|
||||
if(label) {
|
||||
this.div.setAttribute('aria-label', label)
|
||||
} else {
|
||||
this.div.removeAttribute('aria-label')
|
||||
}
|
||||
}
|
||||
|
||||
prepareSelection() {
|
||||
let result = prepareSelection(this.cm, false)
|
||||
result.focus = activeElt(rootNode(this.div)) == this.div
|
||||
return result
|
||||
}
|
||||
|
||||
showSelection(info, takeFocus) {
|
||||
if (!info || !this.cm.display.view.length) return
|
||||
if (info.focus || takeFocus) this.showPrimarySelection()
|
||||
this.showMultipleSelections(info)
|
||||
}
|
||||
|
||||
getSelection() {
|
||||
return this.cm.display.wrapper.ownerDocument.getSelection()
|
||||
}
|
||||
|
||||
showPrimarySelection() {
|
||||
let sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary()
|
||||
let from = prim.from(), to = prim.to()
|
||||
|
||||
if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) {
|
||||
sel.removeAllRanges()
|
||||
return
|
||||
}
|
||||
|
||||
let curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset)
|
||||
let curFocus = domToPos(cm, sel.focusNode, sel.focusOffset)
|
||||
if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
|
||||
cmp(minPos(curAnchor, curFocus), from) == 0 &&
|
||||
cmp(maxPos(curAnchor, curFocus), to) == 0)
|
||||
return
|
||||
|
||||
let view = cm.display.view
|
||||
let start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) ||
|
||||
{node: view[0].measure.map[2], offset: 0}
|
||||
let end = to.line < cm.display.viewTo && posToDOM(cm, to)
|
||||
if (!end) {
|
||||
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]}
|
||||
}
|
||||
|
||||
if (!start || !end) {
|
||||
sel.removeAllRanges()
|
||||
return
|
||||
}
|
||||
|
||||
let old = sel.rangeCount && sel.getRangeAt(0), 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 && cm.state.focused) {
|
||||
sel.collapse(start.node, start.offset)
|
||||
if (!rng.collapsed) {
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(rng)
|
||||
}
|
||||
} else {
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(rng)
|
||||
}
|
||||
if (old && sel.anchorNode == null) sel.addRange(old)
|
||||
else if (gecko) this.startGracePeriod()
|
||||
}
|
||||
this.rememberSelection()
|
||||
}
|
||||
|
||||
startGracePeriod() {
|
||||
clearTimeout(this.gracePeriod)
|
||||
this.gracePeriod = setTimeout(() => {
|
||||
this.gracePeriod = false
|
||||
if (this.selectionChanged())
|
||||
this.cm.operation(() => this.cm.curOp.selectionChanged = true)
|
||||
}, 20)
|
||||
}
|
||||
|
||||
showMultipleSelections(info) {
|
||||
removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors)
|
||||
removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection)
|
||||
}
|
||||
|
||||
rememberSelection() {
|
||||
let sel = this.getSelection()
|
||||
this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
|
||||
this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
|
||||
}
|
||||
|
||||
selectionInEditor() {
|
||||
let sel = this.getSelection()
|
||||
if (!sel.rangeCount) return false
|
||||
let node = sel.getRangeAt(0).commonAncestorContainer
|
||||
return contains(this.div, node)
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.cm.options.readOnly != "nocursor") {
|
||||
if (!this.selectionInEditor() || activeElt(rootNode(this.div)) != this.div)
|
||||
this.showSelection(this.prepareSelection(), true)
|
||||
this.div.focus()
|
||||
}
|
||||
}
|
||||
blur() { this.div.blur() }
|
||||
getField() { return this.div }
|
||||
|
||||
supportsTouch() { return true }
|
||||
|
||||
receivedFocus() {
|
||||
let input = this
|
||||
if (this.selectionInEditor())
|
||||
setTimeout(() => this.pollSelection(), 20)
|
||||
else
|
||||
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)
|
||||
}
|
||||
}
|
||||
this.polling.set(this.cm.options.pollInterval, poll)
|
||||
}
|
||||
|
||||
selectionChanged() {
|
||||
let sel = this.getSelection()
|
||||
return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
|
||||
sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
|
||||
}
|
||||
|
||||
pollSelection() {
|
||||
if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) return
|
||||
let sel = this.getSelection(), cm = this.cm
|
||||
// On Android Chrome (version 56, at least), backspacing into an
|
||||
// uneditable block element will put the cursor in that element,
|
||||
// and then, because it's not editable, hide the virtual keyboard.
|
||||
// Because Android doesn't allow us to actually detect backspace
|
||||
// presses in a sane way, this code checks for when that happens
|
||||
// and simulates a backspace press in this case.
|
||||
if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) {
|
||||
this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs})
|
||||
this.blur()
|
||||
this.focus()
|
||||
return
|
||||
}
|
||||
if (this.composing) return
|
||||
this.rememberSelection()
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
pollContent() {
|
||||
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()
|
||||
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
|
||||
|
||||
let fromIndex, fromLine, fromNode
|
||||
if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
|
||||
fromLine = lineNo(display.view[0].line)
|
||||
fromNode = display.view[0].node
|
||||
} else {
|
||||
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
|
||||
} else {
|
||||
toLine = lineNo(display.view[toIndex + 1].line) - 1
|
||||
toNode = display.view[toIndex + 1].node.previousSibling
|
||||
}
|
||||
|
||||
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))
|
||||
++cutFront
|
||||
let newBot = lst(newText), oldBot = lst(oldText)
|
||||
let maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
|
||||
oldBot.length - (oldText.length == 1 ? cutFront : 0))
|
||||
while (cutEnd < maxCutEnd &&
|
||||
newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
|
||||
++cutEnd
|
||||
// Try to move start of change to start of selection if ambiguous
|
||||
if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) {
|
||||
while (cutFront && cutFront > from.ch &&
|
||||
newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) {
|
||||
cutFront--
|
||||
cutEnd++
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
ensurePolled() {
|
||||
this.forceCompositionEnd()
|
||||
}
|
||||
reset() {
|
||||
this.forceCompositionEnd()
|
||||
}
|
||||
forceCompositionEnd() {
|
||||
if (!this.composing) return
|
||||
clearTimeout(this.readDOMTimeout)
|
||||
this.composing = null
|
||||
this.updateFromDOM()
|
||||
this.div.blur()
|
||||
this.div.focus()
|
||||
}
|
||||
readFromDOMSoon() {
|
||||
if (this.readDOMTimeout != null) return
|
||||
this.readDOMTimeout = setTimeout(() => {
|
||||
this.readDOMTimeout = null
|
||||
if (this.composing) {
|
||||
if (this.composing.done) this.composing = null
|
||||
else return
|
||||
}
|
||||
this.updateFromDOM()
|
||||
}, 80)
|
||||
}
|
||||
|
||||
updateFromDOM() {
|
||||
if (this.cm.isReadOnly() || !this.pollContent())
|
||||
runInOp(this.cm, () => regChange(this.cm))
|
||||
}
|
||||
|
||||
setUneditable(node) {
|
||||
node.contentEditable = "false"
|
||||
}
|
||||
|
||||
onKeyPress(e) {
|
||||
if (e.charCode == 0 || this.composing) return
|
||||
e.preventDefault()
|
||||
if (!this.cm.isReadOnly())
|
||||
operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0)
|
||||
}
|
||||
|
||||
readOnlyChanged(val) {
|
||||
this.div.contentEditable = String(val != "nocursor")
|
||||
}
|
||||
|
||||
onContextMenu() {}
|
||||
resetPosition() {}
|
||||
}
|
||||
|
||||
ContentEditableInput.prototype.needsContentAttribute = true
|
||||
|
||||
function posToDOM(cm, pos) {
|
||||
let view = findViewForLine(cm, pos.line)
|
||||
if (!view || view.hidden) return null
|
||||
let line = getLine(cm.doc, pos.line)
|
||||
let info = mapFromLineView(view, line, pos.line)
|
||||
|
||||
let order = getOrder(line, cm.doc.direction), side = "left"
|
||||
if (order) {
|
||||
let partPos = getBidiPartAt(order, pos.ch)
|
||||
side = partPos % 2 ? "right" : "left"
|
||||
}
|
||||
let result = nodeAndOffsetInLineMap(info.map, pos.ch, side)
|
||||
result.offset = result.collapse == "right" ? result.end : result.start
|
||||
return result
|
||||
}
|
||||
|
||||
function isInGutter(node) {
|
||||
for (let scan = node; scan; scan = scan.parentNode)
|
||||
if (/CodeMirror-gutter-wrapper/.test(scan.className)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
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(), extraLinebreak = false
|
||||
function recognizeMarker(id) { return marker => marker.id == id }
|
||||
function close() {
|
||||
if (closing) {
|
||||
text += lineSep
|
||||
if (extraLinebreak) text += lineSep
|
||||
closing = extraLinebreak = false
|
||||
}
|
||||
}
|
||||
function addText(str) {
|
||||
if (str) {
|
||||
close()
|
||||
text += str
|
||||
}
|
||||
}
|
||||
function walk(node) {
|
||||
if (node.nodeType == 1) {
|
||||
let cmText = node.getAttribute("cm-text")
|
||||
if (cmText) {
|
||||
addText(cmText)
|
||||
return
|
||||
}
|
||||
let markerID = node.getAttribute("cm-marker"), range
|
||||
if (markerID) {
|
||||
let found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID))
|
||||
if (found.length && (range = found[0].find(0)))
|
||||
addText(getBetween(cm.doc, range.from, range.to).join(lineSep))
|
||||
return
|
||||
}
|
||||
if (node.getAttribute("contenteditable") == "false") return
|
||||
let isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName)
|
||||
if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) return
|
||||
|
||||
if (isBlock) close()
|
||||
for (let i = 0; i < node.childNodes.length; i++)
|
||||
walk(node.childNodes[i])
|
||||
|
||||
if (/^(pre|p)$/i.test(node.nodeName)) extraLinebreak = true
|
||||
if (isBlock) closing = true
|
||||
} else if (node.nodeType == 3) {
|
||||
addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " "))
|
||||
}
|
||||
}
|
||||
for (;;) {
|
||||
walk(from)
|
||||
if (from == to) break
|
||||
from = from.nextSibling
|
||||
extraLinebreak = false
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function domToPos(cm, node, offset) {
|
||||
let lineNode
|
||||
if (node == cm.display.lineDiv) {
|
||||
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]
|
||||
if (lineView.node == lineNode)
|
||||
return locateNodeInLineView(lineView, node, offset)
|
||||
}
|
||||
}
|
||||
|
||||
function locateNodeInLineView(lineView, node, offset) {
|
||||
let wrapper = lineView.text.firstChild, bad = false
|
||||
if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true)
|
||||
if (node == wrapper) {
|
||||
bad = true
|
||||
node = wrapper.childNodes[offset]
|
||||
offset = 0
|
||||
if (!node) {
|
||||
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
|
||||
let measure = lineView.measure, maps = measure.maps
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let found = find(textNode, topNode, offset)
|
||||
if (found) return badPos(found, bad)
|
||||
|
||||
// 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) {
|
||||
found = find(after, after.firstChild, 0)
|
||||
if (found)
|
||||
return badPos(Pos(found.line, found.ch - dist), bad)
|
||||
else
|
||||
dist += after.textContent.length
|
||||
}
|
||||
for (let before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {
|
||||
found = find(before, before.firstChild, -1)
|
||||
if (found)
|
||||
return badPos(Pos(found.line, found.ch + dist), bad)
|
||||
else
|
||||
dist += before.textContent.length
|
||||
}
|
||||
}
|
||||
380
node_modules/codemirror/src/input/TextareaInput.js
generated
vendored
Normal file
380
node_modules/codemirror/src/input/TextareaInput.js
generated
vendored
Normal file
@@ -0,0 +1,380 @@
|
||||
import { operation, runInOp } from "../display/operations.js"
|
||||
import { prepareSelection } from "../display/selection.js"
|
||||
import { applyTextInput, copyableRanges, handlePaste, hiddenTextarea, disableBrowserMagic, setLastCopied } from "./input.js"
|
||||
import { cursorCoords, posFromMouse } from "../measurement/position_measurement.js"
|
||||
import { eventInWidget } from "../measurement/widgets.js"
|
||||
import { simpleSelection } from "../model/selection.js"
|
||||
import { selectAll, setSelection } from "../model/selection_updates.js"
|
||||
import { captureRightClick, ie, ie_version, ios, mac, mobile, presto, webkit } from "../util/browser.js"
|
||||
import { activeElt, removeChildrenAndAdd, selectInput, rootNode } from "../util/dom.js"
|
||||
import { e_preventDefault, e_stop, off, on, signalDOMEvent } from "../util/event.js"
|
||||
import { hasSelection } from "../util/feature_detection.js"
|
||||
import { Delayed, sel_dontScroll } from "../util/misc.js"
|
||||
|
||||
// TEXTAREA INPUT STYLE
|
||||
|
||||
export default class TextareaInput {
|
||||
constructor(cm) {
|
||||
this.cm = cm
|
||||
// See input.poll and input.reset
|
||||
this.prevInput = ""
|
||||
|
||||
// Flag that indicates whether we expect input to appear real soon
|
||||
// now (after some event like 'keypress' or 'input') and are
|
||||
// polling intensively.
|
||||
this.pollingFast = false
|
||||
// Self-resetting timeout for the poller
|
||||
this.polling = new Delayed()
|
||||
// Used to work around IE issue with selection being forgotten when focus moves away from textarea
|
||||
this.hasSelection = false
|
||||
this.composing = null
|
||||
this.resetting = false
|
||||
}
|
||||
|
||||
init(display) {
|
||||
let input = this, cm = this.cm
|
||||
this.createField(display)
|
||||
const te = this.textarea
|
||||
|
||||
display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild)
|
||||
|
||||
// Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
|
||||
if (ios) te.style.width = "0px"
|
||||
|
||||
on(te, "input", () => {
|
||||
if (ie && ie_version >= 9 && this.hasSelection) this.hasSelection = null
|
||||
input.poll()
|
||||
})
|
||||
|
||||
on(te, "paste", e => {
|
||||
if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
|
||||
|
||||
cm.state.pasteIncoming = +new Date
|
||||
input.fastPoll()
|
||||
})
|
||||
|
||||
function prepareCopyCut(e) {
|
||||
if (signalDOMEvent(cm, e)) return
|
||||
if (cm.somethingSelected()) {
|
||||
setLastCopied({lineWise: false, text: cm.getSelections()})
|
||||
} else if (!cm.options.lineWiseCopyCut) {
|
||||
return
|
||||
} else {
|
||||
let ranges = copyableRanges(cm)
|
||||
setLastCopied({lineWise: true, text: ranges.text})
|
||||
if (e.type == "cut") {
|
||||
cm.setSelections(ranges.ranges, null, sel_dontScroll)
|
||||
} else {
|
||||
input.prevInput = ""
|
||||
te.value = ranges.text.join("\n")
|
||||
selectInput(te)
|
||||
}
|
||||
}
|
||||
if (e.type == "cut") cm.state.cutIncoming = +new Date
|
||||
}
|
||||
on(te, "cut", prepareCopyCut)
|
||||
on(te, "copy", prepareCopyCut)
|
||||
|
||||
on(display.scroller, "paste", e => {
|
||||
if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return
|
||||
if (!te.dispatchEvent) {
|
||||
cm.state.pasteIncoming = +new Date
|
||||
input.focus()
|
||||
return
|
||||
}
|
||||
|
||||
// Pass the `paste` event to the textarea so it's handled by its event listener.
|
||||
const event = new Event("paste")
|
||||
event.clipboardData = e.clipboardData
|
||||
te.dispatchEvent(event)
|
||||
})
|
||||
|
||||
// Prevent normal selection in the editor (we handle our own)
|
||||
on(display.lineSpace, "selectstart", e => {
|
||||
if (!eventInWidget(display, e)) e_preventDefault(e)
|
||||
})
|
||||
|
||||
on(te, "compositionstart", () => {
|
||||
let start = cm.getCursor("from")
|
||||
if (input.composing) input.composing.range.clear()
|
||||
input.composing = {
|
||||
start: start,
|
||||
range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
|
||||
}
|
||||
})
|
||||
on(te, "compositionend", () => {
|
||||
if (input.composing) {
|
||||
input.poll()
|
||||
input.composing.range.clear()
|
||||
input.composing = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createField(_display) {
|
||||
// Wraps and hides input textarea
|
||||
this.wrapper = hiddenTextarea()
|
||||
// The semihidden textarea that is focused when the editor is
|
||||
// focused, and receives input.
|
||||
this.textarea = this.wrapper.firstChild
|
||||
let opts = this.cm.options
|
||||
disableBrowserMagic(this.textarea, opts.spellcheck, opts.autocorrect, opts.autocapitalize)
|
||||
}
|
||||
|
||||
screenReaderLabelChanged(label) {
|
||||
// Label for screenreaders, accessibility
|
||||
if(label) {
|
||||
this.textarea.setAttribute('aria-label', label)
|
||||
} else {
|
||||
this.textarea.removeAttribute('aria-label')
|
||||
}
|
||||
}
|
||||
|
||||
prepareSelection() {
|
||||
// Redraw the selection and/or cursor
|
||||
let cm = this.cm, display = cm.display, doc = cm.doc
|
||||
let result = prepareSelection(cm)
|
||||
|
||||
// Move the hidden textarea near the cursor to prevent scrolling artifacts
|
||||
if (cm.options.moveInputWithCursor) {
|
||||
let headPos = cursorCoords(cm, doc.sel.primary().head, "div")
|
||||
let wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect()
|
||||
result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
|
||||
headPos.top + lineOff.top - wrapOff.top))
|
||||
result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
|
||||
headPos.left + lineOff.left - wrapOff.left))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
showSelection(drawn) {
|
||||
let cm = this.cm, display = cm.display
|
||||
removeChildrenAndAdd(display.cursorDiv, drawn.cursors)
|
||||
removeChildrenAndAdd(display.selectionDiv, drawn.selection)
|
||||
if (drawn.teTop != null) {
|
||||
this.wrapper.style.top = drawn.teTop + "px"
|
||||
this.wrapper.style.left = drawn.teLeft + "px"
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the input to correspond to the selection (or to be empty,
|
||||
// when not typing and nothing is selected)
|
||||
reset(typing) {
|
||||
if (this.contextMenuPending || this.composing && typing) return
|
||||
let cm = this.cm
|
||||
this.resetting = true
|
||||
if (cm.somethingSelected()) {
|
||||
this.prevInput = ""
|
||||
let content = cm.getSelection()
|
||||
this.textarea.value = content
|
||||
if (cm.state.focused) selectInput(this.textarea)
|
||||
if (ie && ie_version >= 9) this.hasSelection = content
|
||||
} else if (!typing) {
|
||||
this.prevInput = this.textarea.value = ""
|
||||
if (ie && ie_version >= 9) this.hasSelection = null
|
||||
}
|
||||
this.resetting = false
|
||||
}
|
||||
|
||||
getField() { return this.textarea }
|
||||
|
||||
supportsTouch() { return false }
|
||||
|
||||
focus() {
|
||||
if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt(rootNode(this.textarea)) != this.textarea)) {
|
||||
try { this.textarea.focus() }
|
||||
catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
|
||||
}
|
||||
}
|
||||
|
||||
blur() { this.textarea.blur() }
|
||||
|
||||
resetPosition() {
|
||||
this.wrapper.style.top = this.wrapper.style.left = 0
|
||||
}
|
||||
|
||||
receivedFocus() { this.slowPoll() }
|
||||
|
||||
// Poll for input changes, using the normal rate of polling. This
|
||||
// runs as long as the editor is focused.
|
||||
slowPoll() {
|
||||
if (this.pollingFast) return
|
||||
this.polling.set(this.cm.options.pollInterval, () => {
|
||||
this.poll()
|
||||
if (this.cm.state.focused) this.slowPoll()
|
||||
})
|
||||
}
|
||||
|
||||
// When an event has just come in that is likely to add or change
|
||||
// something in the input textarea, we poll faster, to ensure that
|
||||
// the change appears on the screen quickly.
|
||||
fastPoll() {
|
||||
let missed = false, input = this
|
||||
input.pollingFast = true
|
||||
function p() {
|
||||
let changed = input.poll()
|
||||
if (!changed && !missed) {missed = true; input.polling.set(60, p)}
|
||||
else {input.pollingFast = false; input.slowPoll()}
|
||||
}
|
||||
input.polling.set(20, p)
|
||||
}
|
||||
|
||||
// Read input from the textarea, and update the document to match.
|
||||
// When something is selected, it is present in the textarea, and
|
||||
// selected (unless it is huge, in which case a placeholder is
|
||||
// used). When nothing is selected, the cursor sits after previously
|
||||
// seen text (can be empty), which is stored in prevInput (we must
|
||||
// not reset the textarea when typing, because that breaks IME).
|
||||
poll() {
|
||||
let cm = this.cm, input = this.textarea, prevInput = this.prevInput
|
||||
// Since this is called a *lot*, try to bail out as cheaply as
|
||||
// possible when it is clear that nothing happened. hasSelection
|
||||
// will be the case when there is a lot of text in the textarea,
|
||||
// in which case reading its value would be expensive.
|
||||
if (this.contextMenuPending || this.resetting || !cm.state.focused ||
|
||||
(hasSelection(input) && !prevInput && !this.composing) ||
|
||||
cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
|
||||
return false
|
||||
|
||||
let text = input.value
|
||||
// If nothing changed, bail.
|
||||
if (text == prevInput && !cm.somethingSelected()) return false
|
||||
// Work around nonsensical selection resetting in IE9/10, and
|
||||
// inexplicable appearance of private area unicode characters on
|
||||
// some key combos in Mac (#2689).
|
||||
if (ie && ie_version >= 9 && this.hasSelection === text ||
|
||||
mac && /[\uf700-\uf7ff]/.test(text)) {
|
||||
cm.display.input.reset()
|
||||
return false
|
||||
}
|
||||
|
||||
if (cm.doc.sel == cm.display.selForContextMenu) {
|
||||
let first = text.charCodeAt(0)
|
||||
if (first == 0x200b && !prevInput) prevInput = "\u200b"
|
||||
if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") }
|
||||
}
|
||||
// Find the part of the input that is actually new
|
||||
let same = 0, l = Math.min(prevInput.length, text.length)
|
||||
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same
|
||||
|
||||
runInOp(cm, () => {
|
||||
applyTextInput(cm, text.slice(same), prevInput.length - same,
|
||||
null, this.composing ? "*compose" : null)
|
||||
|
||||
// Don't leave long text in the textarea, since it makes further polling slow
|
||||
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = this.prevInput = ""
|
||||
else this.prevInput = text
|
||||
|
||||
if (this.composing) {
|
||||
this.composing.range.clear()
|
||||
this.composing.range = cm.markText(this.composing.start, cm.getCursor("to"),
|
||||
{className: "CodeMirror-composing"})
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
ensurePolled() {
|
||||
if (this.pollingFast && this.poll()) this.pollingFast = false
|
||||
}
|
||||
|
||||
onKeyPress() {
|
||||
if (ie && ie_version >= 9) this.hasSelection = null
|
||||
this.fastPoll()
|
||||
}
|
||||
|
||||
onContextMenu(e) {
|
||||
let input = this, cm = input.cm, display = cm.display, te = input.textarea
|
||||
if (input.contextMenuPending) input.contextMenuPending()
|
||||
let pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop
|
||||
if (!pos || presto) return // Opera is difficult.
|
||||
|
||||
// Reset the current text selection only if the click is done outside of the selection
|
||||
// and 'resetSelectionOnContextMenu' option is true.
|
||||
let reset = cm.options.resetSelectionOnContextMenu
|
||||
if (reset && cm.doc.sel.contains(pos) == -1)
|
||||
operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll)
|
||||
|
||||
let oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText
|
||||
let wrapperBox = input.wrapper.offsetParent.getBoundingClientRect()
|
||||
input.wrapper.style.cssText = "position: static"
|
||||
te.style.cssText = `position: absolute; width: 30px; height: 30px;
|
||||
top: ${e.clientY - wrapperBox.top - 5}px; left: ${e.clientX - wrapperBox.left - 5}px;
|
||||
z-index: 1000; background: ${ie ? "rgba(255, 255, 255, .05)" : "transparent"};
|
||||
outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`
|
||||
let oldScrollY
|
||||
if (webkit) oldScrollY = te.ownerDocument.defaultView.scrollY // Work around Chrome issue (#2712)
|
||||
display.input.focus()
|
||||
if (webkit) te.ownerDocument.defaultView.scrollTo(null, oldScrollY)
|
||||
display.input.reset()
|
||||
// Adds "Select all" to context menu in FF
|
||||
if (!cm.somethingSelected()) te.value = input.prevInput = " "
|
||||
input.contextMenuPending = rehide
|
||||
display.selForContextMenu = cm.doc.sel
|
||||
clearTimeout(display.detectingSelectAll)
|
||||
|
||||
// Select-all will be greyed out if there's nothing to select, so
|
||||
// this adds a zero-width space so that we can later check whether
|
||||
// it got selected.
|
||||
function prepareSelectAllHack() {
|
||||
if (te.selectionStart != null) {
|
||||
let selected = cm.somethingSelected()
|
||||
let extval = "\u200b" + (selected ? te.value : "")
|
||||
te.value = "\u21da" // Used to catch context-menu undo
|
||||
te.value = extval
|
||||
input.prevInput = selected ? "" : "\u200b"
|
||||
te.selectionStart = 1; te.selectionEnd = extval.length
|
||||
// Re-set this, in case some other handler touched the
|
||||
// selection in the meantime.
|
||||
display.selForContextMenu = cm.doc.sel
|
||||
}
|
||||
}
|
||||
function rehide() {
|
||||
if (input.contextMenuPending != rehide) return
|
||||
input.contextMenuPending = false
|
||||
input.wrapper.style.cssText = oldWrapperCSS
|
||||
te.style.cssText = oldCSS
|
||||
if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos)
|
||||
|
||||
// Try to detect the user choosing select-all
|
||||
if (te.selectionStart != null) {
|
||||
if (!ie || (ie && ie_version < 9)) prepareSelectAllHack()
|
||||
let i = 0, poll = () => {
|
||||
if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
|
||||
te.selectionEnd > 0 && input.prevInput == "\u200b") {
|
||||
operation(cm, selectAll)(cm)
|
||||
} else if (i++ < 10) {
|
||||
display.detectingSelectAll = setTimeout(poll, 500)
|
||||
} else {
|
||||
display.selForContextMenu = null
|
||||
display.input.reset()
|
||||
}
|
||||
}
|
||||
display.detectingSelectAll = setTimeout(poll, 200)
|
||||
}
|
||||
}
|
||||
|
||||
if (ie && ie_version >= 9) prepareSelectAllHack()
|
||||
if (captureRightClick) {
|
||||
e_stop(e)
|
||||
let mouseup = () => {
|
||||
off(window, "mouseup", mouseup)
|
||||
setTimeout(rehide, 20)
|
||||
}
|
||||
on(window, "mouseup", mouseup)
|
||||
} else {
|
||||
setTimeout(rehide, 50)
|
||||
}
|
||||
}
|
||||
|
||||
readOnlyChanged(val) {
|
||||
if (!val) this.reset()
|
||||
this.textarea.disabled = val == "nocursor"
|
||||
this.textarea.readOnly = !!val
|
||||
}
|
||||
|
||||
setUneditable() {}
|
||||
}
|
||||
|
||||
TextareaInput.prototype.needsContentAttribute = false
|
||||
71
node_modules/codemirror/src/input/indent.js
generated
vendored
Normal file
71
node_modules/codemirror/src/input/indent.js
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import { getContextBefore } from "../line/highlight.js"
|
||||
import { Pos } from "../line/pos.js"
|
||||
import { getLine } from "../line/utils_line.js"
|
||||
import { replaceRange } from "../model/changes.js"
|
||||
import { Range } from "../model/selection.js"
|
||||
import { replaceOneSelection } from "../model/selection_updates.js"
|
||||
import { countColumn, Pass, spaceStr } from "../util/misc.js"
|
||||
|
||||
// Indent the given line. The how parameter can be "smart",
|
||||
// "add"/null, "subtract", or "prev". When aggressive is false
|
||||
// (typically set to true for forced single-line indents), empty
|
||||
// lines are not indented, and places where the mode returns Pass
|
||||
// are left alone.
|
||||
export function indentLine(cm, n, how, aggressive) {
|
||||
let doc = cm.doc, state
|
||||
if (how == null) how = "add"
|
||||
if (how == "smart") {
|
||||
// Fall back to "prev" when the mode doesn't have an indentation
|
||||
// method.
|
||||
if (!doc.mode.indent) how = "prev"
|
||||
else state = getContextBefore(cm, n).state
|
||||
}
|
||||
|
||||
let tabSize = cm.options.tabSize
|
||||
let line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize)
|
||||
if (line.stateAfter) line.stateAfter = null
|
||||
let curSpaceString = line.text.match(/^\s*/)[0], indentation
|
||||
if (!aggressive && !/\S/.test(line.text)) {
|
||||
indentation = 0
|
||||
how = "not"
|
||||
} else if (how == "smart") {
|
||||
indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text)
|
||||
if (indentation == Pass || indentation > 150) {
|
||||
if (!aggressive) return
|
||||
how = "prev"
|
||||
}
|
||||
}
|
||||
if (how == "prev") {
|
||||
if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize)
|
||||
else indentation = 0
|
||||
} else if (how == "add") {
|
||||
indentation = curSpace + cm.options.indentUnit
|
||||
} else if (how == "subtract") {
|
||||
indentation = curSpace - cm.options.indentUnit
|
||||
} else if (typeof how == "number") {
|
||||
indentation = curSpace + how
|
||||
}
|
||||
indentation = Math.max(0, indentation)
|
||||
|
||||
let indentString = "", pos = 0
|
||||
if (cm.options.indentWithTabs)
|
||||
for (let i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t"}
|
||||
if (pos < indentation) indentString += spaceStr(indentation - pos)
|
||||
|
||||
if (indentString != curSpaceString) {
|
||||
replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input")
|
||||
line.stateAfter = null
|
||||
return true
|
||||
} else {
|
||||
// Ensure that, if the cursor was in the whitespace at the start
|
||||
// of the line, it is moved to the end of that space.
|
||||
for (let i = 0; i < doc.sel.ranges.length; i++) {
|
||||
let range = doc.sel.ranges[i]
|
||||
if (range.head.line == n && range.head.ch < curSpaceString.length) {
|
||||
let pos = Pos(n, curSpaceString.length)
|
||||
replaceOneSelection(doc, i, new Range(pos, pos))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
node_modules/codemirror/src/input/input.js
generated
vendored
Normal file
134
node_modules/codemirror/src/input/input.js
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
import { runInOp } from "../display/operations.js"
|
||||
import { ensureCursorVisible } from "../display/scrolling.js"
|
||||
import { Pos } from "../line/pos.js"
|
||||
import { getLine } from "../line/utils_line.js"
|
||||
import { makeChange } from "../model/changes.js"
|
||||
import { ios, webkit } from "../util/browser.js"
|
||||
import { elt } from "../util/dom.js"
|
||||
import { lst, map } from "../util/misc.js"
|
||||
import { signalLater } from "../util/operation_group.js"
|
||||
import { splitLinesAuto } from "../util/feature_detection.js"
|
||||
|
||||
import { indentLine } from "./indent.js"
|
||||
|
||||
// This will be set to a {lineWise: bool, text: [string]} object, so
|
||||
// that, when pasting, we know what kind of selections the copied
|
||||
// text was made out of.
|
||||
export let lastCopied = null
|
||||
|
||||
export function setLastCopied(newLastCopied) {
|
||||
lastCopied = newLastCopied
|
||||
}
|
||||
|
||||
export function applyTextInput(cm, inserted, deleted, sel, origin) {
|
||||
let doc = cm.doc
|
||||
cm.display.shift = false
|
||||
if (!sel) sel = doc.sel
|
||||
|
||||
let recent = +new Date - 200
|
||||
let paste = origin == "paste" || cm.state.pasteIncoming > recent
|
||||
let textLines = splitLinesAuto(inserted), multiPaste = null
|
||||
// When pasting N lines into N selections, insert one line per selection
|
||||
if (paste && sel.ranges.length > 1) {
|
||||
if (lastCopied && lastCopied.text.join("\n") == inserted) {
|
||||
if (sel.ranges.length % lastCopied.text.length == 0) {
|
||||
multiPaste = []
|
||||
for (let i = 0; i < lastCopied.text.length; i++)
|
||||
multiPaste.push(doc.splitLines(lastCopied.text[i]))
|
||||
}
|
||||
} else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) {
|
||||
multiPaste = map(textLines, l => [l])
|
||||
}
|
||||
}
|
||||
|
||||
let updateInput = cm.curOp.updateInput
|
||||
// Normal behavior is to insert the new text into every selection
|
||||
for (let i = sel.ranges.length - 1; i >= 0; i--) {
|
||||
let range = sel.ranges[i]
|
||||
let from = range.from(), to = range.to()
|
||||
if (range.empty()) {
|
||||
if (deleted && deleted > 0) // Handle deletion
|
||||
from = Pos(from.line, from.ch - deleted)
|
||||
else if (cm.state.overwrite && !paste) // Handle overwrite
|
||||
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length))
|
||||
else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == textLines.join("\n"))
|
||||
from = to = Pos(from.line, 0)
|
||||
}
|
||||
let changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
|
||||
origin: origin || (paste ? "paste" : cm.state.cutIncoming > recent ? "cut" : "+input")}
|
||||
makeChange(cm.doc, changeEvent)
|
||||
signalLater(cm, "inputRead", cm, changeEvent)
|
||||
}
|
||||
if (inserted && !paste)
|
||||
triggerElectric(cm, inserted)
|
||||
|
||||
ensureCursorVisible(cm)
|
||||
if (cm.curOp.updateInput < 2) cm.curOp.updateInput = updateInput
|
||||
cm.curOp.typing = true
|
||||
cm.state.pasteIncoming = cm.state.cutIncoming = -1
|
||||
}
|
||||
|
||||
export function handlePaste(e, cm) {
|
||||
let pasted = e.clipboardData && e.clipboardData.getData("Text")
|
||||
if (pasted) {
|
||||
e.preventDefault()
|
||||
if (!cm.isReadOnly() && !cm.options.disableInput && cm.hasFocus())
|
||||
runInOp(cm, () => applyTextInput(cm, pasted, 0, null, "paste"))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerElectric(cm, inserted) {
|
||||
// When an 'electric' character is inserted, immediately trigger a reindent
|
||||
if (!cm.options.electricChars || !cm.options.smartIndent) return
|
||||
let sel = cm.doc.sel
|
||||
|
||||
for (let i = sel.ranges.length - 1; i >= 0; i--) {
|
||||
let range = sel.ranges[i]
|
||||
if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue
|
||||
let mode = cm.getModeAt(range.head)
|
||||
let indented = false
|
||||
if (mode.electricChars) {
|
||||
for (let j = 0; j < mode.electricChars.length; j++)
|
||||
if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {
|
||||
indented = indentLine(cm, range.head.line, "smart")
|
||||
break
|
||||
}
|
||||
} else if (mode.electricInput) {
|
||||
if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch)))
|
||||
indented = indentLine(cm, range.head.line, "smart")
|
||||
}
|
||||
if (indented) signalLater(cm, "electricInput", cm, range.head.line)
|
||||
}
|
||||
}
|
||||
|
||||
export function copyableRanges(cm) {
|
||||
let text = [], ranges = []
|
||||
for (let i = 0; i < cm.doc.sel.ranges.length; i++) {
|
||||
let line = cm.doc.sel.ranges[i].head.line
|
||||
let lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}
|
||||
ranges.push(lineRange)
|
||||
text.push(cm.getRange(lineRange.anchor, lineRange.head))
|
||||
}
|
||||
return {text: text, ranges: ranges}
|
||||
}
|
||||
|
||||
export function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) {
|
||||
field.setAttribute("autocorrect", autocorrect ? "on" : "off")
|
||||
field.setAttribute("autocapitalize", autocapitalize ? "on" : "off")
|
||||
field.setAttribute("spellcheck", !!spellcheck)
|
||||
}
|
||||
|
||||
export function hiddenTextarea() {
|
||||
let te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; min-height: 1em; outline: none")
|
||||
let div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;")
|
||||
// The textarea is kept positioned near the cursor to prevent the
|
||||
// fact that it'll be scrolled into view on input from scrolling
|
||||
// our fake cursor out of view. On webkit, when wrap=off, paste is
|
||||
// very slow. So make the area wide instead.
|
||||
if (webkit) te.style.width = "1000px"
|
||||
else te.setAttribute("wrap", "off")
|
||||
// If border: 0; -- iOS fails to open keyboard (issue #1287)
|
||||
if (ios) te.style.border = "1px solid black"
|
||||
return div
|
||||
}
|
||||
147
node_modules/codemirror/src/input/keymap.js
generated
vendored
Normal file
147
node_modules/codemirror/src/input/keymap.js
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
import { flipCtrlCmd, mac, presto } from "../util/browser.js"
|
||||
import { map } from "../util/misc.js"
|
||||
|
||||
import { keyNames } from "./keynames.js"
|
||||
|
||||
export let keyMap = {}
|
||||
|
||||
keyMap.basic = {
|
||||
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
|
||||
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
|
||||
"Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
|
||||
"Tab": "defaultTab", "Shift-Tab": "indentAuto",
|
||||
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite",
|
||||
"Esc": "singleSelection"
|
||||
}
|
||||
// Note that the save and find-related commands aren't defined by
|
||||
// default. User code or addons can define them. Unknown commands
|
||||
// are simply ignored.
|
||||
keyMap.pcDefault = {
|
||||
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
|
||||
"Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown",
|
||||
"Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
|
||||
"Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find",
|
||||
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
|
||||
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
|
||||
"Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
|
||||
"fallthrough": "basic"
|
||||
}
|
||||
// Very basic readline/emacs-style bindings, which are standard on Mac.
|
||||
keyMap.emacsy = {
|
||||
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
|
||||
"Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp",
|
||||
"Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine",
|
||||
"Ctrl-T": "transposeChars", "Ctrl-O": "openLine"
|
||||
}
|
||||
keyMap.macDefault = {
|
||||
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
|
||||
"Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
|
||||
"Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
|
||||
"Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
|
||||
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
|
||||
"Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
|
||||
"Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
|
||||
"fallthrough": ["basic", "emacsy"]
|
||||
}
|
||||
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault
|
||||
|
||||
// KEYMAP DISPATCH
|
||||
|
||||
function normalizeKeyName(name) {
|
||||
let parts = name.split(/-(?!$)/)
|
||||
name = parts[parts.length - 1]
|
||||
let alt, ctrl, shift, cmd
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
let mod = parts[i]
|
||||
if (/^(cmd|meta|m)$/i.test(mod)) cmd = true
|
||||
else if (/^a(lt)?$/i.test(mod)) alt = true
|
||||
else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true
|
||||
else if (/^s(hift)?$/i.test(mod)) shift = true
|
||||
else throw new Error("Unrecognized modifier name: " + mod)
|
||||
}
|
||||
if (alt) name = "Alt-" + name
|
||||
if (ctrl) name = "Ctrl-" + name
|
||||
if (cmd) name = "Cmd-" + name
|
||||
if (shift) name = "Shift-" + name
|
||||
return name
|
||||
}
|
||||
|
||||
// This is a kludge to keep keymaps mostly working as raw objects
|
||||
// (backwards compatibility) while at the same time support features
|
||||
// like normalization and multi-stroke key bindings. It compiles a
|
||||
// new normalized keymap, and then updates the old object to reflect
|
||||
// this.
|
||||
export function normalizeKeyMap(keymap) {
|
||||
let copy = {}
|
||||
for (let keyname in keymap) if (keymap.hasOwnProperty(keyname)) {
|
||||
let value = keymap[keyname]
|
||||
if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue
|
||||
if (value == "...") { delete keymap[keyname]; continue }
|
||||
|
||||
let keys = map(keyname.split(" "), normalizeKeyName)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let val, name
|
||||
if (i == keys.length - 1) {
|
||||
name = keys.join(" ")
|
||||
val = value
|
||||
} else {
|
||||
name = keys.slice(0, i + 1).join(" ")
|
||||
val = "..."
|
||||
}
|
||||
let prev = copy[name]
|
||||
if (!prev) copy[name] = val
|
||||
else if (prev != val) throw new Error("Inconsistent bindings for " + name)
|
||||
}
|
||||
delete keymap[keyname]
|
||||
}
|
||||
for (let prop in copy) keymap[prop] = copy[prop]
|
||||
return keymap
|
||||
}
|
||||
|
||||
export function lookupKey(key, map, handle, context) {
|
||||
map = getKeyMap(map)
|
||||
let found = map.call ? map.call(key, context) : map[key]
|
||||
if (found === false) return "nothing"
|
||||
if (found === "...") return "multi"
|
||||
if (found != null && handle(found)) return "handled"
|
||||
|
||||
if (map.fallthrough) {
|
||||
if (Object.prototype.toString.call(map.fallthrough) != "[object Array]")
|
||||
return lookupKey(key, map.fallthrough, handle, context)
|
||||
for (let i = 0; i < map.fallthrough.length; i++) {
|
||||
let result = lookupKey(key, map.fallthrough[i], handle, context)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modifier key presses don't count as 'real' key presses for the
|
||||
// purpose of keymap fallthrough.
|
||||
export function isModifierKey(value) {
|
||||
let name = typeof value == "string" ? value : keyNames[value.keyCode]
|
||||
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"
|
||||
}
|
||||
|
||||
export function addModifierNames(name, event, noShift) {
|
||||
let base = name
|
||||
if (event.altKey && base != "Alt") name = "Alt-" + name
|
||||
if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name
|
||||
if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Mod") name = "Cmd-" + name
|
||||
if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name
|
||||
return name
|
||||
}
|
||||
|
||||
// Look up the name of a key as indicated by an event object.
|
||||
export function keyName(event, noShift) {
|
||||
if (presto && event.keyCode == 34 && event["char"]) return false
|
||||
let name = keyNames[event.keyCode]
|
||||
if (name == null || event.altGraphKey) return false
|
||||
// Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause,
|
||||
// so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+)
|
||||
if (event.keyCode == 3 && event.code) name = event.code
|
||||
return addModifierNames(name, event, noShift)
|
||||
}
|
||||
|
||||
export function getKeyMap(val) {
|
||||
return typeof val == "string" ? keyMap[val] : val
|
||||
}
|
||||
17
node_modules/codemirror/src/input/keynames.js
generated
vendored
Normal file
17
node_modules/codemirror/src/input/keynames.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export let keyNames = {
|
||||
3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
|
||||
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
|
||||
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
|
||||
46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod",
|
||||
106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock",
|
||||
173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
|
||||
221: "]", 222: "'", 224: "Mod", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
|
||||
63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"
|
||||
}
|
||||
|
||||
// Number keys
|
||||
for (let i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i)
|
||||
// Alphabetic keys
|
||||
for (let i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i)
|
||||
// Function keys
|
||||
for (let i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i
|
||||
111
node_modules/codemirror/src/input/movement.js
generated
vendored
Normal file
111
node_modules/codemirror/src/input/movement.js
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Pos } from "../line/pos.js"
|
||||
import { prepareMeasureForLine, measureCharPrepared, wrappedLineExtentChar } from "../measurement/position_measurement.js"
|
||||
import { getBidiPartAt, getOrder } from "../util/bidi.js"
|
||||
import { findFirst, lst, skipExtendingChars } from "../util/misc.js"
|
||||
|
||||
function moveCharLogically(line, ch, dir) {
|
||||
let target = skipExtendingChars(line.text, ch + dir, dir)
|
||||
return target < 0 || target > line.text.length ? null : target
|
||||
}
|
||||
|
||||
export function moveLogically(line, start, dir) {
|
||||
let ch = moveCharLogically(line, start.ch, dir)
|
||||
return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before")
|
||||
}
|
||||
|
||||
export function endOfLine(visually, cm, lineObj, lineNo, dir) {
|
||||
if (visually) {
|
||||
if (cm.doc.direction == "rtl") dir = -dir
|
||||
let order = getOrder(lineObj, cm.doc.direction)
|
||||
if (order) {
|
||||
let part = dir < 0 ? lst(order) : order[0]
|
||||
let moveInStorageOrder = (dir < 0) == (part.level == 1)
|
||||
let sticky = moveInStorageOrder ? "after" : "before"
|
||||
let ch
|
||||
// With a wrapped rtl chunk (possibly spanning multiple bidi parts),
|
||||
// it could be that the last bidi part is not on the last visual line,
|
||||
// since visual lines contain content order-consecutive chunks.
|
||||
// Thus, in rtl, we are looking for the first (content-order) character
|
||||
// in the rtl chunk that is on the last line (that is, the same line
|
||||
// as the last (content-order) character).
|
||||
if (part.level > 0 || cm.doc.direction == "rtl") {
|
||||
let prep = prepareMeasureForLine(cm, lineObj)
|
||||
ch = dir < 0 ? lineObj.text.length - 1 : 0
|
||||
let targetTop = measureCharPrepared(cm, prep, ch).top
|
||||
ch = findFirst(ch => measureCharPrepared(cm, prep, ch).top == targetTop, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch)
|
||||
if (sticky == "before") ch = moveCharLogically(lineObj, ch, 1)
|
||||
} else ch = dir < 0 ? part.to : part.from
|
||||
return new Pos(lineNo, ch, sticky)
|
||||
}
|
||||
}
|
||||
return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after")
|
||||
}
|
||||
|
||||
export function moveVisually(cm, line, start, dir) {
|
||||
let bidi = getOrder(line, cm.doc.direction)
|
||||
if (!bidi) return moveLogically(line, start, dir)
|
||||
if (start.ch >= line.text.length) {
|
||||
start.ch = line.text.length
|
||||
start.sticky = "before"
|
||||
} else if (start.ch <= 0) {
|
||||
start.ch = 0
|
||||
start.sticky = "after"
|
||||
}
|
||||
let partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]
|
||||
if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) {
|
||||
// Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines,
|
||||
// nothing interesting happens.
|
||||
return moveLogically(line, start, dir)
|
||||
}
|
||||
|
||||
let mv = (pos, dir) => moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir)
|
||||
let prep
|
||||
let getWrappedLineExtent = ch => {
|
||||
if (!cm.options.lineWrapping) return {begin: 0, end: line.text.length}
|
||||
prep = prep || prepareMeasureForLine(cm, line)
|
||||
return wrappedLineExtentChar(cm, line, prep, ch)
|
||||
}
|
||||
let wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch)
|
||||
|
||||
if (cm.doc.direction == "rtl" || part.level == 1) {
|
||||
let moveInStorageOrder = (part.level == 1) == (dir < 0)
|
||||
let ch = mv(start, moveInStorageOrder ? 1 : -1)
|
||||
if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) {
|
||||
// Case 2: We move within an rtl part or in an rtl editor on the same visual line
|
||||
let sticky = moveInStorageOrder ? "before" : "after"
|
||||
return new Pos(start.line, ch, sticky)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Could not move within this bidi part in this visual line, so leave
|
||||
// the current bidi part
|
||||
|
||||
let searchInVisualLine = (partPos, dir, wrappedLineExtent) => {
|
||||
let getRes = (ch, moveInStorageOrder) => moveInStorageOrder
|
||||
? new Pos(start.line, mv(ch, 1), "before")
|
||||
: new Pos(start.line, ch, "after")
|
||||
|
||||
for (; partPos >= 0 && partPos < bidi.length; partPos += dir) {
|
||||
let part = bidi[partPos]
|
||||
let moveInStorageOrder = (dir > 0) == (part.level != 1)
|
||||
let ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1)
|
||||
if (part.from <= ch && ch < part.to) return getRes(ch, moveInStorageOrder)
|
||||
ch = moveInStorageOrder ? part.from : mv(part.to, -1)
|
||||
if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) return getRes(ch, moveInStorageOrder)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3a: Look for other bidi parts on the same visual line
|
||||
let res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent)
|
||||
if (res) return res
|
||||
|
||||
// Case 3b: Look for other bidi parts on the next visual line
|
||||
let nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1)
|
||||
if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) {
|
||||
res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh))
|
||||
if (res) return res
|
||||
}
|
||||
|
||||
// Case 4: Nowhere to move
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user