Merge pull request #175 from Tyriar/124_add_textarea_back

Add textarea back and support IMEs
This commit is contained in:
Paris Kasidiaris
2016-07-22 11:50:23 +03:00
committed by GitHub
4 changed files with 581 additions and 156 deletions
+33 -3
View File
@@ -43,22 +43,40 @@
position: relative;
}
.terminal:focus {
.terminal.focus {
outline: none;
}
.terminal .xterm-helpers {
position: absolute;
top: 0;
}
.terminal .xterm-helper-textarea {
position: absolute;
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
left: -9999em;
opacity: 0;
width: 0;
height: 0;
z-index: -10;
}
.terminal .terminal-cursor {
background-color: #fff;
color: #000;
}
.terminal:not(:focus) .terminal-cursor {
.terminal:not(.focus) .terminal-cursor {
outline: 1px solid #fff;
outline-offset: -1px;
background-color: transparent;
}
.terminal:focus .terminal-cursor.blinking {
.terminal.focus .terminal-cursor.blinking {
animation: blink-cursor 1.2s infinite step-end;
}
@@ -73,6 +91,18 @@
}
}
.terminal .composition-view {
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
}
.terminal .composition-view.active {
display: block;
}
/*
* Determine default colors for xterm.js
*/
+280 -153
View File
@@ -108,8 +108,8 @@
var self = this;
function on() {
var args = Array.prototype.slice.call(arguments);
self.removeListener(type, on);
return listener.apply(self, args);
this.removeListener(type, on);
return listener.apply(this, args);
}
on.listener = listener;
return this.on(type, on);
@@ -133,6 +133,195 @@
};
/**
* 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.
* @param {HTMLTextAreaElement} textarea The textarea that xterm uses for input.
* @param {HTMLElement} compositionView The element to display the in-progress composition in.
* @param {Terminal} terminal The Terminal to forward the finished composition to.
*/
function CompositionHelper(textarea, compositionView, terminal) {
this.textarea = textarea;
this.compositionView = compositionView;
this.terminal = terminal;
// 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.
this.isComposing = false;
// The input currently being composed, eg. via a mobile keyboard, speech input or IME.
this.compositionText = null;
// The position within the input textarea's value of the current composition.
this.compositionPosition = { start: null, end: null };
// Whether a composition is in the process of being sent, setting this to false will cancel
// any in-progress composition.
this.isSendingComposition = false;
}
/**
* Handles the compositionstart event, activating the composition view.
*/
CompositionHelper.prototype.compositionstart = function() {
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 {CompositionEvent} ev The event.
*/
CompositionHelper.prototype.compositionupdate = function(ev) {
this.compositionView.textContent = ev.data;
this.updateCompositionElements();
var self = this;
setTimeout(function() {
self.compositionPosition.end = self.textarea.value.length;
}, 0);
};
/**
* Handles the compositionend event, hiding the composition view and sending the composition to
* the handler.
*/
CompositionHelper.prototype.compositionend = function() {
this.finalizeComposition(true);
};
/**
* Handles the keydown event, routing any necessary events to the CompositionHelper functions.
* @return Whether the Terminal should continue processing the keydown event.
*/
CompositionHelper.prototype.keydown = function(ev) {
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;
} else {
// 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 {boolean} waitForPropogation Whether to wait for events to propogate 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 send before
* the command is executed.
*/
CompositionHelper.prototype.finalizeComposition = function(waitForPropogation) {
this.compositionView.classList.remove('active');
this.isComposing = false;
this.clearTextareaPosition();
if (!waitForPropogation) {
// Cancel any delayed composition send requests and send the input immediately.
this.isSendingComposition = false;
var input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
this.terminal.handler(input);
} else {
// Make a deep copy of the composition position here as a new compositionstart event may
// fire before the setTimeout executes.
var 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.
var self = this;
this.isSendingComposition = true;
setTimeout(function () {
// Ensure that the input has not already been sent
if (self.isSendingComposition) {
self.isSendingComposition = false;
var input;
if (self.isComposing) {
// Use the end position to get the string if a new composition has started.
input = self.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 = self.textarea.value.substring(currentCompositionPosition.start);
}
self.terminal.handler(input);
}
}, 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.
*/
CompositionHelper.prototype.handleAnyTextareaChanges = function() {
var oldValue = this.textarea.value;
var self = this;
setTimeout(function() {
// Ignore if a composition has started since the timeout
if (!self.isComposing) {
var newValue = self.textarea.value;
var diff = newValue.replace(oldValue, '');
if (diff.length > 0) {
self.terminal.handler(diff);
}
}
}, 0);
}
/**
* Positions the composition view on top of the cursor and the textarea just below it (so the
* IME helper dialog is positioned correctly).
*/
CompositionHelper.prototype.updateCompositionElements = function() {
var cursor = this.terminal.element.querySelector('.terminal-cursor');
if (cursor) {
this.compositionView.style.left = cursor.offsetLeft + 'px';
this.compositionView.style.top = cursor.offsetTop + 'px';
this.textarea.style.left = cursor.offsetLeft + 'px';
this.textarea.style.top = (cursor.offsetTop + cursor.offsetHeight) + 'px';
}
};
/**
* Clears the textarea's position so that the cursor does not blink on IE.
* @private
*/
CompositionHelper.prototype.clearTextareaPosition = function() {
this.textarea.style.left = undefined;
this.textarea.style.top = undefined;
}
/**
* States
*/
@@ -446,11 +635,17 @@
*/
Terminal.bindFocus = function (term) {
on(term.element, 'focus', function (ev) {
if (Terminal.focus === term) {
return;
}
if (term.sendFocus) {
term.send('\x1b[I');
}
term.element.classList.add('focus');
term.showCursor();
term.textarea.focus();
Terminal.focus = term;
term.emit('focus', {terminal: term});
});
@@ -470,6 +665,12 @@
*/
Terminal.bindBlur = function (term) {
on(term.element, 'blur', function (ev) {
if (Terminal.focus !== term) {
return;
}
term.element.classList.remove('focus');
term.refresh(term.y, term.y);
term.textarea.blur();
if (term.sendFocus) {
term.send('\x1b[O');
}
@@ -482,122 +683,27 @@
* Initialize default behavior
*/
Terminal.prototype.initGlobal = function() {
Terminal.bindKeys(this);
Terminal.bindPaste(this);
Terminal.bindKeys(this);
Terminal.bindCopy(this);
Terminal.bindCut(this);
Terminal.bindDrop(this);
Terminal.bindFocus(this);
Terminal.bindBlur(this);
};
/**
* Clears all selected text, inside the terminal.
*/
Terminal.prototype.clearSelection = function() {
var selectionBaseNode = window.getSelection().baseNode;
if (selectionBaseNode && (this.element.contains(selectionBaseNode.parentElement))) {
window.getSelection().removeAllRanges();
}
};
/**
* This function temporarily enables (leases) the contentEditable value of the terminal, which
* should be set back to false within 5 seconds at most.
*/
Terminal.prototype.leaseContentEditable = function (ms, callback) {
var term = this;
term.element.contentEditable = true;
/**
* Blur and re-focus instantly. This is due to a weird focus state on Chrome, when setting
* contentEditable to true on a focused element.
*/
term.blur();
term.focus();
setTimeout(function () {
term.element.contentEditable = false;
if (typeof callback == 'function') {
callback.call(term);
}
}, ms || 5000);
};
/**
* Bind to paste event and allow both keyboard and right-click pasting, without having the
* contentEditable value set to true.
*/
Terminal.bindPaste = function(term) {
on(term.element, 'paste', function(ev) {
on([term.textarea, term.element], 'paste', function(ev) {
ev.stopPropagation();
if (ev.clipboardData) {
var text = ev.clipboardData.getData('text/plain');
term.emit('paste', text, ev);
term.handler(text);
/**
* Cancel the paste event, or else things will be pasted twice:
* 1. by the terminal handler
* 2. by the browser, because of the contentEditable value being true
*/
term.cancel(ev, true);
/**
* After the paste event is completed, always set the contentEditable value to false.
*/
term.element.contentEditable = false;
term.textarea.value = '';
return term.cancel(ev);
}
});
/**
* Hack pasting with keyboard, in order to make it work without contentEditable.
* When a user types Ctrl + Shift + V or Shift + Insert on a non Mac or Cmd + V on a Mac,
* lease the contentEditable value as true.
*/
on(term.element, 'keydown', function (ev) {
var isEditable = term.element.contentEditable === "true";
/**
* If on a Mac, lease the contentEditable value temporarily, when the user presses
* the Cmd button, in a keydown event order to paste frictionlessly.
*/
if (term.isMac && ev.metaKey && !isEditable) {
term.leaseContentEditable(5000);
}
if (!term.isMac && !isEditable) {
if ((ev.keyCode == 45 && ev.shiftKey && !ev.ctrlKey) || // Shift + Insert
(ev.keyCode == 86 && ev.shiftKey && ev.ctrlKey)) { // Ctrl + Shict + V
term.leaseContentEditable();
}
}
});
/**
* Hack pasting with right-click in order to allow right-click paste, by leasing the
* contentEditable value as true.
*/
on(term.element, 'contextmenu', function (ev) {
term.leaseContentEditable();
});
};
/**
* Apply key handling to the terminal
*
* @param {Xterm} term The terminal on which to bind key handling
* @static
*/
Terminal.bindKeys = function(term) {
on(term.element, 'keydown', function(ev) {
term.keyDown(ev);
}, true);
on(term.element, 'keypress', function(ev) {
term.keyPress(ev);
}, true);
};
/**
@@ -625,52 +731,49 @@
return processedText;
};
/**
* Apply key handling to the terminal
*/
Terminal.bindKeys = function(term) {
on(term.element, 'keydown', function(ev) {
if (document.activeElement != this) {
return;
}
term.keyDown(ev);
}, true);
on(term.element, 'keypress', function(ev) {
if (document.activeElement != this) {
return;
}
term.keyPress(ev);
}, true);
on(term.element, 'keyup', term.focus.bind(term));
on(term.textarea, 'keydown', function(ev) {
term.keyDown(ev);
}, true);
on(term.textarea, 'keypress', function(ev) {
term.keyPress(ev);
// Truncate the textarea's value, since it is not needed
this.value = '';
}, true);
on(term.textarea, 'compositionstart', term.compositionHelper.compositionstart.bind(term.compositionHelper));
on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper));
term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));
};
/**
* Binds copy functionality to the given terminal.
* @static
*/
Terminal.bindCopy = function(term) {
on(term.element, 'copy', function(ev) {
var copiedText = window.getSelection().toString(),
text = Terminal.prepareCopiedTextForClipboard(copiedText);
ev.clipboardData.setData('text/plain', text);
ev.preventDefault();
});
};
/**
* Cancel the cut event completely.
* @param {Xterm} term The terminal on which to bind the cut event handling functionality.
* @static
*/
Terminal.bindCut = function(term) {
on(term.element, 'cut', function (ev) {
ev.preventDefault();
});
};
/**
* Do not perform the "drop" event. Altering the contents of the
* terminal with drag n drop is unwanted behavior.
* @param {Xterm} term The terminal on which to bind the drop event handling functionality.
* @static
*/
Terminal.bindDrop = function (term) {
on(term.element, 'drop', function (ev) {
term.cancel(ev, true);
});
};
/**
* Cancel click handling on the given terminal
* @param {Xterm} term The terminal on which to bind the click event handling functionality.
* @static
*/
Terminal.click = function (term) {
on(term.element, 'click', function (ev) {
term.cancel(ev, true);
return; // temporary
});
};
@@ -746,7 +849,6 @@
this.element.classList.add('xterm');
this.element.classList.add('xterm-theme-' + this.theme);
this.element.setAttribute('tabindex', 0);
this.element.spellcheck = false;
/*
* Create the container that will hold the lines of the terminal and then
@@ -757,11 +859,39 @@
this.element.appendChild(this.rowContainer);
this.children = [];
/*
* Create the container that will hold helpers like the textarea for
* capturing DOM Events. Then produce the helpers.
*/
this.helperContainer = document.createElement('div');
this.helperContainer.classList.add('xterm-helpers');
// TODO: This should probably be inserted once it's filled to prevent an additional layout
this.element.appendChild(this.helperContainer);
this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
this.textarea.setAttribute('autocorrect', 'off');
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.textarea.addEventListener('focus', function() {
self.emit('focus', {terminal: self});
});
this.textarea.addEventListener('blur', function() {
self.emit('blur', {terminal: self});
});
this.helperContainer.appendChild(this.textarea);
this.compositionView = document.createElement('div');
this.compositionView.classList.add('composition-view');
this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this);
this.helperContainer.appendChild(this.compositionView);
for (; i < this.rows; i++) {
this.insertRow();
}
this.parent.appendChild(this.element);
// Draw the screen.
this.refresh(0, this.rows - 1);
@@ -2535,6 +2665,10 @@
* @param {KeyboardEvent} ev The keydown event to be handled.
*/
Terminal.prototype.keyDown = function(ev) {
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) {
return false;
}
var self = this;
var result = this.evaluateKeyEscapeSequence(ev);
@@ -2769,6 +2903,8 @@
Terminal.prototype.keyPress = function(ev) {
var key;
this.cancel(ev);
if (ev.charCode) {
key = ev.charCode;
} else if (ev.which == null) {
@@ -2787,21 +2923,11 @@
key = String.fromCharCode(key);
/**
* When a key is pressed and a character is sent to the terminal, then clear any text
* selected in the terminal.
*/
if (key) {
this.clearSelection();
}
this.emit('keypress', key, ev);
this.emit('key', key, ev);
this.showCursor();
this.handler(key);
this.cancel(ev, true);
return false;
};
@@ -5115,6 +5241,7 @@
*/
Terminal.EventEmitter = EventEmitter;
Terminal.CompositionHelper = CompositionHelper;
Terminal.inherits = inherits;
/**
+260
View File
@@ -0,0 +1,260 @@
var assert = require('chai').assert;
var Terminal = require('../src/xterm');
describe('CompositionHelper', function () {
var terminal;
var compositionHelper;
var compositionView;
var textarea;
var handledText;
beforeEach(function () {
compositionView = {
classList: {
add: function () {},
remove: function () {},
},
style: {
left: 0,
top: 0
},
textContent: ''
}
textarea = {
value: '',
style: {
left: 0,
top: 0
}
}
terminal = {
element: {
querySelector: function () {
return { offsetLeft: 0, offsetTop: 0 };
}
},
handler: function (text) {
handledText += text;
}
}
handledText = '';
compositionHelper = new Terminal.CompositionHelper(textarea, compositionView, terminal);
});
describe('Public API', function () {
it('should define CompositionHelper.prototype.compositionstart', function () {
assert.isDefined(Terminal.CompositionHelper.prototype.compositionstart);
});
it('should define CompositionHelper.prototype.compositionupdate', function () {
assert.isDefined(Terminal.CompositionHelper.prototype.compositionupdate);
});
it('should define CompositionHelper.prototype.compositionend', function () {
assert.isDefined(Terminal.CompositionHelper.prototype.compositionend);
});
it('should define CompositionHelper.prototype.finalizeComposition', function () {
assert.isDefined(Terminal.CompositionHelper.prototype.finalizeComposition);
});
it('should define CompositionHelper.prototype.handleAnyTextareaChanges', function () {
assert.isDefined(Terminal.CompositionHelper.prototype.handleAnyTextareaChanges);
});
it('should define CompositionHelper.prototype.updateCompositionElements', function () {
assert.isDefined(Terminal.CompositionHelper.prototype.updateCompositionElements);
});
it('should define CompositionHelper.isComposing', function () {
assert.isDefined(compositionHelper.isComposing);
});
it('should define CompositionHelper.isSendingComposition', function () {
assert.isDefined(compositionHelper.isSendingComposition);
});
});
describe('Input', function () {
it('Should insert simple characters', function (done) {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, 'ㅇ');
// Second character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇㅇ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, 'ㅇㅇ');
done();
}, 0);
}, 0);
}, 0);
}, 0);
});
it('Should insert complex characters', function (done) {
// First character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '아';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '앙' });
textarea.value = '앙';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, '앙');
// Second character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = '앙ㅇ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '앙아';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '앙' });
textarea.value = '앙앙';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, '앙앙');
done();
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
});
it('Should insert complex characters that change with following character', function (done) {
// First character '아'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '아';
setTimeout(function() { // wait for any textarea updates
// Start second character '아' in first character
compositionHelper.compositionupdate({ data: '앙' });
textarea.value = '앙';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '아아'
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, '아아');
done();
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
});
it('Should insert multi-characters compositions', function (done) {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'd' });
textarea.value = 'd';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'だ' });
textarea.value = 'だ';
setTimeout(function() { // wait for any textarea updates
// Second character 'あ'
compositionHelper.compositionupdate({ data: 'だあ' });
textarea.value = 'だあ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, 'だあ');
done();
}, 0);
}, 0);
}, 0);
}, 0);
});
it('Should insert multi-character compositions that are converted to other characters with the same length', function (done) {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'd' });
textarea.value = 'd';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'だ' });
textarea.value = 'だ';
setTimeout(function() { // wait for any textarea updates
// Second character 'ー'
compositionHelper.compositionupdate({ data: 'だー' });
textarea.value = 'だー';
setTimeout(function() { // wait for any textarea updates
// Convert to katakana 'ダー'
compositionHelper.compositionupdate({ data: 'ダー' });
textarea.value = 'ダー';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, 'ダー');
done();
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
})
it('Should insert multi-character compositions that are converted to other characters with different lengths', function (done) {
// First character 'い'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'い' });
textarea.value = 'い';
setTimeout(function() { // wait for any textarea updates
// Second character 'ま'
compositionHelper.compositionupdate({ data: 'いm' });
textarea.value = 'いm';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'いま' });
textarea.value = 'いま';
setTimeout(function() { // wait for any textarea updates
// Convert to kanji '今'
compositionHelper.compositionupdate({ data: '今' });
textarea.value = '今';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, '今');
done();
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
});
it('Should insert non-composition characters input immediately after composition characters', function (done) {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(function() { // wait for any textarea updates
compositionHelper.compositionend();
// Second character '1' (a non-composition character)
textarea.value = 'ㅇ1';
setTimeout(function() { // wait for any textarea updates
assert.equal(handledText, 'ㅇ1');
done();
}, 0);
}, 0);
});
});
});
+8
View File
@@ -87,6 +87,14 @@ describe('xterm.js', function() {
xterm.handler = function() {};
xterm.showCursor = function() {};
xterm.clearSelection = function() {};
xterm.compositionHelper = {
isComposing: false,
keydown: {
bind: function() {
return function() { return true; };
}
}
};
});
describe('On Mac OS', function() {