Merge pull request #175 from Tyriar/124_add_textarea_back
Add textarea back and support IMEs
This commit is contained in:
+33
-3
@@ -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
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user