Merge remote-tracking branch 'upstream/master' into 118_support_custom_keydown_handler

This commit is contained in:
Daniel Imms
2016-07-22 05:12:52 -07:00
13 changed files with 768 additions and 221 deletions
+11
View File
@@ -0,0 +1,11 @@
<!-- Enter your issue description here -->
## Details
- Browser and browser version:
- OS version:
- xterm.js version:
### Steps to reproduce
1.
2.
+7
View File
@@ -0,0 +1,7 @@
Antonis Kalipetis <akalipetis@sourcelair.com>
Antonis Kalipetis <akalipetis@sourcelair.com> <akalipetis@gmail.com>
Daniel Imms <daimms@microsoft.com> <tyriar@tyriar.com>
Paris Kasidiaris <paris@sourcelair.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
Thanasis Daglis <thanasis@sourcelair.com> <ath.daglis@gmail.com>
+19
View File
@@ -0,0 +1,19 @@
List of xterm.js contributors. Updated before every release.
Alexander Olsson <noseglid@gmail.com>
Antonis Kalipetis <akalipetis@sourcelair.com>
Austin Robertson <austinrobertson@gmail.com>
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
Carson Anderson <carson@betterservers.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
Dan Kaplun <dbkaplun@twitch.tv>
Darin Morrison <freebroccolo@users.noreply.github.com>
Jean Bruenn <himself@jeanbruenn.info>
Jörg Breitbart <jerch@rockborn.de>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Paris Kasidiaris <paris@sourcelair.com>
runarberg <runar@greenqloud.com>
Steven Silvester <steven.silvester@ieee.org>
Thanasis Daglis <thanasis@sourcelair.com>
+52
View File
@@ -0,0 +1,52 @@
# How to contribute to xterm.js
- [Opening issues for bug reports or feature requests](#opening-issues)
- [Contributing code](#contributing-code)
## Opening issues
The preferred way to report bugs or request features is to use
[GitHub issues](http://github.com/sourcelair/xterm.js/issues). Before
opening an issue, read these pointers.
### Opening issues effectively
- Include information about **the browser in which the problem occurred**. Even
if you tested several browsers, and the problem occurred in all of them,
mention this fact in the bug report. Also include browser version numbers and
the operating system that you're on.
- Mention which release of xterm.js you're using. Preferably, try also with
the current HEAD of the master branch, to ensure the problem has not already been
fixed.
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
exact steps a maintainer has to take to make the problem occur.
- If the problem can not be reproduced in the [demo of xterm.js](README.md#demo), please provide an HTML document that demonstrates the problem.
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
## Contributing code
- Make sure you have a [GitHub account](https://github.com/join)
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Make your changes
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below).
- Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to xterm.js you
- agree to license the contributed code under xterm.js' [MIT
license](LICENSE).
- confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
+32 -5
View File
@@ -10,6 +10,19 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad
![xterm.js screenshot](xtermjs.png)
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
- Chrome 48+
- Edge 13+
- Firefox 44+
- Internet Explorer 11+
- Opera 35+
- Safari 8+
xterm.js may work on earlier versions of the browsers but these are the browsers we strive to keep working.
## Demo
To launch the demo simply run:
@@ -29,21 +42,35 @@ To use an addon, just include the JavaScript file after xterm.js and before the
```html
<script src="node_modules/src/xterm.js"></script>
<script src="node_modules/addons/linkify/linkify.js"></script>
<script src="node_modules/addons/fit/fit.js"></script>
```
```js
var xterm = new Terminal();
// init code...
xterm.linkify();
xterm.fit();
```
## Contribution and License Agreement
## Development and Contribution
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) before.
The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features).
It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are:
#### [SourceLair](https://www.sourcelair.com)
Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser.
#### [Visual Studio Code](http://code.visualstudio.com/)
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set.
## License Agreement
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
## License
Copyright (c) 2014-2016, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+7
View File
@@ -0,0 +1,7 @@
#! /usr/bin/env sh
tail --lines=+3 AUTHORS > AUTHORS.tmp
git log --format='%aN <%aE>' >> AUTHORS.tmp
echo "List of xterm.js contributors. Updated before every release.\n" > AUTHORS
sort -u AUTHORS.tmp >> AUTHORS
rm -f AUTHORS.tmp
+27
View File
@@ -0,0 +1,27 @@
#! /usr/bin/env sh
# Usage: ./bin/prepare-release x.y.z
# x.y.z should be semver (e.g. 1.0.0)
NEW_VERSION=$1
CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
| grep version \
| head -1 \
| awk -F: '{ print $2 }' \
| sed 's/[",]//g' \
| tr -d '[[:space:]]')
CURRENT_BOWER_JSON_VERSION=$(cat bower.json \
| grep version \
| head -1 \
| awk -F: '{ print $2 }' \
| sed 's/[",]//g' \
| tr -d '[[:space:]]')
# Update version in package.json and bower.json
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
sed -i "s/\"version\": \"$CURRENT_BOWER_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" bower.json
# Update AUTHORS file
sh bin/generate-authors
git commit -a -m "Bump version to $NEW_VERSION"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "xterm.js",
"version": "0.33",
"version": "1.0.0",
"ignore": ["demo", "test", ".gitignore"]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm",
"version": "0.33.0",
"version": "1.0.0",
"ignore": [
"demo",
"test",
+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
*/
+306 -207
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
*/
@@ -437,7 +626,7 @@
* Focus the terminal. Delegates focus handling to the terminal's DOM element.
*/
Terminal.prototype.focus = function() {
return this.element.focus();
return this.textarea.focus();
};
/**
@@ -446,11 +635,11 @@
* @static
*/
Terminal.bindFocus = function (term) {
on(term.element, 'focus', function (ev) {
on(term.textarea, 'focus', function (ev) {
if (term.sendFocus) {
term.send('\x1b[I');
}
term.element.classList.add('focus');
term.showCursor();
Terminal.focus = term;
term.emit('focus', {terminal: term});
@@ -461,7 +650,7 @@
* Blur the terminal. Delegates blur handling to the terminal's DOM element.
*/
Terminal.prototype.blur = function() {
return this.element.blur();
return this.textarea.blur();
};
/**
@@ -470,10 +659,12 @@
* @static
*/
Terminal.bindBlur = function (term) {
on(term.element, 'blur', function (ev) {
on(term.textarea, 'blur', function (ev) {
term.refresh(term.y, term.y);
if (term.sendFocus) {
term.send('\x1b[O');
}
term.element.classList.remove('focus');
Terminal.focus = null;
term.emit('blur', {terminal: term});
});
@@ -483,122 +674,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);
};
/**
@@ -626,52 +722,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
});
};
@@ -747,7 +840,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
@@ -758,11 +850,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);
@@ -2551,6 +2671,11 @@
if (this.customKeydownHandler && this.customKeydownHandler(ev) === false) {
return false;
}
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) {
return false;
}
var self = this;
var result = this.evaluateKeyEscapeSequence(ev);
@@ -2597,6 +2722,7 @@
// The number of characters to scroll, if this is defined it will cancel the event
scrollDisp: undefined
};
var modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3;
switch (ev.keyCode) {
// backspace
case 8:
@@ -2627,61 +2753,39 @@
break;
// left-arrow
case 37:
if (ev.altKey) {
result.key = '\x1bb' // Jump a word back
result.cancel = true;
break;
}
if (ev.ctrlKey) {
result.key = '\x1b[5D'; // Jump a word back
break;
}
if (this.applicationCursor) {
result.key = '\x1bOD'; // SS3 as ^[O for 7-bit
break;
}
result.key = '\x1b[D';
if (modifiers)
result.key = '\x1b[1;' + (modifiers + 1) + 'D';
else if (this.applicationCursor)
result.key = '\x1bOD';
else
result.key = '\x1b[D';
break;
// right-arrow
case 39:
if (ev.altKey) {
result.key = '\x1bf' // Jump a word forward
result.cancel = true;
break;
}
if (ev.ctrlKey) {
result.key = '\x1b[5C'; // Jump a word forward
break;
}
if (this.applicationCursor) {
if (modifiers)
result.key = '\x1b[1;' + (modifiers + 1) + 'C';
else if (this.applicationCursor)
result.key = '\x1bOC';
break;
}
result.key = '\x1b[C';
else
result.key = '\x1b[C';
break;
// up-arrow
case 38:
if (this.applicationCursor) {
if (modifiers)
result.key = '\x1b[1;' + (modifiers + 1) + 'A';
else if (this.applicationCursor)
result.key = '\x1bOA';
break;
}
if (ev.ctrlKey) {
result.scrollDisp = -1;
} else {
else
result.key = '\x1b[A';
}
break;
// down-arrow
case 40:
if (this.applicationCursor) {
if (modifiers)
result.key = '\x1b[1;' + (modifiers + 1) + 'B';
else if (this.applicationCursor)
result.key = '\x1bOB';
break;
}
if (ev.ctrlKey) {
result.scrollDisp = 1;
} else {
else
result.key = '\x1b[B';
}
break;
// insert
case 45:
@@ -2695,19 +2799,21 @@
case 46: result.key = '\x1b[3~'; break;
// home
case 36:
if (this.applicationKeypad) {
if (modifiers)
result.key = '\x1b[1;' + (modifiers + 1) + 'H';
else if (this.applicationCursor)
result.key = '\x1bOH';
break;
}
result.key = '\x1bOH';
else
result.key = '\x1b[H';
break;
// end
case 35:
if (this.applicationKeypad) {
if (modifiers)
result.key = '\x1b[1;' + (modifiers + 1) + 'F';
else if (this.applicationCursor)
result.key = '\x1bOF';
break;
}
result.key = '\x1bOF';
else
result.key = '\x1b[F';
break;
// page up
case 33:
@@ -2804,6 +2910,8 @@
Terminal.prototype.keyPress = function(ev) {
var key;
this.cancel(ev);
if (ev.charCode) {
key = ev.charCode;
} else if (ev.which == null) {
@@ -2822,21 +2930,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;
};
@@ -5150,6 +5248,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);
});
});
});
+12 -4
View File
@@ -24,8 +24,8 @@ describe('xterm.js', function() {
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
// End, Home
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1bOF'); // SS3 F
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1bOH'); // SS3 H
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
// Left, up, right, down arrows
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A
@@ -50,10 +50,10 @@ describe('xterm.js', function() {
assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
});
it('should return \\x1b[5D for ctrl+left', function() {
assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[5D'); // CSI 5 D
assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for ctrl+right', function() {
assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[5C'); // CSI 5 C
assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
});
@@ -113,6 +113,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() {