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