OLD | NEW |
(Empty) | |
| 1 /** |
| 2 * xterm.js: xterm, in the browser |
| 3 * Copyright (c) 2016, SourceLair Limited <www.sourcelair.com> (MIT License) |
| 4 */ |
| 5 |
| 6 /** |
| 7 * Encapsulates the logic for handling compositionstart, compositionupdate and c
ompositionend |
| 8 * events, displaying the in-progress composition to the UI and forwarding the f
inal composition |
| 9 * to the handler. |
| 10 * @param {HTMLTextAreaElement} textarea The textarea that xterm uses for input. |
| 11 * @param {HTMLElement} compositionView The element to display the in-progress c
omposition in. |
| 12 * @param {Terminal} terminal The Terminal to forward the finished composition t
o. |
| 13 */ |
| 14 function CompositionHelper(textarea, compositionView, terminal) { |
| 15 this.textarea = textarea; |
| 16 this.compositionView = compositionView; |
| 17 this.terminal = terminal; |
| 18 |
| 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 disp
layed on the UI. |
| 21 this.isComposing = false; |
| 22 |
| 23 // The input currently being composed, eg. via a mobile keyboard, speech input
or IME. |
| 24 this.compositionText = null; |
| 25 |
| 26 // The position within the input textarea's value of the current composition. |
| 27 this.compositionPosition = { start: null, end: null }; |
| 28 |
| 29 // Whether a composition is in the process of being sent, setting this to fals
e will cancel |
| 30 // any in-progress composition. |
| 31 this.isSendingComposition = false; |
| 32 } |
| 33 |
| 34 /** |
| 35 * Handles the compositionstart event, activating the composition view. |
| 36 */ |
| 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'); |
| 42 }; |
| 43 |
| 44 /** |
| 45 * Handles the compositionupdate event, updating the composition view. |
| 46 * @param {CompositionEvent} ev The event. |
| 47 */ |
| 48 CompositionHelper.prototype.compositionupdate = function(ev) { |
| 49 this.compositionView.textContent = ev.data; |
| 50 this.updateCompositionElements(); |
| 51 var self = this; |
| 52 setTimeout(function() { |
| 53 self.compositionPosition.end = self.textarea.value.length; |
| 54 }, 0); |
| 55 }; |
| 56 |
| 57 /** |
| 58 * Handles the compositionend event, hiding the composition view and sending the
composition to |
| 59 * the handler. |
| 60 */ |
| 61 CompositionHelper.prototype.compositionend = function() { |
| 62 this.finalizeComposition(true); |
| 63 }; |
| 64 |
| 65 /** |
| 66 * Handles the keydown event, routing any necessary events to the CompositionHel
per functions. |
| 67 * @return Whether the Terminal should continue processing the keydown event. |
| 68 */ |
| 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" |
| 73 return false; |
| 74 } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { |
| 75 // Continue composing if the keyCode is a modifier key |
| 76 return false; |
| 77 } else { |
| 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 exe
cuted. |
| 80 this.finalizeComposition(false); |
| 81 } |
| 82 } |
| 83 |
| 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 acti
ve. |
| 87 this.handleAnyTextareaChanges(); |
| 88 return false; |
| 89 } |
| 90 |
| 91 return true; |
| 92 }; |
| 93 |
| 94 /** |
| 95 * Finalizes the composition, resuming regular input actions. This is called whe
n a composition |
| 96 * is ending. |
| 97 * @param {boolean} waitForPropogation Whether to wait for events to propogate b
efore sending |
| 98 * the input. This should be false if a non-composition keystroke is entered b
efore the |
| 99 * compositionend event is triggered, such as enter, so that the composition i
s send before |
| 100 * the command is executed. |
| 101 */ |
| 102 CompositionHelper.prototype.finalizeComposition = function(waitForPropogation) { |
| 103 this.compositionView.classList.remove('active'); |
| 104 this.isComposing = false; |
| 105 this.clearTextareaPosition(); |
| 106 |
| 107 if (!waitForPropogation) { |
| 108 // Cancel any delayed composition send requests and send the input immediate
ly. |
| 109 this.isSendingComposition = false; |
| 110 var input = this.textarea.value.substring(this.compositionPosition.start, th
is.compositionPosition.end); |
| 111 this.terminal.handler(input); |
| 112 } else { |
| 113 // Make a deep copy of the composition position here as a new compositionsta
rt event may |
| 114 // fire before the setTimeout executes. |
| 115 var currentCompositionPosition = { |
| 116 start: this.compositionPosition.start, |
| 117 end: this.compositionPosition.end, |
| 118 } |
| 119 |
| 120 // Since composition* events happen before the changes take place in the tex
tarea on most |
| 121 // browsers, use a setTimeout with 0ms time to allow the native compositione
nd event to |
| 122 // complete. This ensures the correct character is retrieved, this solution
was used |
| 123 // because: |
| 124 // - The compositionend event's data property is unreliable, at least on Chr
omium |
| 125 // - The last compositionupdate event's data property does not always accura
tely describe |
| 126 // the character, a counter example being Korean where an ending consonsan
t can move to |
| 127 // the following character if the following input is a vowel. |
| 128 var self = this; |
| 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; |
| 134 var input; |
| 135 if (self.isComposing) { |
| 136 // Use the end position to get the string if a new composition has sta
rted. |
| 137 input = self.textarea.value.substring(currentCompositionPosition.start
, currentCompositionPosition.end); |
| 138 } else { |
| 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
); |
| 143 } |
| 144 self.terminal.handler(input); |
| 145 } |
| 146 }, 0); |
| 147 } |
| 148 }; |
| 149 |
| 150 /** |
| 151 * Apply any changes made to the textarea after the current event chain is allow
ed to complete. |
| 152 * This should be called when not currently composing but a keydown event with t
he "composition |
| 153 * character" (229) is triggered, in order to allow non-composition text to be e
ntered when an |
| 154 * IME is active. |
| 155 */ |
| 156 CompositionHelper.prototype.handleAnyTextareaChanges = function() { |
| 157 var oldValue = this.textarea.value; |
| 158 var self = this; |
| 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); |
| 166 } |
| 167 } |
| 168 }, 0); |
| 169 }; |
| 170 |
| 171 /** |
| 172 * Positions the composition view on top of the cursor and the textarea just bel
ow it (so the |
| 173 * IME helper dialog is positioned correctly). |
| 174 */ |
| 175 CompositionHelper.prototype.updateCompositionElements = function(dontRecurse) { |
| 176 if (!this.isComposing) { |
| 177 return; |
| 178 } |
| 179 var cursor = this.terminal.element.querySelector('.terminal-cursor'); |
| 180 if (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'; |
| 186 } |
| 187 if (!dontRecurse) { |
| 188 setTimeout(this.updateCompositionElements.bind(this, true), 0); |
| 189 } |
| 190 }; |
| 191 |
| 192 /** |
| 193 * Clears the textarea's position so that the cursor does not blink on IE. |
| 194 * @private |
| 195 */ |
| 196 CompositionHelper.prototype.clearTextareaPosition = function() { |
| 197 this.textarea.style.left = ''; |
| 198 this.textarea.style.top = ''; |
| 199 }; |
| 200 |
| 201 export { CompositionHelper }; |
OLD | NEW |