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