14a4053a101d7372eb5e4fe25161a043e6e661d91dc492f09011cfca1af4acd179d54f9931d13f967669a0afb6c8844458f4167a6051cfc942ee59346bd191 19 KB

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