TextareaInput.js (13170B)
1 import { operation, runInOp } from "../display/operations.js" 2 import { prepareSelection } from "../display/selection.js" 3 import { applyTextInput, copyableRanges, handlePaste, hiddenTextarea, setLastCopied } from "./input.js" 4 import { cursorCoords, posFromMouse } from "../measurement/position_measurement.js" 5 import { eventInWidget } from "../measurement/widgets.js" 6 import { simpleSelection } from "../model/selection.js" 7 import { selectAll, setSelection } from "../model/selection_updates.js" 8 import { captureRightClick, ie, ie_version, ios, mac, mobile, presto, webkit } from "../util/browser.js" 9 import { activeElt, removeChildrenAndAdd, selectInput } from "../util/dom.js" 10 import { e_preventDefault, e_stop, off, on, signalDOMEvent } from "../util/event.js" 11 import { hasSelection } from "../util/feature_detection.js" 12 import { Delayed, sel_dontScroll } from "../util/misc.js" 13 14 // TEXTAREA INPUT STYLE 15 16 export default class TextareaInput { 17 constructor(cm) { 18 this.cm = cm 19 // See input.poll and input.reset 20 this.prevInput = "" 21 22 // Flag that indicates whether we expect input to appear real soon 23 // now (after some event like 'keypress' or 'input') and are 24 // polling intensively. 25 this.pollingFast = false 26 // Self-resetting timeout for the poller 27 this.polling = new Delayed() 28 // Used to work around IE issue with selection being forgotten when focus moves away from textarea 29 this.hasSelection = false 30 this.composing = null 31 } 32 33 init(display) { 34 let input = this, cm = this.cm 35 36 // Wraps and hides input textarea 37 let div = this.wrapper = hiddenTextarea() 38 // The semihidden textarea that is focused when the editor is 39 // focused, and receives input. 40 let te = this.textarea = div.firstChild 41 display.wrapper.insertBefore(div, display.wrapper.firstChild) 42 43 // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) 44 if (ios) te.style.width = "0px" 45 46 on(te, "input", () => { 47 if (ie && ie_version >= 9 && this.hasSelection) this.hasSelection = null 48 input.poll() 49 }) 50 51 on(te, "paste", e => { 52 if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return 53 54 cm.state.pasteIncoming = true 55 input.fastPoll() 56 }) 57 58 function prepareCopyCut(e) { 59 if (signalDOMEvent(cm, e)) return 60 if (cm.somethingSelected()) { 61 setLastCopied({lineWise: false, text: cm.getSelections()}) 62 } else if (!cm.options.lineWiseCopyCut) { 63 return 64 } else { 65 let ranges = copyableRanges(cm) 66 setLastCopied({lineWise: true, text: ranges.text}) 67 if (e.type == "cut") { 68 cm.setSelections(ranges.ranges, null, sel_dontScroll) 69 } else { 70 input.prevInput = "" 71 te.value = ranges.text.join("\n") 72 selectInput(te) 73 } 74 } 75 if (e.type == "cut") cm.state.cutIncoming = true 76 } 77 on(te, "cut", prepareCopyCut) 78 on(te, "copy", prepareCopyCut) 79 80 on(display.scroller, "paste", e => { 81 if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return 82 cm.state.pasteIncoming = true 83 input.focus() 84 }) 85 86 // Prevent normal selection in the editor (we handle our own) 87 on(display.lineSpace, "selectstart", e => { 88 if (!eventInWidget(display, e)) e_preventDefault(e) 89 }) 90 91 on(te, "compositionstart", () => { 92 let start = cm.getCursor("from") 93 if (input.composing) input.composing.range.clear() 94 input.composing = { 95 start: start, 96 range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) 97 } 98 }) 99 on(te, "compositionend", () => { 100 if (input.composing) { 101 input.poll() 102 input.composing.range.clear() 103 input.composing = null 104 } 105 }) 106 } 107 108 prepareSelection() { 109 // Redraw the selection and/or cursor 110 let cm = this.cm, display = cm.display, doc = cm.doc 111 let result = prepareSelection(cm) 112 113 // Move the hidden textarea near the cursor to prevent scrolling artifacts 114 if (cm.options.moveInputWithCursor) { 115 let headPos = cursorCoords(cm, doc.sel.primary().head, "div") 116 let wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect() 117 result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, 118 headPos.top + lineOff.top - wrapOff.top)) 119 result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, 120 headPos.left + lineOff.left - wrapOff.left)) 121 } 122 123 return result 124 } 125 126 showSelection(drawn) { 127 let cm = this.cm, display = cm.display 128 removeChildrenAndAdd(display.cursorDiv, drawn.cursors) 129 removeChildrenAndAdd(display.selectionDiv, drawn.selection) 130 if (drawn.teTop != null) { 131 this.wrapper.style.top = drawn.teTop + "px" 132 this.wrapper.style.left = drawn.teLeft + "px" 133 } 134 } 135 136 // Reset the input to correspond to the selection (or to be empty, 137 // when not typing and nothing is selected) 138 reset(typing) { 139 if (this.contextMenuPending || this.composing) return 140 let cm = this.cm 141 if (cm.somethingSelected()) { 142 this.prevInput = "" 143 let content = cm.getSelection() 144 this.textarea.value = content 145 if (cm.state.focused) selectInput(this.textarea) 146 if (ie && ie_version >= 9) this.hasSelection = content 147 } else if (!typing) { 148 this.prevInput = this.textarea.value = "" 149 if (ie && ie_version >= 9) this.hasSelection = null 150 } 151 } 152 153 getField() { return this.textarea } 154 155 supportsTouch() { return false } 156 157 focus() { 158 if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { 159 try { this.textarea.focus() } 160 catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM 161 } 162 } 163 164 blur() { this.textarea.blur() } 165 166 resetPosition() { 167 this.wrapper.style.top = this.wrapper.style.left = 0 168 } 169 170 receivedFocus() { this.slowPoll() } 171 172 // Poll for input changes, using the normal rate of polling. This 173 // runs as long as the editor is focused. 174 slowPoll() { 175 if (this.pollingFast) return 176 this.polling.set(this.cm.options.pollInterval, () => { 177 this.poll() 178 if (this.cm.state.focused) this.slowPoll() 179 }) 180 } 181 182 // When an event has just come in that is likely to add or change 183 // something in the input textarea, we poll faster, to ensure that 184 // the change appears on the screen quickly. 185 fastPoll() { 186 let missed = false, input = this 187 input.pollingFast = true 188 function p() { 189 let changed = input.poll() 190 if (!changed && !missed) {missed = true; input.polling.set(60, p)} 191 else {input.pollingFast = false; input.slowPoll()} 192 } 193 input.polling.set(20, p) 194 } 195 196 // Read input from the textarea, and update the document to match. 197 // When something is selected, it is present in the textarea, and 198 // selected (unless it is huge, in which case a placeholder is 199 // used). When nothing is selected, the cursor sits after previously 200 // seen text (can be empty), which is stored in prevInput (we must 201 // not reset the textarea when typing, because that breaks IME). 202 poll() { 203 let cm = this.cm, input = this.textarea, prevInput = this.prevInput 204 // Since this is called a *lot*, try to bail out as cheaply as 205 // possible when it is clear that nothing happened. hasSelection 206 // will be the case when there is a lot of text in the textarea, 207 // in which case reading its value would be expensive. 208 if (this.contextMenuPending || !cm.state.focused || 209 (hasSelection(input) && !prevInput && !this.composing) || 210 cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) 211 return false 212 213 let text = input.value 214 // If nothing changed, bail. 215 if (text == prevInput && !cm.somethingSelected()) return false 216 // Work around nonsensical selection resetting in IE9/10, and 217 // inexplicable appearance of private area unicode characters on 218 // some key combos in Mac (#2689). 219 if (ie && ie_version >= 9 && this.hasSelection === text || 220 mac && /[\uf700-\uf7ff]/.test(text)) { 221 cm.display.input.reset() 222 return false 223 } 224 225 if (cm.doc.sel == cm.display.selForContextMenu) { 226 let first = text.charCodeAt(0) 227 if (first == 0x200b && !prevInput) prevInput = "\u200b" 228 if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } 229 } 230 // Find the part of the input that is actually new 231 let same = 0, l = Math.min(prevInput.length, text.length) 232 while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same 233 234 runInOp(cm, () => { 235 applyTextInput(cm, text.slice(same), prevInput.length - same, 236 null, this.composing ? "*compose" : null) 237 238 // Don't leave long text in the textarea, since it makes further polling slow 239 if (text.length > 1000 || text.indexOf("\n") > -1) input.value = this.prevInput = "" 240 else this.prevInput = text 241 242 if (this.composing) { 243 this.composing.range.clear() 244 this.composing.range = cm.markText(this.composing.start, cm.getCursor("to"), 245 {className: "CodeMirror-composing"}) 246 } 247 }) 248 return true 249 } 250 251 ensurePolled() { 252 if (this.pollingFast && this.poll()) this.pollingFast = false 253 } 254 255 onKeyPress() { 256 if (ie && ie_version >= 9) this.hasSelection = null 257 this.fastPoll() 258 } 259 260 onContextMenu(e) { 261 let input = this, cm = input.cm, display = cm.display, te = input.textarea 262 let pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop 263 if (!pos || presto) return // Opera is difficult. 264 265 // Reset the current text selection only if the click is done outside of the selection 266 // and 'resetSelectionOnContextMenu' option is true. 267 let reset = cm.options.resetSelectionOnContextMenu 268 if (reset && cm.doc.sel.contains(pos) == -1) 269 operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll) 270 271 let oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText 272 input.wrapper.style.cssText = "position: absolute" 273 let wrapperBox = input.wrapper.getBoundingClientRect() 274 te.style.cssText = `position: absolute; width: 30px; height: 30px; 275 top: ${e.clientY - wrapperBox.top - 5}px; left: ${e.clientX - wrapperBox.left - 5}px; 276 z-index: 1000; background: ${ie ? "rgba(255, 255, 255, .05)" : "transparent"}; 277 outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);` 278 let oldScrollY 279 if (webkit) oldScrollY = window.scrollY // Work around Chrome issue (#2712) 280 display.input.focus() 281 if (webkit) window.scrollTo(null, oldScrollY) 282 display.input.reset() 283 // Adds "Select all" to context menu in FF 284 if (!cm.somethingSelected()) te.value = input.prevInput = " " 285 input.contextMenuPending = true 286 display.selForContextMenu = cm.doc.sel 287 clearTimeout(display.detectingSelectAll) 288 289 // Select-all will be greyed out if there's nothing to select, so 290 // this adds a zero-width space so that we can later check whether 291 // it got selected. 292 function prepareSelectAllHack() { 293 if (te.selectionStart != null) { 294 let selected = cm.somethingSelected() 295 let extval = "\u200b" + (selected ? te.value : "") 296 te.value = "\u21da" // Used to catch context-menu undo 297 te.value = extval 298 input.prevInput = selected ? "" : "\u200b" 299 te.selectionStart = 1; te.selectionEnd = extval.length 300 // Re-set this, in case some other handler touched the 301 // selection in the meantime. 302 display.selForContextMenu = cm.doc.sel 303 } 304 } 305 function rehide() { 306 input.contextMenuPending = false 307 input.wrapper.style.cssText = oldWrapperCSS 308 te.style.cssText = oldCSS 309 if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos) 310 311 // Try to detect the user choosing select-all 312 if (te.selectionStart != null) { 313 if (!ie || (ie && ie_version < 9)) prepareSelectAllHack() 314 let i = 0, poll = () => { 315 if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && 316 te.selectionEnd > 0 && input.prevInput == "\u200b") { 317 operation(cm, selectAll)(cm) 318 } else if (i++ < 10) { 319 display.detectingSelectAll = setTimeout(poll, 500) 320 } else { 321 display.selForContextMenu = null 322 display.input.reset() 323 } 324 } 325 display.detectingSelectAll = setTimeout(poll, 200) 326 } 327 } 328 329 if (ie && ie_version >= 9) prepareSelectAllHack() 330 if (captureRightClick) { 331 e_stop(e) 332 let mouseup = () => { 333 off(window, "mouseup", mouseup) 334 setTimeout(rehide, 20) 335 } 336 on(window, "mouseup", mouseup) 337 } else { 338 setTimeout(rehide, 50) 339 } 340 } 341 342 readOnlyChanged(val) { 343 if (!val) this.reset() 344 this.textarea.disabled = val == "nocursor" 345 } 346 347 setUneditable() {} 348 } 349 350 TextareaInput.prototype.needsContentAttribute = false