]> git.proxmox.com Git - mirror_xterm.js.git/blame - src/CompositionHelper.ts
tsify CompositionHelper test
[mirror_xterm.js.git] / src / CompositionHelper.ts
CommitLineData
81fccee9
DI
1/**
2 * xterm.js: xterm, in the browser
a7acee72 3 * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
81fccee9
DI
4 */
5
30fcdd6c 6import { ITerminal } from './Interfaces';
28c3a202 7
30fcdd6c
DI
8interface IPosition {
9 start: number;
10 end: number;
11}
28c3a202 12
30fcdd6c
DI
13export class CompositionHelper {
14 /**
15 * Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
16 * IME. This variable determines whether the compositionText should be displayed on the UI.
17 */
18 private isComposing: boolean;
19
20 /**
21 * The position within the input textarea's value of the current composition.
22 */
23 private compositionPosition: IPosition;;
24
25 /**
26 * Whether a composition is in the process of being sent, setting this to false will cancel any
27 * in-progress composition.
28 */
29 private isSendingComposition: boolean;
30
31 /**
32 * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend
33 * events, displaying the in-progress composition to the UI and forwarding the final composition
34 * to the handler.
35 * @param textarea The textarea that xterm uses for input.
36 * @param compositionView The element to display the in-progress composition in.
37 * @param terminal The Terminal to forward the finished composition to.
38 */
39 constructor(
40 private textarea: HTMLTextAreaElement,
41 private compositionView: HTMLElement,
42 private terminal: ITerminal
43 ) {
44 this.isComposing = false;
45 this.isSendingComposition = false;
46 this.compositionPosition = { start: null, end: null };
47 }
28c3a202 48
30fcdd6c
DI
49 /**
50 * Handles the compositionstart event, activating the composition view.
51 */
52 public compositionstart() {
53 this.isComposing = true;
54 this.compositionPosition.start = this.textarea.value.length;
55 this.compositionView.textContent = '';
56 this.compositionView.classList.add('active');
57 }
28c3a202 58
30fcdd6c
DI
59 /**
60 * Handles the compositionupdate event, updating the composition view.
61 * @param {CompositionEvent} ev The event.
62 */
63 public compositionupdate(ev: CompositionEvent) {
64 this.compositionView.textContent = ev.data;
65 this.updateCompositionElements();
66 var self = this;
67 setTimeout(function() {
68 self.compositionPosition.end = self.textarea.value.length;
69 }, 0);
70 }
28c3a202 71
30fcdd6c
DI
72 /**
73 * Handles the compositionend event, hiding the composition view and sending the composition to
74 * the handler.
75 */
76 public compositionend() {
77 this.finalizeComposition(true);
78 }
28c3a202 79
30fcdd6c
DI
80 /**
81 * Handles the keydown event, routing any necessary events to the CompositionHelper functions.
82 * @param ev The keydown event.
83 * @return Whether the Terminal should continue processing the keydown event.
84 */
85 public keydown(ev: KeyboardEvent) {
86 if (this.isComposing || this.isSendingComposition) {
87 if (ev.keyCode === 229) {
88 // Continue composing if the keyCode is the "composition character"
89 return false;
90 } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
91 // Continue composing if the keyCode is a modifier key
92 return false;
93 } else {
94 // Finish composition immediately. This is mainly here for the case where enter is
95 // pressed and the handler needs to be triggered before the command is executed.
96 this.finalizeComposition(false);
97 }
98 }
28c3a202 99
28c3a202 100 if (ev.keyCode === 229) {
30fcdd6c
DI
101 // If the "composition character" is used but gets to this point it means a non-composition
102 // character (eg. numbers and punctuation) was pressed when the IME was active.
103 this.handleAnyTextareaChanges();
28c3a202 104 return false;
28c3a202 105 }
28c3a202 106
30fcdd6c 107 return true;
28c3a202
DI
108 }
109
30fcdd6c
DI
110 /**
111 * Finalizes the composition, resuming regular input actions. This is called when a composition
112 * is ending.
113 * @param waitForPropogation Whether to wait for events to propogate before sending
114 * the input. This should be false if a non-composition keystroke is entered before the
115 * compositionend event is triggered, such as enter, so that the composition is send before
116 * the command is executed.
117 */
118 private finalizeComposition(waitForPropogation: boolean) {
119 this.compositionView.classList.remove('active');
120 this.isComposing = false;
121 this.clearTextareaPosition();
122
123 if (!waitForPropogation) {
124 // Cancel any delayed composition send requests and send the input immediately.
125 this.isSendingComposition = false;
126 var input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
127 this.terminal.handler(input);
128 } else {
129 // Make a deep copy of the composition position here as a new compositionstart event may
130 // fire before the setTimeout executes.
131 var currentCompositionPosition = {
132 start: this.compositionPosition.start,
133 end: this.compositionPosition.end,
134 }
28c3a202 135
30fcdd6c
DI
136 // Since composition* events happen before the changes take place in the textarea on most
137 // browsers, use a setTimeout with 0ms time to allow the native compositionend event to
138 // complete. This ensures the correct character is retrieved, this solution was used
139 // because:
140 // - The compositionend event's data property is unreliable, at least on Chromium
141 // - The last compositionupdate event's data property does not always accurately describe
142 // the character, a counter example being Korean where an ending consonsant can move to
143 // the following character if the following input is a vowel.
144 var self = this;
145 this.isSendingComposition = true;
146 setTimeout(function () {
147 // Ensure that the input has not already been sent
148 if (self.isSendingComposition) {
149 self.isSendingComposition = false;
150 var input;
151 if (self.isComposing) {
152 // Use the end position to get the string if a new composition has started.
153 input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
154 } else {
155 // Don't use the end position here in order to pick up any characters after the
156 // composition has finished, for example when typing a non-composition character
157 // (eg. 2) after a composition character.
158 input = self.textarea.value.substring(currentCompositionPosition.start);
159 }
160 self.terminal.handler(input);
161 }
162 }, 0);
28c3a202 163 }
30fcdd6c 164 }
28c3a202 165
30fcdd6c
DI
166 /**
167 * Apply any changes made to the textarea after the current event chain is allowed to complete.
168 * This should be called when not currently composing but a keydown event with the "composition
169 * character" (229) is triggered, in order to allow non-composition text to be entered when an
170 * IME is active.
171 */
172 private handleAnyTextareaChanges() {
173 var oldValue = this.textarea.value;
28c3a202 174 var self = this;
30fcdd6c
DI
175 setTimeout(function() {
176 // Ignore if a composition has started since the timeout
177 if (!self.isComposing) {
178 var newValue = self.textarea.value;
179 var diff = newValue.replace(oldValue, '');
180 if (diff.length > 0) {
181 self.terminal.handler(diff);
28c3a202 182 }
28c3a202
DI
183 }
184 }, 0);
185 }
28c3a202 186
30fcdd6c
DI
187 /**
188 * Positions the composition view on top of the cursor and the textarea just below it (so the
189 * IME helper dialog is positioned correctly).
190 * @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is
191 * necessary as the IME events across browsers are not consistently triggered.
192 */
193 public updateCompositionElements(dontRecurse?: boolean) {
194 if (!this.isComposing) {
195 return;
28c3a202 196 }
30fcdd6c
DI
197 var cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
198 if (cursor) {
199 // Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
200 // the .xterm element.
201 var xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
202 var cursorTop = xtermRows.offsetTop + cursor.offsetTop;
203
204 this.compositionView.style.left = cursor.offsetLeft + 'px';
205 this.compositionView.style.top = cursorTop + 'px';
206 this.compositionView.style.height = cursor.offsetHeight + 'px';
207 this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
208 // Sync the textarea to the exact position of the composition view so the IME knows where the
209 // text is.
210 var compositionViewBounds = this.compositionView.getBoundingClientRect();
211 this.textarea.style.left = cursor.offsetLeft + 'px';
212 this.textarea.style.top = cursorTop + 'px';
213 this.textarea.style.width = compositionViewBounds.width + 'px';
214 this.textarea.style.height = compositionViewBounds.height + 'px';
215 this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
216 }
217 if (!dontRecurse) {
218 setTimeout(this.updateCompositionElements.bind(this, true), 0);
219 }
220 };
221
222 /**
223 * Clears the textarea's position so that the cursor does not blink on IE.
224 * @private
225 */
226 private clearTextareaPosition() {
227 this.textarea.style.left = '';
228 this.textarea.style.top = '';
229 };
230}