2 * xterm.js: xterm, in the browser
3 * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
7 * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend
8 * events, displaying the in-progress composition to the UI and forwarding the final composition
10 * @param {HTMLTextAreaElement} textarea The textarea that xterm uses for input.
11 * @param {HTMLElement} compositionView The element to display the in-progress composition in.
12 * @param {Terminal} terminal The Terminal to forward the finished composition to.
14 function CompositionHelper(textarea
, compositionView
, terminal
) {
15 this.textarea
= textarea
;
16 this.compositionView
= compositionView
;
17 this.terminal
= terminal
;
19 // Whether input composition is currently happening, eg. via a mobile keyboard, speech input
20 // or IME. This variable determines whether the compositionText should be displayed on the UI.
21 this.isComposing
= false;
23 // The input currently being composed, eg. via a mobile keyboard, speech input or IME.
24 this.compositionText
= null;
26 // The position within the input textarea's value of the current composition.
27 this.compositionPosition
= { start
: null, end
: null };
29 // Whether a composition is in the process of being sent, setting this to false will cancel
30 // any in-progress composition.
31 this.isSendingComposition
= false;
35 * Handles the compositionstart event, activating the composition view.
37 CompositionHelper
.prototype.compositionstart = function() {
38 this.isComposing
= true;
39 this.compositionPosition
.start
= this.textarea
.value
.length
;
40 this.compositionView
.textContent
= '';
41 this.compositionView
.classList
.add('active');
45 * Handles the compositionupdate event, updating the composition view.
46 * @param {CompositionEvent} ev The event.
48 CompositionHelper
.prototype.compositionupdate = function(ev
) {
49 this.compositionView
.textContent
= ev
.data
;
50 this.updateCompositionElements();
52 setTimeout(function() {
53 self
.compositionPosition
.end
= self
.textarea
.value
.length
;
58 * Handles the compositionend event, hiding the composition view and sending the composition to
61 CompositionHelper
.prototype.compositionend = function() {
62 this.finalizeComposition(true);
66 * Handles the keydown event, routing any necessary events to the CompositionHelper functions.
67 * @return Whether the Terminal should continue processing the keydown event.
69 CompositionHelper
.prototype.keydown = function(ev
) {
70 if (this.isComposing
|| this.isSendingComposition
) {
71 if (ev
.keyCode
=== 229) {
72 // Continue composing if the keyCode is the "composition character"
74 } else if (ev
.keyCode
=== 16 || ev
.keyCode
=== 17 || ev
.keyCode
=== 18) {
75 // Continue composing if the keyCode is a modifier key
78 // Finish composition immediately. This is mainly here for the case where enter is
79 // pressed and the handler needs to be triggered before the command is executed.
80 this.finalizeComposition(false);
84 if (ev
.keyCode
=== 229) {
85 // If the "composition character" is used but gets to this point it means a non-composition
86 // character (eg. numbers and punctuation) was pressed when the IME was active.
87 this.handleAnyTextareaChanges();
95 * Finalizes the composition, resuming regular input actions. This is called when a composition
97 * @param {boolean} waitForPropogation Whether to wait for events to propogate before sending
98 * the input. This should be false if a non-composition keystroke is entered before the
99 * compositionend event is triggered, such as enter, so that the composition is send before
100 * the command is executed.
102 CompositionHelper
.prototype.finalizeComposition = function(waitForPropogation
) {
103 this.compositionView
.classList
.remove('active');
104 this.isComposing
= false;
105 this.clearTextareaPosition();
107 if (!waitForPropogation
) {
108 // Cancel any delayed composition send requests and send the input immediately.
109 this.isSendingComposition
= false;
110 var input
= this.textarea
.value
.substring(this.compositionPosition
.start
, this.compositionPosition
.end
);
111 this.terminal
.handler(input
);
113 // Make a deep copy of the composition position here as a new compositionstart event may
114 // fire before the setTimeout executes.
115 var currentCompositionPosition
= {
116 start
: this.compositionPosition
.start
,
117 end
: this.compositionPosition
.end
,
120 // Since composition* events happen before the changes take place in the textarea on most
121 // browsers, use a setTimeout with 0ms time to allow the native compositionend event to
122 // complete. This ensures the correct character is retrieved, this solution was used
124 // - The compositionend event's data property is unreliable, at least on Chromium
125 // - The last compositionupdate event's data property does not always accurately describe
126 // the character, a counter example being Korean where an ending consonsant can move to
127 // the following character if the following input is a vowel.
129 this.isSendingComposition
= true;
130 setTimeout(function () {
131 // Ensure that the input has not already been sent
132 if (self
.isSendingComposition
) {
133 self
.isSendingComposition
= false;
135 if (self
.isComposing
) {
136 // Use the end position to get the string if a new composition has started.
137 input
= self
.textarea
.value
.substring(currentCompositionPosition
.start
, currentCompositionPosition
.end
);
139 // Don't use the end position here in order to pick up any characters after the
140 // composition has finished, for example when typing a non-composition character
141 // (eg. 2) after a composition character.
142 input
= self
.textarea
.value
.substring(currentCompositionPosition
.start
);
144 self
.terminal
.handler(input
);
151 * Apply any changes made to the textarea after the current event chain is allowed to complete.
152 * This should be called when not currently composing but a keydown event with the "composition
153 * character" (229) is triggered, in order to allow non-composition text to be entered when an
156 CompositionHelper
.prototype.handleAnyTextareaChanges = function() {
157 var oldValue
= this.textarea
.value
;
159 setTimeout(function() {
160 // Ignore if a composition has started since the timeout
161 if (!self
.isComposing
) {
162 var newValue
= self
.textarea
.value
;
163 var diff
= newValue
.replace(oldValue
, '');
164 if (diff
.length
> 0) {
165 self
.terminal
.handler(diff
);
172 * Positions the composition view on top of the cursor and the textarea just below it (so the
173 * IME helper dialog is positioned correctly).
175 CompositionHelper
.prototype.updateCompositionElements = function(dontRecurse
) {
176 if (!this.isComposing
) {
179 var cursor
= this.terminal
.element
.querySelector('.terminal-cursor');
181 this.compositionView
.style
.left
= cursor
.offsetLeft
+ 'px';
182 this.compositionView
.style
.top
= cursor
.offsetTop
+ 'px';
183 var compositionViewBounds
= this.compositionView
.getBoundingClientRect();
184 this.textarea
.style
.left
= cursor
.offsetLeft
+ compositionViewBounds
.width
+ 'px';
185 this.textarea
.style
.top
= (cursor
.offsetTop
+ cursor
.offsetHeight
) + 'px';
188 setTimeout(this.updateCompositionElements
.bind(this, true), 0);
193 * Clears the textarea's position so that the cursor does not blink on IE.
196 CompositionHelper
.prototype.clearTextareaPosition = function() {
197 this.textarea
.style
.left
= '';
198 this.textarea
.style
.top
= '';
201 export { CompositionHelper
};