From cfc5f49c0d484d8dff2a8b0b3155f04731df5023 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Fri, 9 May 2025 19:24:51 +0400 Subject: [PATCH 1/3] transform to es6 class --- ace-internal.d.ts | 14 +- ace.d.ts | 15 +- src/editor.js | 2 +- src/keyboard/textinput.js | 1463 +++++++++++++++++++------------------ types/ace-modules.d.ts | 76 +- 5 files changed, 848 insertions(+), 722 deletions(-) diff --git a/ace-internal.d.ts b/ace-internal.d.ts index febda4711f1..f4bd122c1f8 100644 --- a/ace-internal.d.ts +++ b/ace-internal.d.ts @@ -32,6 +32,7 @@ export namespace Ace { type HoverTooltip = import("ace-code/src/tooltip").HoverTooltip; type Tooltip = import("ace-code/src/tooltip").Tooltip; type PopupManager = import("ace-code/src/tooltip").PopupManager; + type TextInput = import("ace-code/src/keyboard/textinput").TextInput; type AfterLoadCallback = (err: Error | null, module: unknown) => void; type LoaderFunction = (moduleName: string, afterLoad: AfterLoadCallback) => void; @@ -967,12 +968,6 @@ export namespace Ace { new(session: EditSession): Selection; } - interface TextInput { - resetSelection(): void; - - setAriaOption(options?: { activeDescendant: string, role: string, setLabel: any }): void; - } - type CompleterCallback = (error: any, completions: Completion[]) => void; interface Completer { @@ -1292,6 +1287,13 @@ export namespace Ace { export interface ScrollbarEvents { "scroll": (e: { data: number }) => void; } + + export interface AriaOptions { + activeDescendant?: string; + role?: string; + setLabel?: boolean; + inline?: boolean; + } } diff --git a/ace.d.ts b/ace.d.ts index dddfe9a5ac3..4d8379ef290 100644 --- a/ace.d.ts +++ b/ace.d.ts @@ -41,6 +41,7 @@ declare module "ace-code" { type HoverTooltip = import("ace-code/src/tooltip").HoverTooltip; type Tooltip = import("ace-code/src/tooltip").Tooltip; type PopupManager = import("ace-code/src/tooltip").PopupManager; + type TextInput = import("ace-code/src/keyboard/textinput").TextInput; type AfterLoadCallback = (err: Error | null, module: unknown) => void; type LoaderFunction = (moduleName: string, afterLoad: AfterLoadCallback) => void; export interface ConfigOptions { @@ -775,14 +776,6 @@ declare module "ace-code" { var Selection: { new(session: EditSession): Selection; }; - interface TextInput { - resetSelection(): void; - setAriaOption(options?: { - activeDescendant: string; - role: string; - setLabel: any; - }): void; - } type CompleterCallback = (error: any, completions: Completion[]) => void; interface Completer { identifierRegexps?: Array; @@ -1032,6 +1025,12 @@ declare module "ace-code" { data: number; }) => void; } + export interface AriaOptions { + activeDescendant?: string; + role?: string; + setLabel?: boolean; + inline?: boolean; + } } export const config: typeof import("ace-code/src/config"); export function edit(el?: string | (HTMLElement & { diff --git a/src/editor.js b/src/editor.js index f9394b66d20..9f162c18c9a 100644 --- a/src/editor.js +++ b/src/editor.js @@ -906,7 +906,7 @@ class Editor { /** * - * @param {string | string[]} command + * @param {string | string[] | import("../ace-internal").Ace.Command} command * @param [args] * @return {boolean} */ diff --git a/src/keyboard/textinput.js b/src/keyboard/textinput.js index e118443245a..2d1ee5c7b34 100644 --- a/src/keyboard/textinput.js +++ b/src/keyboard/textinput.js @@ -12,7 +12,7 @@ var HAS_FOCUS_ARGS = useragent.isChrome > 63; var MAX_LINE_LENGTH = 400; /** - * + * * @type {{[key: string]: any}} */ var KEYS = require("../lib/keys"); @@ -21,273 +21,647 @@ var isIOS = useragent.isIOS; var valueResetRegex = isIOS ? /\s/ : /\n/; var isMobile = useragent.isMobile; -var TextInput; -TextInput= function(/**@type{HTMLTextAreaElement} */parentNode, /**@type{import("../editor").Editor} */host) { - /**@type {HTMLTextAreaElement & {msGetInputContext?: () => {compositionStartOffset: number}, getInputContext?: () => {compositionStartOffset: number}}}*/ - var text = dom.createElement("textarea"); - text.className = "ace_text-input"; - - text.setAttribute("wrap", "off"); - text.setAttribute("autocorrect", "off"); - text.setAttribute("autocapitalize", "off"); - text.setAttribute("spellcheck", "false"); - - text.style.opacity = "0"; - parentNode.insertBefore(text, parentNode.firstChild); - - this.setHost = function(newHost) { - host = newHost; - }; - - /**@type{boolean|string}*/var copied = false; - var pasted = false; - /**@type {(boolean|Object) & {context?: any, useTextareaForIME?: boolean, selectionStart?: number, markerRange?: any}}} */ - var inComposition = false; - var sendingText = false; - var tempStyle = ''; - - if (!isMobile) - text.style.fontSize = "1px"; - - var commandMode = false; - var ignoreFocusEvents = false; - - var lastValue = ""; - var lastSelectionStart = 0; - var lastSelectionEnd = 0; - var lastRestoreEnd = 0; - var rowStart = Number.MAX_SAFE_INTEGER; - var rowEnd = Number.MIN_SAFE_INTEGER; - var numberOfExtraLines = 0; - - // FOCUS - // ie9 throws error if document.activeElement is accessed too soon - try { var isFocused = document.activeElement === text; } catch(e) {} - - // Set number of extra lines in textarea, some screenreaders - // perform better with extra lines above and below in the textarea. - this.setNumberOfExtraLines = function(/**@type{number}*/number) { - rowStart = Number.MAX_SAFE_INTEGER; - rowEnd = Number.MIN_SAFE_INTEGER; +class TextInput { + /** + * @param {HTMLElement} parentNode + * @param {import("../editor").Editor} host + */ + constructor(parentNode, host) { + this.host = host; + /**@type {HTMLTextAreaElement & {msGetInputContext?: () => {compositionStartOffset: number}, getInputContext?: () => {compositionStartOffset: number}}}*/ + this.text = dom.createElement("textarea"); + this.text.className = "ace_text-input"; + + this.text.setAttribute("wrap", "off"); + this.text.setAttribute("autocorrect", "off"); + this.text.setAttribute("autocapitalize", "off"); + this.text.setAttribute("spellcheck", "false"); + + this.text.style.opacity = "0"; + parentNode.insertBefore(this.text, parentNode.firstChild); + + /**@type{boolean|string}*/this.copied = false; + this.pasted = false; + /**@type {(boolean|Object) & {context?: any, useTextareaForIME?: boolean, selectionStart?: number, markerRange?: any}}} */ + this.inComposition = false; + this.sendingText = false; + this.tempStyle = ''; + + if (!isMobile) this.text.style.fontSize = "1px"; + + this.commandMode = false; + this.ignoreFocusEvents = false; + + this.lastValue = ""; + this.lastSelectionStart = 0; + this.lastSelectionEnd = 0; + this.lastRestoreEnd = 0; + this.rowStart = Number.MAX_SAFE_INTEGER; + this.rowEnd = Number.MIN_SAFE_INTEGER; + this.numberOfExtraLines = 0; + + // FOCUS + // ie9 throws error if document.activeElement is accessed too soon + try { + this.$isFocused = document.activeElement === this.text; + } catch (e) { + } - if (number < 0) { - numberOfExtraLines = 0; - return; + this.cancelComposition = this.cancelComposition.bind(this); + + this.setAriaOptions({role: "textbox"}); + + event.addListener(this.text, "blur", (e) => { + if (this.ignoreFocusEvents) return; + host.onBlur(e); + this.$isFocused = false; + }, host); + event.addListener(this.text, "focus", (e) => { + if (this.ignoreFocusEvents) return; + this.$isFocused = true; + if (useragent.isEdge) { + // on edge focus event is fired even if document itself is not focused + try { + if (!document.hasFocus()) return; + } catch (e) { + } + } + host.onFocus(e); + if (useragent.isEdge) setTimeout(this.resetSelection.bind(this)); else this.resetSelection(); + }, host); + + /**@type {boolean | string}*/this.$focusScroll = false; + + host.on("beforeEndOperation", () => { + var curOp = host.curOp; + var commandName = curOp && curOp.command && curOp.command.name; + if (commandName == "insertstring") return; + var isUserAction = commandName && (curOp.docChanged || curOp.selectionChanged); + if (this.inComposition && isUserAction) { + // exit composition from commands other than insertstring + this.lastValue = this.text.value = ""; + this.onCompositionEnd(); + } + // sync value of textarea + this.resetSelection(); + }); + + // if cursor changes position, we need to update the label with the correct row + host.on("changeSelection", this.setAriaLabel.bind(this)); + + this.resetSelection = isIOS ? this.$resetSelectionIOS : this.$resetSelection; + + if (this.$isFocused) host.onFocus(); + + this.inputHandler = null; + this.afterContextMenu = false; + + event.addCommandKeyListener(this.text, (e, hashId, keyCode) => { + // ignore command events during composition as they will + // either be handled by ime itself or fired again after ime end + if (this.inComposition) return; + return host.onCommandKey(e, hashId, keyCode); + }, host); + + event.addListener(this.text, "select", this.onSelect.bind(this), host); + event.addListener(this.text, "input", this.onInput.bind(this), host); + + event.addListener(this.text, "cut", this.onCut.bind(this), host); + event.addListener(this.text, "copy", this.onCopy.bind(this), host); + event.addListener(this.text, "paste", this.onPaste.bind(this), host); + + + // Opera has no clipboard events + if (!('oncut' in this.text) || !('oncopy' in this.text) || !('onpaste' in this.text)) { + event.addListener(parentNode, "keydown", (e) => { + if ((useragent.isMac && !e.metaKey) || !e.ctrlKey) return; + + switch (e.keyCode) { + case 67: + this.onCopy(e); + break; + case 86: + this.onPaste(e); + break; + case 88: + this.onCut(e); + break; + } + }, host); } - - numberOfExtraLines = number; - }; - this.setAriaLabel = function() { - var ariaLabel = ""; - if (host.$textInputAriaLabel) { - ariaLabel += `${host.$textInputAriaLabel}, `; + this.syncComposition = lang.delayedCall(this.onCompositionUpdate.bind(this), 50).schedule.bind(null, null); //TODO: check this + + event.addListener(this.text, "compositionstart", this.onCompositionStart.bind(this), host); + event.addListener(this.text, "compositionupdate", this.onCompositionUpdate.bind(this), host); + event.addListener(this.text, "keyup", this.onKeyup.bind(this), host); + event.addListener(this.text, "keydown", this.syncComposition.bind(this), host); + event.addListener(this.text, "compositionend", this.onCompositionEnd.bind(this), host); + + this.closeTimeout; + + event.addListener(this.text, "mouseup", this.$onContextMenu.bind(this), host); + event.addListener(this.text, "mousedown", (e) => { + e.preventDefault(); + this.onContextMenuClose(); + }, host); + event.addListener(host.renderer.scroller, "contextmenu", this.$onContextMenu.bind(this), host); + event.addListener(this.text, "contextmenu", this.$onContextMenu.bind(this), host); + + if (isIOS) this.addIosSelectionHandler(parentNode, host, this.text); + } + + /** + * @internal + * @param {HTMLElement} parentNode + * @param {import("../editor").Editor} host + * @param {HTMLTextAreaElement} text + */ + addIosSelectionHandler(parentNode, host, text) { + var typingResetTimeout = null; + var typing = false; + + text.addEventListener("keydown", function (e) { + if (typingResetTimeout) clearTimeout(typingResetTimeout); + typing = true; + }, true); + + text.addEventListener("keyup", function (e) { + typingResetTimeout = setTimeout(function () { + typing = false; + }, 100); + }, true); + + // IOS doesn't fire events for arrow keys, but this unique hack changes everything! + var detectArrowKeys = (e) => { + if (document.activeElement !== text) return; + if (typing || this.inComposition || host.$mouseHandler.isMousePressed) return; + + if (this.copied) { + return; + } + var selectionStart = text.selectionStart; + var selectionEnd = text.selectionEnd; + + var key = null; + var modifier = 0; + // console.log(selectionStart, selectionEnd); + if (selectionStart == 0) { + key = KEYS.up; + } + else if (selectionStart == 1) { + key = KEYS.home; + } + else if (selectionEnd > this.lastSelectionEnd && this.lastValue[selectionEnd] == "\n") { + key = KEYS.end; + } + else if (selectionStart < this.lastSelectionStart && this.lastValue[selectionStart - 1] == " ") { + key = KEYS.left; + modifier = MODS.option; + } + else if (selectionStart < this.lastSelectionStart || (selectionStart == this.lastSelectionStart + && this.lastSelectionEnd != this.lastSelectionStart && selectionStart == selectionEnd)) { + key = KEYS.left; + } + else if (selectionEnd > this.lastSelectionEnd && this.lastValue.slice(0, selectionEnd).split( + "\n").length > 2) { + key = KEYS.down; + } + else if (selectionEnd > this.lastSelectionEnd && this.lastValue[selectionEnd - 1] == " ") { + key = KEYS.right; + modifier = MODS.option; + } + else if (selectionEnd > this.lastSelectionEnd || (selectionEnd == this.lastSelectionEnd + && this.lastSelectionEnd != this.lastSelectionStart && selectionStart == selectionEnd)) { + key = KEYS.right; + } + + if (selectionStart !== selectionEnd) modifier |= MODS.shift; + + if (key) { + var result = host.onCommandKey({}, modifier, key); + if (!result && host.commands) { + key = KEYS.keyCodeToString(key); + var command = host.commands.findKeyCommand(modifier, key); + if (command) host.execCommand(command); + } + this.lastSelectionStart = selectionStart; + this.lastSelectionEnd = selectionEnd; + this.resetSelection(""); + } + }; + // On iOS, "selectionchange" can only be attached to the document object... + document.addEventListener("selectionchange", detectArrowKeys); + host.on("destroy", function () { + document.removeEventListener("selectionchange", detectArrowKeys); + }); + } + + onContextMenuClose() { + clearTimeout(this.closeTimeout); + this.closeTimeout = setTimeout(() => { + if (this.tempStyle) { + this.text.style.cssText = this.tempStyle; + this.tempStyle = ''; + } + this.host.renderer.$isMousePressed = false; + if (this.host.renderer.$keepTextAreaAtCursor) this.host.renderer.$moveTextAreaToCursor(); + }, 0); + } + + $onContextMenu(e) { + this.host.textInput.onContextMenu(e); + this.onContextMenuClose(); + } + + /** + * @internal + * @param e + */ + onKeyup(e) { + // workaround for a bug in ie where pressing esc silently moves selection out of textarea + if (e.keyCode == 27 && this.text.value.length < this.text.selectionStart) { + if (!this.inComposition) this.lastValue = this.text.value; + this.lastSelectionStart = this.lastSelectionEnd = -1; + this.resetSelection(); } - if(host.session) { - var row = host.session.selection.cursor.row; - ariaLabel += nls("text-input.aria-label", "Cursor at row $0", [row + 1]); + this.syncComposition(); + } + + // COMPOSITION + + /** + * @internal + */ + cancelComposition() { + // force end composition + this.ignoreFocusEvents = true; + this.text.blur(); + this.text.focus(); + this.ignoreFocusEvents = false; + } + + /** + * @internal + */ + onCompositionStart(e) { + if (this.inComposition || !this.host.onCompositionStart || this.host.$readOnly) return; + + this.inComposition = {}; + + if (this.commandMode) return; + + if (e.data) this.inComposition.useTextareaForIME = false; + + setTimeout(this.onCompositionUpdate.bind(this), 0); + this.host._signal("compositionStart"); + this.host.on("mousedown", this.cancelComposition); //TODO: + + var range = this.host.getSelectionRange(); + range.end.row = range.start.row; + range.end.column = range.start.column; + this.inComposition.markerRange = range; + this.inComposition.selectionStart = this.lastSelectionStart; + this.host.onCompositionStart(this.inComposition); + + if (this.inComposition.useTextareaForIME) { + this.lastValue = this.text.value = ""; + this.lastSelectionStart = 0; + this.lastSelectionEnd = 0; + } + else { + if (this.text.msGetInputContext) this.inComposition.context = this.text.msGetInputContext(); + if (this.text.getInputContext) this.inComposition.context = this.text.getInputContext(); } - text.setAttribute("aria-label", ariaLabel); - }; + } - this.setAriaOptions = function(options) { - if (options.activeDescendant) { - text.setAttribute("aria-haspopup", "true"); - text.setAttribute("aria-autocomplete", options.inline ? "both" : "list"); - text.setAttribute("aria-activedescendant", options.activeDescendant); - } else { - text.setAttribute("aria-haspopup", "false"); - text.setAttribute("aria-autocomplete", "both"); - text.removeAttribute("aria-activedescendant"); + /** + * @internal + */ + onCompositionUpdate() { + if (!this.inComposition || !this.host.onCompositionUpdate || this.host.$readOnly) return; + if (this.commandMode) return this.cancelComposition(); + + if (this.inComposition.useTextareaForIME) { + this.host.onCompositionUpdate(this.text.value); } - if (options.role) { - text.setAttribute("role", options.role); - } - if (options.setLabel) { - text.setAttribute("aria-roledescription", nls("text-input.aria-roledescription", "editor")); - this.setAriaLabel(); + else { + var data = this.text.value; + this.sendText(data); + if (this.inComposition.markerRange) { + if (this.inComposition.context) { + this.inComposition.markerRange.start.column = this.inComposition.selectionStart = this.inComposition.context.compositionStartOffset; + } + this.inComposition.markerRange.end.column = this.inComposition.markerRange.start.column + + this.lastSelectionEnd - this.inComposition.selectionStart + this.lastRestoreEnd; + } } - }; + } - this.setAriaOptions({role: "textbox"}); + /** + * @internal + */ + onCompositionEnd(e) { + if (!this.host.onCompositionEnd || this.host.$readOnly) return; + this.inComposition = false; + this.host.onCompositionEnd(); + this.host.off("mousedown", this.cancelComposition); + // note that resetting value of textarea at this point doesn't always work + // because textarea value can be silently restored + if (e) this.onInput(); + } - event.addListener(text, "blur", function(e) { - if (ignoreFocusEvents) return; - host.onBlur(e); - isFocused = false; - }, host); - event.addListener(text, "focus", function(e) { - if (ignoreFocusEvents) return; - isFocused = true; - if (useragent.isEdge) { - // on edge focus event is fired even if document itself is not focused - try { - if (!document.hasFocus()) - return; - } catch(e) {} - } - host.onFocus(e); - if (useragent.isEdge) - setTimeout(resetSelection); - else - resetSelection(); - }, host); /** - * - * @type {boolean | string} + * @internal */ - this.$focusScroll = false; - this.focus = function() { - // On focusing on the textarea, read active row number to assistive tech. - this.setAriaOptions({ - setLabel: host.renderer.enableKeyboardAccessibility - }); + onCut(e) { + this.doCopy(e, true); + } - if (tempStyle || HAS_FOCUS_ARGS || this.$focusScroll == "browser") - return text.focus({ preventScroll: true }); + /** + * @internal + */ + onCopy(e) { + this.doCopy(e, false); + } - var top = text.style.top; - text.style.position = "fixed"; - text.style.top = "0px"; - try { - var isTransformed = text.getBoundingClientRect().top != 0; - } catch(e) { - // getBoundingClientRect on IE throws error if element is not in the dom tree - return; + /** + * @internal + */ + onPaste(e) { + var data = this.handleClipboardData(e); + if (clipboard.pasteCancelled()) return; + if (typeof data == "string") { + if (data) this.host.onPaste(data, e); + if (useragent.isIE) setTimeout(this.resetSelection); + event.preventDefault(e); } - var ancestors = []; - if (isTransformed) { - var t = text.parentElement; - while (t && t.nodeType == 1) { - ancestors.push(t); - t.setAttribute("ace_nocontext", "true"); - if (!t.parentElement && t.getRootNode) - t = t.getRootNode()["host"]; - else - t = t.parentElement; + else { + this.text.value = ""; + this.pasted = true; + } + } + + /** + * @internal + * @param {ClipboardEvent} e + * @param {boolean} isCut + */ + doCopy(e, isCut) { + var data = this.host.getCopyText(); + if (!data) return event.preventDefault(e); + + if (this.handleClipboardData(e, data)) { + if (isIOS) { + this.resetSelection(data); + this.copied = data; + setTimeout(() => { + this.copied = false; + }, 10); } + isCut ? this.host.onCut() : this.host.onCopy(); + event.preventDefault(e); } - text.focus({ preventScroll: true }); - if (isTransformed) { - ancestors.forEach(function(p) { - p.removeAttribute("ace_nocontext"); + else { + this.copied = true; + this.text.value = data; + this.text.select(); + setTimeout(() => { + this.copied = false; + this.resetSelection(); + isCut ? this.host.onCut() : this.host.onCopy(); }); } - setTimeout(function() { - text.style.position = ""; - if (text.style.top == "0px") - text.style.top = top; - }, 0); - }; - this.blur = function() { - text.blur(); - }; - this.isFocused = function() { - return isFocused; - }; - - host.on("beforeEndOperation", function() { - var curOp = host.curOp; - var commandName = curOp && curOp.command && curOp.command.name; - if (commandName == "insertstring") - return; - var isUserAction = commandName && (curOp.docChanged || curOp.selectionChanged); - if (inComposition && isUserAction) { - // exit composition from commands other than insertstring - lastValue = text.value = ""; - onCompositionEnd(); - } - // sync value of textarea - resetSelection(); - }); - - // if cursor changes position, we need to update the label with the correct row - host.on("changeSelection", this.setAriaLabel); - - // Convert from row,column position to the linear position with respect to the current - // block of lines in the textarea. - var positionToSelection = function(row, column) { - var selection = column; - - for (var i = 1; i <= row - rowStart && i < 2*numberOfExtraLines + 1; i++) { - selection += host.session.getLine(row - i).length + 1; - } - return selection; - }; - - var resetSelection = isIOS - ? function(value) { - if (!isFocused || (copied && !value) || sendingText) return; - if (!value) - value = ""; + } + + /** + * + * @internal + * @param {ClipboardEvent} e + * @param {string} [data] + * @param {boolean} [forceIEMime] + */ + handleClipboardData(e, data, forceIEMime) { + var clipboardData = e.clipboardData || window["clipboardData"]; + if (!clipboardData || BROKEN_SETDATA) return; + // using "Text" doesn't work on old webkit but ie needs it + var mime = USE_IE_MIME_TYPE || forceIEMime ? "Text" : "text/plain"; + try { + if (data) { + // Safari 5 has clipboardData object, but does not handle setData() + return clipboardData.setData(mime, data) !== false; + } + else { + return clipboardData.getData(mime); + } + } catch (e) { + if (!forceIEMime) return this.handleClipboardData(e, data, true); + } + } + + /** + * @internal + * @param e + */ + onInput(e) { + if (this.inComposition) return this.onCompositionUpdate(); + if (e && e.inputType) { + if (e.inputType == "historyUndo") return this.host.execCommand("undo"); + if (e.inputType == "historyRedo") return this.host.execCommand("redo"); + } + var data = this.text.value; + var inserted = this.sendText(data, true); + if (data.length > MAX_LINE_LENGTH + 100 || valueResetRegex.test(inserted) || isMobile && this.lastSelectionStart + < 1 && this.lastSelectionStart == this.lastSelectionEnd) { + this.resetSelection(); + } + } + + /** + * @internal + * @param {string} value + * @param {boolean} [fromInput] + * @return {string} + */ + sendText(value, fromInput) { + if (this.afterContextMenu) this.afterContextMenu = false; + if (this.pasted) { + this.resetSelection(); + if (value) this.host.onPaste(value); + this.pasted = false; + return ""; + } + else { + var selectionStart = this.text.selectionStart; + var selectionEnd = this.text.selectionEnd; + + var extendLeft = this.lastSelectionStart; + var extendRight = this.lastValue.length - this.lastSelectionEnd; + + var inserted = value; + var restoreStart = value.length - selectionStart; + var restoreEnd = value.length - selectionEnd; + + var i = 0; + while (extendLeft > 0 && this.lastValue[i] == value[i]) { + i++; + extendLeft--; + } + inserted = inserted.slice(i); + i = 1; + while (extendRight > 0 && this.lastValue.length - i > this.lastSelectionStart - 1 + && this.lastValue[this.lastValue.length - i] == value[value.length - i]) { + i++; + extendRight--; + } + restoreStart -= i - 1; + restoreEnd -= i - 1; + var endIndex = inserted.length - i + 1; + if (endIndex < 0) { + extendLeft = -endIndex; + endIndex = 0; + } + inserted = inserted.slice(0, endIndex); + + // composition update can be called without any change + if (!fromInput && !inserted && !restoreStart && !extendLeft && !extendRight && !restoreEnd) return ""; + this.sendingText = true; + + // some android keyboards converts two spaces into sentence end, which is not useful for code + var shouldReset = false; + if (useragent.isAndroid && inserted == ". ") { + inserted = " "; + shouldReset = true; + } + + if (inserted && !extendLeft && !extendRight && !restoreStart && !restoreEnd || this.commandMode) { + this.host.onTextInput(inserted); + } + else { + this.host.onTextInput(inserted, { + extendLeft: extendLeft, + extendRight: extendRight, + restoreStart: restoreStart, + restoreEnd: restoreEnd + }); + } + this.sendingText = false; + + this.lastValue = value; + this.lastSelectionStart = selectionStart; + this.lastSelectionEnd = selectionEnd; + this.lastRestoreEnd = restoreEnd; + return shouldReset ? "\n" : inserted; + } + } + + /** + * @internal + * @param e + */ + onSelect(e) { + if (this.inComposition) return; + + var isAllSelected = (text) => { + return text.selectionStart === 0 && text.selectionEnd >= this.lastValue.length && text.value + === this.lastValue && this.lastValue && text.selectionEnd !== this.lastSelectionEnd; + }; + + if (this.copied) { + this.copied = false; + } + else if (isAllSelected(this.text)) { + this.host.selectAll(); + this.resetSelection(); + } + else if (isMobile && this.text.selectionStart != this.lastSelectionStart) { + this.resetSelection(); + } + } + + $resetSelectionIOS(value) { + if (!this.$isFocused || (this.copied && !value) || this.sendingText) return; + if (!value) value = ""; var newValue = "\n ab" + value + "cde fg\n"; - if (newValue != text.value) - text.value = lastValue = newValue; - + if (newValue != this.text.value) this.text.value = this.lastValue = newValue; + var selectionStart = 4; - var selectionEnd = 4 + (value.length || (host.selection.isEmpty() ? 0 : 1)); + var selectionEnd = 4 + (value.length || (this.host.selection.isEmpty() ? 0 : 1)); - if (lastSelectionStart != selectionStart || lastSelectionEnd != selectionEnd) { - text.setSelectionRange(selectionStart, selectionEnd); + if (this.lastSelectionStart != selectionStart || this.lastSelectionEnd != selectionEnd) { + this.text.setSelectionRange(selectionStart, selectionEnd); } - lastSelectionStart = selectionStart; - lastSelectionEnd = selectionEnd; + this.lastSelectionStart = selectionStart; + this.lastSelectionEnd = selectionEnd; } - : function() { - if (inComposition || sendingText) - return; + + $resetSelection() { + if (this.inComposition || this.sendingText) return; // modifying selection of blured textarea can focus it (chrome mac/linux) - if (!isFocused && !afterContextMenu) - return; + if (!this.$isFocused && !this.afterContextMenu) return; // see https://github.com/ajaxorg/ace/issues/2114 // this prevents infinite recursion on safari 8 - inComposition = true; - + this.inComposition = true; + var selectionStart = 0; var selectionEnd = 0; var line = ""; - if (host.session) { - var selection = host.selection; + // Convert from row,column position to the linear position with respect to the current + // block of lines in the textarea. + var positionToSelection = (row, column) => { + var selection = column; + + for (var i = 1; i <= row - this.rowStart && i < 2 * this.numberOfExtraLines + 1; i++) { + selection += this.host.session.getLine(row - i).length + 1; + } + return selection; + }; + + if (this.host.session) { + var selection = this.host.selection; var range = selection.getRange(); var row = selection.cursor.row; // We keep 2*numberOfExtraLines + 1 lines in the textarea, if the new active row // is within the current block of lines in the textarea we do nothing. If the new row // is one row above or below the current block, move up or down to the next block of lines. - // If the new row is further than 1 row away from the current block grab a new block centered + // If the new row is further than 1 row away from the current block grab a new block centered // around the new row. - if (row === rowEnd + 1) { - rowStart = rowEnd + 1; - rowEnd = rowStart + 2*numberOfExtraLines; - } else if (row === rowStart - 1) { - rowEnd = rowStart - 1; - rowStart = rowEnd - 2*numberOfExtraLines; - } else if (row < rowStart - 1 || row > rowEnd + 1) { - rowStart = row > numberOfExtraLines ? row - numberOfExtraLines : 0; - rowEnd = row > numberOfExtraLines ? row + numberOfExtraLines : 2*numberOfExtraLines; - } - + if (row === this.rowEnd + 1) { + this.rowStart = this.rowEnd + 1; + this.rowEnd = this.rowStart + 2 * this.numberOfExtraLines; + } + else if (row === this.rowStart - 1) { + this.rowEnd = this.rowStart - 1; + this.rowStart = this.rowEnd - 2 * this.numberOfExtraLines; + } + else if (row < this.rowStart - 1 || row > this.rowEnd + 1) { + this.rowStart = row > this.numberOfExtraLines ? row - this.numberOfExtraLines : 0; + this.rowEnd = row > this.numberOfExtraLines ? row + this.numberOfExtraLines : 2 + * this.numberOfExtraLines; + } + var lines = []; - for (var i = rowStart; i <= rowEnd; i++) { - lines.push(host.session.getLine(i)); + for (var i = this.rowStart; i <= this.rowEnd; i++) { + lines.push(this.host.session.getLine(i)); } - + line = lines.join('\n'); selectionStart = positionToSelection(range.start.row, range.start.column); selectionEnd = positionToSelection(range.end.row, range.end.column); - - if (range.start.row < rowStart) { - var prevLine = host.session.getLine(rowStart - 1); - selectionStart = range.start.row < rowStart - 1 ? 0 : selectionStart; + + if (range.start.row < this.rowStart) { + var prevLine = this.host.session.getLine(this.rowStart - 1); + selectionStart = range.start.row < this.rowStart - 1 ? 0 : selectionStart; selectionEnd += prevLine.length + 1; line = prevLine + "\n" + line; } - else if (range.end.row > rowEnd) { - var nextLine = host.session.getLine(rowEnd + 1); - selectionEnd = range.end.row > rowEnd + 1 ? nextLine.length : range.end.column; + else if (range.end.row > this.rowEnd) { + var nextLine = this.host.session.getLine(this.rowEnd + 1); + selectionEnd = range.end.row > this.rowEnd + 1 ? nextLine.length : range.end.column; selectionEnd += line.length + 1; line = line + "\n" + nextLine; } @@ -300,7 +674,8 @@ TextInput= function(/**@type{HTMLTextAreaElement} */parentNode, /**@type{import( if (line.length > MAX_LINE_LENGTH) { if (selectionStart < MAX_LINE_LENGTH && selectionEnd < MAX_LINE_LENGTH) { line = line.slice(0, MAX_LINE_LENGTH); - } else { + } + else { line = "\n"; if (selectionStart == selectionEnd) { selectionStart = selectionEnd = 0; @@ -311,536 +686,216 @@ TextInput= function(/**@type{HTMLTextAreaElement} */parentNode, /**@type{import( } } } - + var newValue = line + "\n\n"; - if (newValue != lastValue) { - text.value = lastValue = newValue; - lastSelectionStart = lastSelectionEnd = newValue.length; + if (newValue != this.lastValue) { + this.text.value = this.lastValue = newValue; + this.lastSelectionStart = this.lastSelectionEnd = newValue.length; } } - + // contextmenu on mac may change the selection - if (afterContextMenu) { - lastSelectionStart = text.selectionStart; - lastSelectionEnd = text.selectionEnd; + if (this.afterContextMenu) { + this.lastSelectionStart = this.text.selectionStart; + this.lastSelectionEnd = this.text.selectionEnd; } // on firefox this throws if textarea is hidden - if ( - lastSelectionEnd != selectionEnd - || lastSelectionStart != selectionStart - || text.selectionEnd != lastSelectionEnd // on ie edge selectionEnd changes silently after the initialization + if (this.lastSelectionEnd != selectionEnd || this.lastSelectionStart != selectionStart || this.text.selectionEnd + != this.lastSelectionEnd // on ie edge selectionEnd changes silently after the initialization ) { try { - text.setSelectionRange(selectionStart, selectionEnd); - lastSelectionStart = selectionStart; - lastSelectionEnd = selectionEnd; - } catch(e){} + this.text.setSelectionRange(selectionStart, selectionEnd); + this.lastSelectionStart = selectionStart; + this.lastSelectionEnd = selectionEnd; + } catch (e) { + } } - inComposition = false; - }; - this.resetSelection = resetSelection; - - if (isFocused) - host.onFocus(); + this.inComposition = false; + } + /** + * @param {import("../editor").Editor} newHost + */ + setHost(newHost) { + this.host = newHost; + } - var isAllSelected = function(text) { - return text.selectionStart === 0 && text.selectionEnd >= lastValue.length - && text.value === lastValue && lastValue - && text.selectionEnd !== lastSelectionEnd; - }; + /** + * Sets the number of extra lines in the textarea to improve screen reader compatibility. + * Extra lines can help screen readers perform better when reading text. + * + * @param {number} number - The number of extra lines to add. Must be non-negative. + */ + setNumberOfExtraLines(number) { + this.rowStart = Number.MAX_SAFE_INTEGER; + this.rowEnd = Number.MIN_SAFE_INTEGER; - var onSelect = function(e) { - if (inComposition) - return; - if (copied) { - copied = false; - } else if (isAllSelected(text)) { - host.selectAll(); - resetSelection(); - } else if (isMobile && text.selectionStart != lastSelectionStart) { - resetSelection(); - } - }; - - - var inputHandler = null; - this.setInputHandler = function(cb) {inputHandler = cb;}; - this.getInputHandler = function() {return inputHandler;}; - var afterContextMenu = false; - - var sendText = function(value, fromInput) { - if (afterContextMenu) - afterContextMenu = false; - if (pasted) { - resetSelection(); - if (value) - host.onPaste(value); - pasted = false; - return ""; - } else { - var selectionStart = text.selectionStart; - var selectionEnd = text.selectionEnd; - - var extendLeft = lastSelectionStart; - var extendRight = lastValue.length - lastSelectionEnd; - - var inserted = value; - var restoreStart = value.length - selectionStart; - var restoreEnd = value.length - selectionEnd; - - var i = 0; - while (extendLeft > 0 && lastValue[i] == value[i]) { - i++; - extendLeft--; - } - inserted = inserted.slice(i); - i = 1; - while (extendRight > 0 && lastValue.length - i > lastSelectionStart - 1 && lastValue[lastValue.length - i] == value[value.length - i]) { - i++; - extendRight--; - } - restoreStart -= i-1; - restoreEnd -= i-1; - var endIndex = inserted.length - i + 1; - if (endIndex < 0) { - extendLeft = -endIndex; - endIndex = 0; - } - inserted = inserted.slice(0, endIndex); - - // composition update can be called without any change - if (!fromInput && !inserted && !restoreStart && !extendLeft && !extendRight && !restoreEnd) - return ""; - sendingText = true; - - // some android keyboards converts two spaces into sentence end, which is not useful for code - var shouldReset = false; - if (useragent.isAndroid && inserted == ". ") { - inserted = " "; - shouldReset = true; - } - - if (inserted && !extendLeft && !extendRight && !restoreStart && !restoreEnd || commandMode) { - host.onTextInput(inserted); - } else { - host.onTextInput(inserted, { - extendLeft: extendLeft, - extendRight: extendRight, - restoreStart: restoreStart, - restoreEnd: restoreEnd - }); - } - sendingText = false; - - lastValue = value; - lastSelectionStart = selectionStart; - lastSelectionEnd = selectionEnd; - lastRestoreEnd = restoreEnd; - return shouldReset ? "\n" : inserted; - } - }; - var onInput = function(e) { - if (inComposition) - return onCompositionUpdate(); - if (e && e.inputType) { - if (e.inputType == "historyUndo") return host.execCommand("undo"); - if (e.inputType == "historyRedo") return host.execCommand("redo"); - } - var data = text.value; - var inserted = sendText(data, true); - if ( - data.length > MAX_LINE_LENGTH + 100 - || valueResetRegex.test(inserted) - || isMobile && lastSelectionStart < 1 && lastSelectionStart == lastSelectionEnd - ) { - resetSelection(); - } - }; - - var handleClipboardData = function(e, data, forceIEMime) { - var clipboardData = e.clipboardData || window["clipboardData"]; - if (!clipboardData || BROKEN_SETDATA) + if (number < 0) { + this.numberOfExtraLines = 0; return; - // using "Text" doesn't work on old webkit but ie needs it - var mime = USE_IE_MIME_TYPE || forceIEMime ? "Text" : "text/plain"; - try { - if (data) { - // Safari 5 has clipboardData object, but does not handle setData() - return clipboardData.setData(mime, data) !== false; - } else { - return clipboardData.getData(mime); - } - } catch(e) { - if (!forceIEMime) - return handleClipboardData(e, data, true); } - }; - var doCopy = function(e, isCut) { - var data = host.getCopyText(); - if (!data) - return event.preventDefault(e); + this.numberOfExtraLines = number; + } - if (handleClipboardData(e, data)) { - if (isIOS) { - resetSelection(data); - copied = data; - setTimeout(function () { - copied = false; - }, 10); - } - isCut ? host.onCut() : host.onCopy(); - event.preventDefault(e); - } else { - copied = true; - text.value = data; - text.select(); - setTimeout(function(){ - copied = false; - resetSelection(); - isCut ? host.onCut() : host.onCopy(); - }); + + setAriaLabel() { + var ariaLabel = ""; + if (this.host.$textInputAriaLabel) { + ariaLabel += `${this.host.$textInputAriaLabel}, `; } - }; - - var onCut = function(e) { - doCopy(e, true); - }; - - var onCopy = function(e) { - doCopy(e, false); - }; - - var onPaste = function(e) { - var data = handleClipboardData(e); - if (clipboard.pasteCancelled()) - return; - if (typeof data == "string") { - if (data) - host.onPaste(data, e); - if (useragent.isIE) - setTimeout(resetSelection); - event.preventDefault(e); + if (this.host.session) { + var row = this.host.session.selection.cursor.row; + ariaLabel += nls("text-input.aria-label", "Cursor at row $0", [row + 1]); + } + this.text.setAttribute("aria-label", ariaLabel); + } + + /** + * @param {import("../../ace-internal").Ace.AriaOptions} options + */ + setAriaOptions(options) { + if (options.activeDescendant) { + this.text.setAttribute("aria-haspopup", "true"); + this.text.setAttribute("aria-autocomplete", options.inline ? "both" : "list"); + this.text.setAttribute("aria-activedescendant", options.activeDescendant); } else { - text.value = ""; - pasted = true; + this.text.setAttribute("aria-haspopup", "false"); + this.text.setAttribute("aria-autocomplete", "both"); + this.text.removeAttribute("aria-activedescendant"); } - }; - - event.addCommandKeyListener(text, function(e, hashId, keyCode) { - // ignore command events during composition as they will - // either be handled by ime itself or fired again after ime end - if (inComposition) return; - return host.onCommandKey(e, hashId, keyCode); - }, host); + if (options.role) { + this.text.setAttribute("role", options.role); + } + if (options.setLabel) { + this.text.setAttribute("aria-roledescription", nls("text-input.aria-roledescription", "editor")); + this.setAriaLabel(); + } + } - event.addListener(text, "select", onSelect, host); - event.addListener(text, "input", onInput, host); + focus() { + // On focusing on the textarea, read active row number to assistive tech. + this.setAriaOptions({ + setLabel: this.host.renderer.enableKeyboardAccessibility + }); - event.addListener(text, "cut", onCut, host); - event.addListener(text, "copy", onCopy, host); - event.addListener(text, "paste", onPaste, host); + if (this.tempStyle || HAS_FOCUS_ARGS || this.$focusScroll == "browser") return this.text.focus( + {preventScroll: true}); + var top = this.text.style.top; + this.text.style.position = "fixed"; + this.text.style.top = "0px"; + try { + var isTransformed = this.text.getBoundingClientRect().top != 0; + } catch (e) { + // getBoundingClientRect on IE throws error if element is not in the dom tree + return; + } + var ancestors = []; + if (isTransformed) { + var t = this.text.parentElement; + while (t && t.nodeType == 1) { + ancestors.push(t); + t.setAttribute("ace_nocontext", "true"); + if (!t.parentElement && t.getRootNode) t = t.getRootNode()["host"]; else t = t.parentElement; + } + } + this.text.focus({preventScroll: true}); + if (isTransformed) { + ancestors.forEach(function (p) { + p.removeAttribute("ace_nocontext"); + }); + } + setTimeout(() => { + this.text.style.position = ""; + if (this.text.style.top == "0px") this.text.style.top = top; + }, 0); + } - // Opera has no clipboard events - if (!('oncut' in text) || !('oncopy' in text) || !('onpaste' in text)) { - event.addListener(parentNode, "keydown", function(e) { - if ((useragent.isMac && !e.metaKey) || !e.ctrlKey) - return; + blur() { + this.text.blur(); + } - switch (e.keyCode) { - case 67: - onCopy(e); - break; - case 86: - onPaste(e); - break; - case 88: - onCut(e); - break; - } - }, host); + isFocused() { + return this.$isFocused; } + setInputHandler(cb) { + this.inputHandler = cb; + } - // COMPOSITION - var onCompositionStart = function(e) { - if (inComposition || !host.onCompositionStart || host.$readOnly) - return; - - inComposition = {}; + getInputHandler() { + return this.inputHandler; + } - if (commandMode) - return; - - if (e.data) - inComposition.useTextareaForIME = false; - - setTimeout(onCompositionUpdate, 0); - host._signal("compositionStart"); - host.on("mousedown", cancelComposition); - - var range = host.getSelectionRange(); - range.end.row = range.start.row; - range.end.column = range.start.column; - inComposition.markerRange = range; - inComposition.selectionStart = lastSelectionStart; - host.onCompositionStart(inComposition); - - if (inComposition.useTextareaForIME) { - lastValue = text.value = ""; - lastSelectionStart = 0; - lastSelectionEnd = 0; - } - else { - if (text.msGetInputContext) - inComposition.context = text.msGetInputContext(); - if (text.getInputContext) - inComposition.context = text.getInputContext(); - } - }; + getElement() { + return this.text; + } - var onCompositionUpdate = function() { - if (!inComposition || !host.onCompositionUpdate || host.$readOnly) - return; - if (commandMode) - return cancelComposition(); - - if (inComposition.useTextareaForIME) { - host.onCompositionUpdate(text.value); - } - else { - var data = text.value; - sendText(data); - if (inComposition.markerRange) { - if (inComposition.context) { - inComposition.markerRange.start.column = inComposition.selectionStart - = inComposition.context.compositionStartOffset; - } - inComposition.markerRange.end.column = inComposition.markerRange.start.column - + lastSelectionEnd - inComposition.selectionStart + lastRestoreEnd; - } - } - }; + /** + * allows to ignore composition (used by vim keyboard handler in the normal mode) + * this is useful on mac, where with some keyboard layouts (e.g swedish) ^ starts composition + * @param {boolean} value + */ + setCommandMode(value) { + this.commandMode = value; + this.text.readOnly = false; + } - var onCompositionEnd = function(e) { - if (!host.onCompositionEnd || host.$readOnly) return; - inComposition = false; - host.onCompositionEnd(); - host.off("mousedown", cancelComposition); - // note that resetting value of textarea at this point doesn't always work - // because textarea value can be silently restored - if (e) onInput(); - }; - + setReadOnly(readOnly) { + if (!this.commandMode) this.text.readOnly = readOnly; + } - function cancelComposition() { - // force end composition - ignoreFocusEvents = true; - text.blur(); - text.focus(); - ignoreFocusEvents = false; + setCopyWithEmptySelection(value) { } - var syncComposition = lang.delayedCall(onCompositionUpdate, 50).schedule.bind(null, null); - - function onKeyup(e) { - // workaround for a bug in ie where pressing esc silently moves selection out of textarea - if (e.keyCode == 27 && text.value.length < text.selectionStart) { - if (!inComposition) - lastValue = text.value; - lastSelectionStart = lastSelectionEnd = -1; - resetSelection(); - } - syncComposition(); - } - - event.addListener(text, "compositionstart", onCompositionStart, host); - event.addListener(text, "compositionupdate", onCompositionUpdate, host); - event.addListener(text, "keyup", onKeyup, host); - event.addListener(text, "keydown", syncComposition, host); - event.addListener(text, "compositionend", onCompositionEnd, host); - - this.getElement = function() { - return text; - }; - - // allows to ignore composition (used by vim keyboard handler in the normal mode) - // this is useful on mac, where with some keyboard layouts (e.g swedish) ^ starts composition - this.setCommandMode = function(value) { - commandMode = value; - text.readOnly = false; - }; - - this.setReadOnly = function(readOnly) { - if (!commandMode) - text.readOnly = readOnly; - }; - - this.setCopyWithEmptySelection = function(value) { - }; - - this.onContextMenu = function(e) { - afterContextMenu = true; - resetSelection(); - host._emit("nativecontextmenu", {target: host, domEvent: e}); + onContextMenu(e) { + this.afterContextMenu = true; + this.resetSelection(); + this.host._emit("nativecontextmenu", { + target: this.host, + domEvent: e + }); this.moveToMouse(e, true); - }; - - this.moveToMouse = function(e, bringToFront) { - if (!tempStyle) - tempStyle = text.style.cssText; - text.style.cssText = (bringToFront ? "z-index:100000;" : "") - + (useragent.isIE ? "opacity:0.1;" : "") - + "text-indent: -" + (lastSelectionStart + lastSelectionEnd) * host.renderer.characterWidth * 0.5 + "px;"; - - var rect = host.container.getBoundingClientRect(); - var style = dom.computedStyle(host.container); + } + + /** + * @param e + * @param {boolean} bringToFront + */ + moveToMouse(e, bringToFront) { + if (!this.tempStyle) this.tempStyle = this.text.style.cssText; + this.text.style.cssText = (bringToFront ? "z-index:100000;" : "") + (useragent.isIE ? "opacity:0.1;" : "") + + "text-indent: -" + (this.lastSelectionStart + this.lastSelectionEnd) * this.host.renderer.characterWidth + * 0.5 + "px;"; + + var rect = this.host.container.getBoundingClientRect(); + var style = dom.computedStyle(this.host.container); var top = rect.top + (parseInt(style.borderTopWidth) || 0); var left = rect.left + (parseInt(style.borderLeftWidth) || 0); - var maxTop = rect.bottom - top - text.clientHeight -2; - var move = function(e) { - dom.translate(text, e.clientX - left - 2, Math.min(e.clientY - top - 2, maxTop)); - }; + var maxTop = rect.bottom - top - this.text.clientHeight - 2; + var move = (e) => { + dom.translate(this.text, e.clientX - left - 2, Math.min(e.clientY - top - 2, maxTop)); + }; move(e); - if (e.type != "mousedown") - return; + if (e.type != "mousedown") return; - host.renderer.$isMousePressed = true; + this.host.renderer.$isMousePressed = true; - clearTimeout(closeTimeout); + clearTimeout(this.closeTimeout); // on windows context menu is opened after mouseup - if (useragent.isWin) - event.capture(host.container, move, onContextMenuClose); - }; - - this.onContextMenuClose = onContextMenuClose; - var closeTimeout; - function onContextMenuClose() { - clearTimeout(closeTimeout); - closeTimeout = setTimeout(function () { - if (tempStyle) { - text.style.cssText = tempStyle; - tempStyle = ''; - } - host.renderer.$isMousePressed = false; - if (host.renderer.$keepTextAreaAtCursor) - host.renderer.$moveTextAreaToCursor(); - }, 0); + if (useragent.isWin) event.capture(this.host.container, move, this.onContextMenuClose.bind(this)); } - var onContextMenu = function(e) { - host.textInput.onContextMenu(e); - onContextMenuClose(); - }; - event.addListener(text, "mouseup", onContextMenu, host); - event.addListener(text, "mousedown", function(e) { - e.preventDefault(); - onContextMenuClose(); - }, host); - event.addListener(host.renderer.scroller, "contextmenu", onContextMenu, host); - event.addListener(text, "contextmenu", onContextMenu, host); - - if (isIOS) - addIosSelectionHandler(parentNode, host, text); - - function addIosSelectionHandler(parentNode, host, text) { - var typingResetTimeout = null; - var typing = false; - - text.addEventListener("keydown", function (e) { - if (typingResetTimeout) clearTimeout(typingResetTimeout); - typing = true; - }, true); - - text.addEventListener("keyup", function (e) { - typingResetTimeout = setTimeout(function () { - typing = false; - }, 100); - }, true); - - // IOS doesn't fire events for arrow keys, but this unique hack changes everything! - var detectArrowKeys = function(e) { - if (document.activeElement !== text) return; - if (typing || inComposition || host.$mouseHandler.isMousePressed) return; - - if (copied) { - return; - } - var selectionStart = text.selectionStart; - var selectionEnd = text.selectionEnd; - - var key = null; - var modifier = 0; - // console.log(selectionStart, selectionEnd); - if (selectionStart == 0) { - key = KEYS.up; - } else if (selectionStart == 1) { - key = KEYS.home; - } else if (selectionEnd > lastSelectionEnd && lastValue[selectionEnd] == "\n") { - key = KEYS.end; - } else if (selectionStart < lastSelectionStart && lastValue[selectionStart - 1] == " ") { - key = KEYS.left; - modifier = MODS.option; - } else if ( - selectionStart < lastSelectionStart - || ( - selectionStart == lastSelectionStart - && lastSelectionEnd != lastSelectionStart - && selectionStart == selectionEnd - ) - ) { - key = KEYS.left; - } else if (selectionEnd > lastSelectionEnd && lastValue.slice(0, selectionEnd).split("\n").length > 2) { - key = KEYS.down; - } else if (selectionEnd > lastSelectionEnd && lastValue[selectionEnd - 1] == " ") { - key = KEYS.right; - modifier = MODS.option; - } else if ( - selectionEnd > lastSelectionEnd - || ( - selectionEnd == lastSelectionEnd - && lastSelectionEnd != lastSelectionStart - && selectionStart == selectionEnd - ) - ) { - key = KEYS.right; - } - - if (selectionStart !== selectionEnd) - modifier |= MODS.shift; - - if (key) { - var result = host.onCommandKey({}, modifier, key); - if (!result && host.commands) { - key = KEYS.keyCodeToString(key); - var command = host.commands.findKeyCommand(modifier, key); - if (command) - host.execCommand(command); - } - lastSelectionStart = selectionStart; - lastSelectionEnd = selectionEnd; - resetSelection(""); - } - }; - // On iOS, "selectionchange" can only be attached to the document object... - document.addEventListener("selectionchange", detectArrowKeys); - host.on("destroy", function() { - document.removeEventListener("selectionchange", detectArrowKeys); - }); + destroy() { + if (this.text.parentElement) this.text.parentElement.removeChild(this.text); } - - this.destroy = function() { - if (text.parentElement) - text.parentElement.removeChild(text); - }; -}; +} exports.TextInput = TextInput; -exports.$setUserAgentForTests = function(_isMobile, _isIOS) { +exports.$setUserAgentForTests = function (_isMobile, _isIOS) { isMobile = _isMobile; isIOS = _isIOS; }; diff --git a/types/ace-modules.d.ts b/types/ace-modules.d.ts index 5e5f10eab8b..b1e5fe7f41a 100644 --- a/types/ace-modules.d.ts +++ b/types/ace-modules.d.ts @@ -1560,7 +1560,76 @@ declare module "ace-code/src/clipboard" { } declare module "ace-code/src/keyboard/textinput" { export function $setUserAgentForTests(_isMobile: any, _isIOS: any): void; - export var TextInput: any; + export class TextInput { + constructor(parentNode: HTMLElement, host: import("ace-code/src/editor").Editor); + host: import("ace-code/src/editor").Editor; + text: HTMLTextAreaElement & { + msGetInputContext?: () => { + compositionStartOffset: number; + }; + getInputContext?: () => { + compositionStartOffset: number; + }; + }; + copied: boolean | string; + pasted: boolean; + inComposition: (boolean | any) & { + context?: any; + useTextareaForIME?: boolean; + selectionStart?: number; + markerRange?: any; + }; + sendingText: boolean; + tempStyle: string; + commandMode: boolean; + ignoreFocusEvents: boolean; + lastValue: string; + lastSelectionStart: number; + lastSelectionEnd: number; + lastRestoreEnd: number; + rowStart: number; + rowEnd: number; + numberOfExtraLines: number; + resetSelection: (value: any) => void; + inputHandler: any; + afterContextMenu: boolean; + syncComposition: any; + onContextMenuClose(): void; + closeTimeout: number; + setHost(newHost: import("ace-code/src/editor").Editor): void; + /** + * Sets the number of extra lines in the textarea to improve screen reader compatibility. + * Extra lines can help screen readers perform better when reading text. + * + * @param {number} number - The number of extra lines to add. Must be non-negative. + */ + setNumberOfExtraLines(number: number): void; + setAriaLabel(): void; + setAriaOptions(options: import("ace-code").Ace.AriaOptions): void; + focus(): void; + blur(): void; + isFocused(): boolean; + setInputHandler(cb: any): void; + getInputHandler(): any; + getElement(): HTMLTextAreaElement & { + msGetInputContext?: () => { + compositionStartOffset: number; + }; + getInputContext?: () => { + compositionStartOffset: number; + }; + }; + /** + * allows to ignore composition (used by vim keyboard handler in the normal mode) + * this is useful on mac, where with some keyboard layouts (e.g swedish) ^ starts composition + */ + setCommandMode(value: boolean): void; + setReadOnly(readOnly: any): void; + setCopyWithEmptySelection(value: any): void; + onContextMenu(e: any): void; + moveToMouse(e: any, bringToFront: boolean): void; + destroy(): void; + } } declare module "ace-code/src/mouse/mouse_event" { export class MouseEvent { @@ -2047,7 +2116,7 @@ declare module "ace-code/src/editor" { }; renderer: VirtualRenderer; commands: CommandManager; - textInput: any; + textInput: TextInput; keyBinding: KeyBinding; startOperation(commandEvent: any): void; /** @@ -2162,7 +2231,7 @@ declare module "ace-code/src/editor" { * Returns the string of text currently highlighted. **/ getCopyText(): string; - execCommand(command: string | string[], args?: any): boolean; + execCommand(command: string | string[] | import("ace-code").Ace.Command, args?: any): boolean; /** * Inserts `text` into wherever the cursor is pointing. * @param {String} text The new text to add @@ -2706,6 +2775,7 @@ declare module "ace-code/src/editor" { export type SearchOptions = import("ace-code").Ace.SearchOptions; import { EditSession } from "ace-code/src/edit_session"; import { CommandManager } from "ace-code/src/commands/command_manager"; + import { TextInput } from "ace-code/src/keyboard/textinput"; import { MouseHandler } from "ace-code/src/mouse/mouse_handler"; import { KeyBinding } from "ace-code/src/keyboard/keybinding"; import { Search } from "ace-code/src/search"; From 8c4ad0770314ad2676a41f9746f4db74eae66a71 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Thu, 22 May 2025 13:11:31 +0400 Subject: [PATCH 2/3] generate types after merge --- types/ace-ext.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/ace-ext.d.ts b/types/ace-ext.d.ts index c190183b2f1..ac6dd0aa740 100644 --- a/types/ace-ext.d.ts +++ b/types/ace-ext.d.ts @@ -645,7 +645,7 @@ declare module "ace-code/src/ext/whitespace" { export type EditSession = import("ace-code/src/edit_session").EditSession; } declare module "ace-code/src/ext/diff/styles-css" { - export const cssText: "\n/*\n * Line Markers\n */\n.ace_diff {\n position: absolute;\n z-index: 0;\n}\n.ace_diff.inline {\n z-index: 20;\n}\n/*\n * Light Colors \n */\n.ace_diff.insert {\n background-color: #eaffea; /*rgb(74 251 74 / 12%); */\n}\n.ace_diff.delete {\n background-color: #ffecec; /*rgb(251 74 74 / 12%);*/\n}\n.ace_diff.aligned_diff {\n background: rgba(206, 194, 191, 0.26);\n background: repeating-linear-gradient(\n 45deg,\n rgba(122, 111, 108, 0.26),\n rgba(122, 111, 108, 0.26) 5px,\n #FFFFFF 5px,\n #FFFFFF 10px \n );\n}\n\n.ace_diff.insert.inline {\n background-color: rgb(74 251 74 / 18%); \n}\n.ace_diff.delete.inline {\n background-color: rgb(251 74 74 / 15%);\n}\n\n.ace_diff.delete.inline.empty {\n background-color: rgba(255, 128, 79, 0.8);\n width: 2px !important;\n}\n\n.ace_diff.insert.inline.empty {\n background-color: rgba(49, 230, 96, 0.8);\n width: 2px !important;\n}\n\n.ace_diff.selection {\n border-bottom: 1px solid black;\n border-top: 1px solid black;\n background: transparent;\n}\n\n/*\n * Dark Colors \n */\n\n.ace_dark .ace_diff.insert.inline {\n background-color: rgba(0, 130, 58, 0.45);\n}\n.ace_dark .ace_diff.delete.inline {\n background-color: rgba(169, 46, 33, 0.55);\n}\n\n.ace_dark .ace_diff.selection {\n border-bottom: 1px solid white;\n border-top: 1px solid white;\n background: transparent;\n}\n \n\n/* gutter changes */\n.ace_mini-diff_gutter-enabled > .ace_gutter-cell {\n background-color: #f0f0f0;\n padding-right: 13px;\n}\n\n.ace_mini-diff_gutter-enabled > .mini-diff-added {\n background-color: #eaffea;\n border-left: 3px solid #00FF00;\n padding-left: 0;\n}\n\n.ace_mini-diff_gutter-enabled > .mini-diff-deleted {\n background-color: #ffecec;\n border-left: 3px solid #FF0000;\n padding-left: 0;\n}\n\n\n.ace_mini-diff_gutter-enabled > .mini-diff-added:after {\n position: absolute;\n right: 2px;\n content: \"+\";\n color: darkgray;\n background-color: inherit;\n}\n\n.ace_mini-diff_gutter-enabled > .mini-diff-deleted:after {\n position: absolute;\n right: 2px;\n content: \"-\";\n color: darkgray;\n background-color: inherit;\n}\n.ace_fade-fold-widgets:hover .mini-diff-added:after {\n display: none;\n}\n.ace_fade-fold-widgets:hover .mini-diff-deleted:after {\n display: none;\n}\n\n.ace_diff_other .ace_selection {\n filter: drop-shadow(1px 2px 3px darkgray);\n}\n\n"; + export const cssText: "\n/*\n * Line Markers\n */\n.ace_diff {\n position: absolute;\n z-index: 0;\n}\n.ace_diff.inline {\n z-index: 20;\n}\n/*\n * Light Colors \n */\n.ace_diff.insert {\n background-color: #eaffea; /*rgb(74 251 74 / 12%); */\n}\n.ace_diff.delete {\n background-color: #ffecec; /*rgb(251 74 74 / 12%);*/\n}\n.ace_diff.aligned_diff {\n background: rgba(206, 194, 191, 0.26);\n background: repeating-linear-gradient(\n 45deg,\n rgba(122, 111, 108, 0.26),\n rgba(122, 111, 108, 0.26) 5px,\n #FFFFFF 5px,\n #FFFFFF 10px \n );\n}\n\n.ace_diff.insert.inline {\n background-color: rgb(74 251 74 / 18%); \n}\n.ace_diff.delete.inline {\n background-color: rgb(251 74 74 / 15%);\n}\n\n.ace_diff.delete.inline.empty {\n background-color: rgba(255, 128, 79, 0.8);\n width: 2px !important;\n}\n\n.ace_diff.insert.inline.empty {\n background-color: rgba(49, 230, 96, 0.8);\n width: 2px !important;\n}\n\n.ace_diff.selection {\n border-bottom: 1px solid black;\n border-top: 1px solid black;\n background: transparent;\n}\n\n/*\n * Dark Colors \n */\n\n.ace_dark .ace_diff.insert.inline {\n background-color: rgba(0, 130, 58, 0.45);\n}\n.ace_dark .ace_diff.delete.inline {\n background-color: rgba(169, 46, 33, 0.55);\n}\n\n.ace_dark .ace_diff.selection {\n border-bottom: 1px solid white;\n border-top: 1px solid white;\n background: transparent;\n}\n \n\n/* gutter changes */\n.ace_mini-diff_gutter-enabled > .ace_gutter-cell,\n.ace_mini-diff_gutter-enabled > .ace_gutter-cell_svg-icons {\n padding-right: 13px;\n}\n\n.ace_mini-diff_gutter_other > .ace_gutter-cell,\n.ace_mini-diff_gutter_other > .ace_gutter-cell_svg-icons {\n display: none;\n}\n\n.ace_mini-diff_gutter_other {\n pointer-events: none;\n}\n\n\n.ace_mini-diff_gutter-enabled > .mini-diff-added {\n background-color: #eaffea;\n border-left: 3px solid #00FF00;\n padding-left: 16px;\n display: block;\n}\n\n.ace_mini-diff_gutter-enabled > .mini-diff-deleted {\n background-color: #ffecec;\n border-left: 3px solid #FF0000;\n padding-left: 16px;\n display: block;\n}\n\n\n.ace_mini-diff_gutter-enabled > .mini-diff-added:after {\n position: absolute;\n right: 2px;\n content: \"+\";\n color: darkgray;\n background-color: inherit;\n}\n\n.ace_mini-diff_gutter-enabled > .mini-diff-deleted:after {\n position: absolute;\n right: 2px;\n content: \"-\";\n color: darkgray;\n background-color: inherit;\n}\n.ace_fade-fold-widgets:hover .mini-diff-added:after {\n display: none;\n}\n.ace_fade-fold-widgets:hover .mini-diff-deleted:after {\n display: none;\n}\n\n.ace_diff_other .ace_selection {\n filter: drop-shadow(1px 2px 3px darkgray);\n}\n\n"; } declare module "ace-code/src/ext/diff/gutter_decorator" { export class MinimalGutterDiffDecorator { From 7799ef066c34348e59b632bc50af3ab219c8e11d Mon Sep 17 00:00:00 2001 From: mkslanc Date: Mon, 26 May 2025 12:47:10 +0400 Subject: [PATCH 3/3] rename AriaOptions to TextInputAriaOptions for clarity --- ace-internal.d.ts | 2 +- ace.d.ts | 2 +- src/keyboard/textinput.js | 2 +- types/ace-modules.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ace-internal.d.ts b/ace-internal.d.ts index f4bd122c1f8..f5432c940c2 100644 --- a/ace-internal.d.ts +++ b/ace-internal.d.ts @@ -1288,7 +1288,7 @@ export namespace Ace { "scroll": (e: { data: number }) => void; } - export interface AriaOptions { + export interface TextInputAriaOptions { activeDescendant?: string; role?: string; setLabel?: boolean; diff --git a/ace.d.ts b/ace.d.ts index 4d8379ef290..3930acc4dd3 100644 --- a/ace.d.ts +++ b/ace.d.ts @@ -1025,7 +1025,7 @@ declare module "ace-code" { data: number; }) => void; } - export interface AriaOptions { + export interface TextInputAriaOptions { activeDescendant?: string; role?: string; setLabel?: boolean; diff --git a/src/keyboard/textinput.js b/src/keyboard/textinput.js index 2d1ee5c7b34..7de343a520d 100644 --- a/src/keyboard/textinput.js +++ b/src/keyboard/textinput.js @@ -752,7 +752,7 @@ class TextInput { } /** - * @param {import("../../ace-internal").Ace.AriaOptions} options + * @param {import("../../ace-internal").Ace.TextInputAriaOptions} options */ setAriaOptions(options) { if (options.activeDescendant) { diff --git a/types/ace-modules.d.ts b/types/ace-modules.d.ts index b1e5fe7f41a..9b34e9fa255 100644 --- a/types/ace-modules.d.ts +++ b/types/ace-modules.d.ts @@ -1605,7 +1605,7 @@ declare module "ace-code/src/keyboard/textinput" { */ setNumberOfExtraLines(number: number): void; setAriaLabel(): void; - setAriaOptions(options: import("ace-code").Ace.AriaOptions): void; + setAriaOptions(options: import("ace-code").Ace.TextInputAriaOptions): void; focus(): void; blur(): void; isFocused(): boolean;