X-Git-Url: https://git.josue.xyz/?p=VSoRC%2F.git;a=blobdiff_plain;f=node_modules%2Fxterm%2Fsrc%2Fbrowser%2Finput%2FCompositionHelper.ts;fp=node_modules%2Fxterm%2Fsrc%2Fbrowser%2Finput%2FCompositionHelper.ts;h=e6994ea3b422e8086ca544f501323691ce90d0e0;hp=0000000000000000000000000000000000000000;hb=4339da12467b75fb8b6ca831f4bf0081c485ed2c;hpb=af450fde25a9ccf4767b29254c463ffb8ef25237 diff --git a/node_modules/xterm/src/browser/input/CompositionHelper.ts b/node_modules/xterm/src/browser/input/CompositionHelper.ts new file mode 100644 index 0000000..e6994ea --- /dev/null +++ b/node_modules/xterm/src/browser/input/CompositionHelper.ts @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharSizeService } from 'browser/services/Services'; +import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services'; + +interface IPosition { + start: number; + end: number; +} + +/** + * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend + * events, displaying the in-progress composition to the UI and forwarding the final composition + * to the handler. + */ +export class CompositionHelper { + /** + * Whether input composition is currently happening, eg. via a mobile keyboard, speech input or + * IME. This variable determines whether the compositionText should be displayed on the UI. + */ + private _isComposing: boolean; + + /** + * The position within the input textarea's value of the current composition. + */ + private _compositionPosition: IPosition; + + /** + * Whether a composition is in the process of being sent, setting this to false will cancel any + * in-progress composition. + */ + private _isSendingComposition: boolean; + + constructor( + private readonly _textarea: HTMLTextAreaElement, + private readonly _compositionView: HTMLElement, + @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @ICoreService private readonly _coreService: ICoreService + ) { + this._isComposing = false; + this._isSendingComposition = false; + this._compositionPosition = { start: 0, end: 0 }; + } + + /** + * Handles the compositionstart event, activating the composition view. + */ + public compositionstart(): void { + this._isComposing = true; + this._compositionPosition.start = this._textarea.value.length; + this._compositionView.textContent = ''; + this._compositionView.classList.add('active'); + } + + /** + * Handles the compositionupdate event, updating the composition view. + * @param ev The event. + */ + public compositionupdate(ev: CompositionEvent): void { + this._compositionView.textContent = ev.data; + this.updateCompositionElements(); + setTimeout(() => { + this._compositionPosition.end = this._textarea.value.length; + }, 0); + } + + /** + * Handles the compositionend event, hiding the composition view and sending the composition to + * the handler. + */ + public compositionend(): void { + this._finalizeComposition(true); + } + + /** + * Handles the keydown event, routing any necessary events to the CompositionHelper functions. + * @param ev The keydown event. + * @return Whether the Terminal should continue processing the keydown event. + */ + public keydown(ev: KeyboardEvent): boolean { + if (this._isComposing || this._isSendingComposition) { + if (ev.keyCode === 229) { + // Continue composing if the keyCode is the "composition character" + return false; + } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { + // Continue composing if the keyCode is a modifier key + return false; + } + // Finish composition immediately. This is mainly here for the case where enter is + // pressed and the handler needs to be triggered before the command is executed. + this._finalizeComposition(false); + } + + if (ev.keyCode === 229) { + // If the "composition character" is used but gets to this point it means a non-composition + // character (eg. numbers and punctuation) was pressed when the IME was active. + this._handleAnyTextareaChanges(); + return false; + } + + return true; + } + + /** + * Finalizes the composition, resuming regular input actions. This is called when a composition + * is ending. + * @param waitForPropagation Whether to wait for events to propagate before sending + * the input. This should be false if a non-composition keystroke is entered before the + * compositionend event is triggered, such as enter, so that the composition is sent before + * the command is executed. + */ + private _finalizeComposition(waitForPropagation: boolean): void { + this._compositionView.classList.remove('active'); + this._isComposing = false; + this._clearTextareaPosition(); + + if (!waitForPropagation) { + // Cancel any delayed composition send requests and send the input immediately. + this._isSendingComposition = false; + const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end); + this._coreService.triggerDataEvent(input, true); + } else { + // Make a deep copy of the composition position here as a new compositionstart event may + // fire before the setTimeout executes. + const currentCompositionPosition = { + start: this._compositionPosition.start, + end: this._compositionPosition.end + }; + + // Since composition* events happen before the changes take place in the textarea on most + // browsers, use a setTimeout with 0ms time to allow the native compositionend event to + // complete. This ensures the correct character is retrieved. + // This solution was used because: + // - The compositionend event's data property is unreliable, at least on Chromium + // - The last compositionupdate event's data property does not always accurately describe + // the character, a counter example being Korean where an ending consonsant can move to + // the following character if the following input is a vowel. + this._isSendingComposition = true; + setTimeout(() => { + // Ensure that the input has not already been sent + if (this._isSendingComposition) { + this._isSendingComposition = false; + let input; + if (this._isComposing) { + // Use the end position to get the string if a new composition has started. + input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + } else { + // Don't use the end position here in order to pick up any characters after the + // composition has finished, for example when typing a non-composition character + // (eg. 2) after a composition character. + input = this._textarea.value.substring(currentCompositionPosition.start); + } + this._coreService.triggerDataEvent(input, true); + } + }, 0); + } + } + + /** + * Apply any changes made to the textarea after the current event chain is allowed to complete. + * This should be called when not currently composing but a keydown event with the "composition + * character" (229) is triggered, in order to allow non-composition text to be entered when an + * IME is active. + */ + private _handleAnyTextareaChanges(): void { + const oldValue = this._textarea.value; + setTimeout(() => { + // Ignore if a composition has started since the timeout + if (!this._isComposing) { + const newValue = this._textarea.value; + const diff = newValue.replace(oldValue, ''); + if (diff.length > 0) { + this._coreService.triggerDataEvent(diff, true); + } + } + }, 0); + } + + /** + * Positions the composition view on top of the cursor and the textarea just below it (so the + * IME helper dialog is positioned correctly). + * @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is + * necessary as the IME events across browsers are not consistently triggered. + */ + public updateCompositionElements(dontRecurse?: boolean): void { + if (!this._isComposing) { + return; + } + + if (this._bufferService.buffer.isCursorInViewport) { + const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight); + const cursorTop = this._bufferService.buffer.y * cellHeight; + const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width; + + this._compositionView.style.left = cursorLeft + 'px'; + this._compositionView.style.top = cursorTop + 'px'; + this._compositionView.style.height = cellHeight + 'px'; + this._compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.fontFamily = this._optionsService.options.fontFamily; + this._compositionView.style.fontSize = this._optionsService.options.fontSize + 'px'; + // Sync the textarea to the exact position of the composition view so the IME knows where the + // text is. + const compositionViewBounds = this._compositionView.getBoundingClientRect(); + this._textarea.style.left = cursorLeft + 'px'; + this._textarea.style.top = cursorTop + 'px'; + this._textarea.style.width = compositionViewBounds.width + 'px'; + this._textarea.style.height = compositionViewBounds.height + 'px'; + this._textarea.style.lineHeight = compositionViewBounds.height + 'px'; + } + + if (!dontRecurse) { + setTimeout(() => this.updateCompositionElements(true), 0); + } + } + + /** + * Clears the textarea's position so that the cursor does not blink on IE. + * @private + */ + private _clearTextareaPosition(): void { + this._textarea.style.left = ''; + this._textarea.style.top = ''; + } +}