Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd70d298fc | ||
|
|
70d0ed3ebf | ||
|
|
5a5c6ab621 | ||
|
|
a34bb9724f | ||
|
|
824a9c6dbf | ||
|
|
c9eaae6bd5 | ||
|
|
9f5790fcaf | ||
|
|
6aeb0435ce | ||
|
|
70ec2496db | ||
|
|
55314e0fc7 | ||
|
|
243d76617b | ||
|
|
6cc66a0b20 | ||
|
|
1e928df531 | ||
|
|
db9bec02f5 | ||
|
|
0dc033692f | ||
|
|
1d30091188 | ||
|
|
64f80f8623 | ||
|
|
83b06c6bb0 | ||
|
|
5cbc2f0ea5 | ||
|
|
2bc8adee8c | ||
|
|
c6eb7f136c | ||
|
|
48b87c2f40 | ||
|
|
ca818fbf04 | ||
|
|
264fe4edc7 | ||
|
|
317c0ec2de | ||
|
|
3b2e89d878 | ||
|
|
15a94240eb | ||
|
|
f75b7db90e | ||
|
|
81dc48ef00 | ||
|
|
3638ef808b | ||
|
|
f24cba722a | ||
|
|
30fcdd6ccb | ||
|
|
b0df236dea | ||
|
|
d91e33ff39 | ||
|
|
1ee316ca83 | ||
|
|
dcbf014402 | ||
|
|
20b0d4fdcc | ||
|
|
2da1255c26 | ||
|
|
5bddb93745 | ||
|
|
51932161af | ||
|
|
e0d9871195 | ||
|
|
634d4af4dd | ||
|
|
a507e4f002 | ||
|
|
c15fed382d | ||
|
|
9937d54410 | ||
|
|
64f6610f11 | ||
|
|
e6f21f0bfc | ||
|
|
c63732ef5f | ||
|
|
99496de242 | ||
|
|
29cb576737 | ||
|
|
bf4ef599ad | ||
|
|
5ddb33419e | ||
|
|
bc10fbdfae | ||
|
|
6dcf7267a5 | ||
|
|
5712365cac | ||
|
|
1fc5a9aada | ||
|
|
38cd1bd6d7 | ||
|
|
771642d17d | ||
|
|
bb19b2040b | ||
|
|
12840dae6c | ||
|
|
521d7dbdaa | ||
|
|
2cba2ff3d4 | ||
|
|
547db92693 | ||
|
|
bf334238b7 | ||
|
|
5808de648b | ||
|
|
9ab0455b05 | ||
|
|
55ab36662a | ||
|
|
b1ad564b0d | ||
|
|
bc70b3b37d | ||
|
|
d426c99866 | ||
|
|
356377975b | ||
|
|
0de3d839e6 | ||
|
|
0fcd6b1d22 | ||
|
|
99b5464392 | ||
|
|
70965a8fd6 | ||
|
|
77ca15497c | ||
|
|
56ecc77dfc | ||
|
|
a995b610cf | ||
|
|
332bc6be76 | ||
|
|
26de9fe4c5 | ||
|
|
00dcb4f6e3 | ||
|
|
996c641a79 | ||
|
|
a077860464 | ||
|
|
5e68acfc76 | ||
|
|
269540bfed | ||
|
|
868f15173d | ||
|
|
06a93344bb | ||
|
|
e8e7870be2 | ||
|
|
a9417c6878 | ||
|
|
605d61b64f | ||
|
|
8feaacf57e | ||
|
|
553c651166 | ||
|
|
1ec0bac9c6 | ||
|
|
b1c6ae8a06 | ||
|
|
8a124d0b35 | ||
|
|
0ad02a4afa | ||
|
|
9389f899d7 | ||
|
|
050a34921b | ||
|
|
32e878db81 | ||
|
|
0bf7bf56a5 | ||
|
|
e5d130b698 | ||
|
|
941e57a218 | ||
|
|
c02eea6ad3 | ||
|
|
fe0d878b23 | ||
|
|
763f148fa1 | ||
|
|
8f0ada708f | ||
|
|
8117999ab3 | ||
|
|
10dd22a3d4 | ||
|
|
738f37e86c | ||
|
|
729c0abf04 | ||
|
|
fe54da34f8 | ||
|
|
09de0f31fa | ||
|
|
6ee524864e | ||
|
|
8fd350f0fc | ||
|
|
ebbeeefd86 | ||
|
|
76a890aa74 | ||
|
|
5ce522a401 | ||
|
|
bd994f9e57 | ||
|
|
441009d359 |
+1
-1
@@ -6,7 +6,7 @@ indent_size = 2
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.js]
|
[*.{j,t}s]
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|
||||||
[*.css]
|
[*.css]
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*.swp
|
*.swp
|
||||||
build/*
|
|
||||||
.lock-wscript
|
.lock-wscript
|
||||||
out/
|
out/
|
||||||
Makefile.gyp
|
Makefile.gyp
|
||||||
@@ -11,3 +10,5 @@ example/*.log
|
|||||||
docs/
|
docs/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
/.idea/
|
/.idea/
|
||||||
|
.env
|
||||||
|
build/
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 4
|
- 6
|
||||||
env:
|
env:
|
||||||
- CXX=g++-4.8
|
- CXX=g++-4.8
|
||||||
addons:
|
addons:
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ Daniel Imms <daimms@microsoft.com>
|
|||||||
Daniel Risacher <drisacher@gmail.com>
|
Daniel Risacher <drisacher@gmail.com>
|
||||||
Dan Kaplun <dbkaplun@twitch.tv>
|
Dan Kaplun <dbkaplun@twitch.tv>
|
||||||
Darin Morrison <freebroccolo@users.noreply.github.com>
|
Darin Morrison <freebroccolo@users.noreply.github.com>
|
||||||
|
Elliot Saba <staticfloat@gmail.com>
|
||||||
|
hiro-su <h.sugipon@gmail.com>
|
||||||
|
Ian Lewis <ianlewis@google.com>
|
||||||
imoses <ido@twiggle.com>
|
imoses <ido@twiggle.com>
|
||||||
Jean Bruenn <himself@jeanbruenn.info>
|
Jean Bruenn <himself@jeanbruenn.info>
|
||||||
Jörg Breitbart <jerch@rockborn.de>
|
Jörg Breitbart <jerch@rockborn.de>
|
||||||
|
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||||
Paris Kasidiaris <paris@sourcelair.com>
|
Paris Kasidiaris <paris@sourcelair.com>
|
||||||
runarberg <runar@greenqloud.com>
|
runarberg <runar@greenqloud.com>
|
||||||
|
Shuanglei Tao <tsl0922@gmail.com>
|
||||||
Steven Silvester <steven.silvester@ieee.org>
|
Steven Silvester <steven.silvester@ieee.org>
|
||||||
Thanasis Daglis <thanasis@sourcelair.com>
|
Thanasis Daglis <thanasis@sourcelair.com>
|
||||||
Tine Jozelj <tine.jozelj@outlook.com>
|
Tine Jozelj <tine.jozelj@outlook.com>
|
||||||
|
YuviPanda <yuvipanda@gmail.com>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# xterm.js
|
# xterm.js
|
||||||
|
|
||||||
 [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://travis-ci.org/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
|||||||
|
|
||||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
|
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
|
||||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
|
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
|
||||||
|
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
|
||||||
|
|
||||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
|
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
|
||||||
|
|
||||||
@@ -53,13 +54,13 @@ Then open http://0.0.0.0:3000 in a web browser (use http://127.0.0.1:3000 if run
|
|||||||
|
|
||||||
## Addons
|
## Addons
|
||||||
|
|
||||||
Addons are JavaScript modules that attach functions to the `Terminal` prototype to extend its functionality. There are a handful available in the main repository in the `addons` directory, you can even write your own (though they may break when the internals of xterm.js change across versions).
|
Addons are JavaScript modules that attach functions to the `Terminal` prototype to extend its functionality. There are a handful available in the main repository in the `dist/addons` directory, you can even write your own (though they may break when the internals of xterm.js change across versions).
|
||||||
|
|
||||||
To use an addon, just include the JavaScript file after xterm.js and before the `Terminal` object has been instantiated. The function should then be exposed on the `Terminal` object:
|
To use an addon, just include the JavaScript file after xterm.js and before the `Terminal` object has been instantiated. The function should then be exposed on the `Terminal` object:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="node_modules/dist/xterm.js"></script>
|
<script src="node_modules/dist/xterm.js"></script>
|
||||||
<script src="node_modules/addons/fit/fit.js"></script>
|
<script src="node_modules/dist/addons/fit/fit.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -1,38 +1,35 @@
|
|||||||
#! /usr/bin/env node
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
var child_process = require('child_process');
|
set -e
|
||||||
var fs = require('fs');
|
|
||||||
|
|
||||||
var buildDir = process.env.BUILD_DIR || 'build';
|
# $BUILD_DIR should default to "build"
|
||||||
|
BUILD_DIR=${BUILD_DIR:=build}
|
||||||
|
|
||||||
if (!fs.existsSync(buildDir)){
|
# Create the build directory
|
||||||
fs.mkdirSync(buildDir);
|
mkdir -p $BUILD_DIR
|
||||||
}
|
|
||||||
|
|
||||||
// Add `node_modules/.bin` to PATH
|
|
||||||
process.env.PATH = process.cwd() + '/node_modules/.bin:' + process.env.PATH;
|
|
||||||
|
|
||||||
console.log('Building xterm.js into ' + buildDir);
|
# Clean out/* to prevent confusion if files were deleted in src/
|
||||||
|
rm -rf out/*
|
||||||
|
|
||||||
// Build ES2015 modules into ES5 form, then concatenate them,
|
# Build all TypeScript files (including tests) to out/
|
||||||
// then remove unused require calls and save in output file with source map.
|
tsc
|
||||||
console.log(' - Building ' + buildDir + '/xterm.js...');
|
|
||||||
|
|
||||||
var jsBuildCmd = 'browserify src/xterm.js -s Terminal -t [ babelify --presets [ es2015 ] ] --debug | ';
|
# Concat all xterm.js files into a single file and output as a UMD to $BUILD_DIR/xterm.js
|
||||||
jsBuildCmd += 'derequire | exorcist ' + buildDir + '/xterm.js.map > ' + buildDir + '/xterm.js';
|
browserify ./out/xterm.js --standalone Terminal --debug --outfile ./$BUILD_DIR/xterm.js
|
||||||
|
cat ./$BUILD_DIR/xterm.js | exorcist ./$BUILD_DIR/xterm.js.map -b ./$BUILD_DIR > ./$BUILD_DIR/xterm.temp.js
|
||||||
|
rm ./$BUILD_DIR/xterm.js
|
||||||
|
mv ./$BUILD_DIR/xterm.temp.js ./$BUILD_DIR/xterm.js
|
||||||
|
|
||||||
var jsBuildProcess = child_process.execSync(jsBuildCmd);
|
# Resolve the chain of sourcemaps so that ./$BUILD_DIR/xterm.js.map points at ./src
|
||||||
|
sorcery -i $BUILD_DIR/xterm.js
|
||||||
|
|
||||||
if (jsBuildProcess.status) {
|
# Copy all CSS files from src/ to $BUILD_DIR/
|
||||||
console.log(jsBuildProcess.error);
|
cd src
|
||||||
}
|
find . -name '*.css' | cpio -pdm ../$BUILD_DIR
|
||||||
|
cd ..
|
||||||
|
|
||||||
console.log(' OK.');
|
# Copy addons from out/ to $BUILD_DIR/
|
||||||
|
cd out/addons
|
||||||
// Copy CSS into $BUILD_DIR
|
find . -name '*.js' | cpio -pdm ../../$BUILD_DIR/addons
|
||||||
console.log(' - Building ' + buildDir + '/xterm.css...');
|
cd ../..
|
||||||
|
|
||||||
fs.createReadStream('src/xterm.css').pipe(
|
|
||||||
fs.createWriteStream(buildDir + '/xterm.css')
|
|
||||||
);
|
|
||||||
console.log(' OK.');
|
|
||||||
|
|||||||
+6
-2
@@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm.js",
|
"name": "xterm.js",
|
||||||
"version": "2.0.0",
|
"version": "2.2.0",
|
||||||
"ignore": ["demo", "test", ".gitignore"]
|
"ignore": ["demo", "test", ".gitignore"],
|
||||||
|
"main": [
|
||||||
|
"dist/xterm.js",
|
||||||
|
"dist/xterm.css"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ var terminals = {},
|
|||||||
logs = {};
|
logs = {};
|
||||||
|
|
||||||
app.use('/build', express.static(__dirname + '/../build'));
|
app.use('/build', express.static(__dirname + '/../build'));
|
||||||
app.use('/addons', express.static(__dirname + '/../addons'));
|
|
||||||
|
|
||||||
app.get('/', function(req, res){
|
app.get('/', function(req, res){
|
||||||
res.sendFile(__dirname + '/index.html');
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
|||||||
+6
-6
@@ -2,14 +2,14 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>xterm.js demo</title>
|
<title>xterm.js demo</title>
|
||||||
<link rel="stylesheet" href="../build/xterm.css" />
|
<link rel="stylesheet" href="/build/xterm.css" />
|
||||||
<link rel="stylesheet" href="../addons/fullscreen/fullscreen.css" />
|
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||||
<script src="../build/xterm.js" ></script>
|
<script src="/build/xterm.js" ></script>
|
||||||
<script src="../addons/attach/attach.js" ></script>
|
<script src="/build/addons/attach/attach.js" ></script>
|
||||||
<script src="../addons/fit/fit.js" ></script>
|
<script src="/build/addons/fit/fit.js" ></script>
|
||||||
<script src="../addons/fullscreen/fullscreen.js" ></script>
|
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>xterm.js: xterm, in the browser</h1>
|
<h1>xterm.js: xterm, in the browser</h1>
|
||||||
|
|||||||
Vendored
+2206
File diff suppressed because it is too large
Load Diff
Vendored
+5157
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
+6
-7
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"source": {
|
"source": {
|
||||||
"include": [
|
"include": [
|
||||||
"src/xterm.js",
|
"src/"
|
||||||
"src/handlers/Clipboard.js",
|
],
|
||||||
"addons/attach/attach.js",
|
"exclude": [
|
||||||
"addons/fit/fit.js",
|
"src/test/"
|
||||||
"addons/fullscreen/fullscreen.js",
|
],
|
||||||
"addons/linkify/linkify.js"
|
"excludePattern": "src/.+\\.test\\.(js|ts)$"
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"opts": {
|
"opts": {
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
|
|||||||
+10
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"version": "2.0.0",
|
"version": "2.2.0",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"demo",
|
"demo",
|
||||||
"test",
|
"test",
|
||||||
@@ -10,26 +10,27 @@
|
|||||||
"repository": "https://github.com/sourcelair/xterm.js",
|
"repository": "https://github.com/sourcelair/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "6.14.0",
|
"@types/chai": "^3.4.34",
|
||||||
"babel-preset-es2015": "6.14.0",
|
"@types/mocha": "^2.2.33",
|
||||||
"babelify": "^7.3.0",
|
"@types/node": "^6.0.41",
|
||||||
"browserify": "^13.1.0",
|
"browserify": "^13.1.0",
|
||||||
"chai": "3.5.0",
|
"chai": "3.5.0",
|
||||||
"derequire": "^2.0.3",
|
|
||||||
"docdash": "0.4.0",
|
"docdash": "0.4.0",
|
||||||
"exorcist": "^0.4.0",
|
"exorcist": "^0.4.0",
|
||||||
"express": "4.13.4",
|
"express": "4.13.4",
|
||||||
"express-ws": "2.0.0-rc.1",
|
"express-ws": "2.0.0-rc.1",
|
||||||
"glob": "^7.0.5",
|
"glob": "^7.0.5",
|
||||||
"jsdoc": "3.4.0",
|
"jsdoc": "3.4.3",
|
||||||
"mocha": "2.5.3",
|
"mocha": "2.5.3",
|
||||||
"nodemon": "1.10.2",
|
"nodemon": "1.10.2",
|
||||||
"pty.js": "0.3.1",
|
"pty.js": "0.3.1",
|
||||||
"sleep": "^3.0.1"
|
"sleep": "^3.0.1",
|
||||||
|
"sorcery": "^0.10.0",
|
||||||
|
"typescript": "^2.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon --watch src --watch addons --exec bash -c './bin/build && node demo/app'",
|
"start": "nodemon --watch src --watch addons --watch demo --exec bash -c './bin/build && node demo/app'",
|
||||||
"test": "mocha --recursive --compilers js:babel-core/register",
|
"test": "./bin/build && mocha --recursive ./out",
|
||||||
"build:docs": "jsdoc -c jsdoc.json",
|
"build:docs": "jsdoc -c jsdoc.json",
|
||||||
"build": "./bin/build"
|
"build": "./bin/build"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
/**
|
|
||||||
* xterm.js: xterm, in the browser
|
|
||||||
* Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(dontRecurse) {
|
|
||||||
if (!this.isComposing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var cursor = this.terminal.element.querySelector('.terminal-cursor');
|
|
||||||
if (cursor) {
|
|
||||||
this.compositionView.style.left = cursor.offsetLeft + 'px';
|
|
||||||
this.compositionView.style.top = cursor.offsetTop + 'px';
|
|
||||||
var compositionViewBounds = this.compositionView.getBoundingClientRect();
|
|
||||||
this.textarea.style.left = cursor.offsetLeft + compositionViewBounds.width + 'px';
|
|
||||||
this.textarea.style.top = (cursor.offsetTop + cursor.offsetHeight) + 'px';
|
|
||||||
}
|
|
||||||
if (!dontRecurse) {
|
|
||||||
setTimeout(this.updateCompositionElements.bind(this, true), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the textarea's position so that the cursor does not blink on IE.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
CompositionHelper.prototype.clearTextareaPosition = function() {
|
|
||||||
this.textarea.style.left = '';
|
|
||||||
this.textarea.style.top = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export { CompositionHelper };
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
var assert = require('chai').assert;
|
import { assert } from 'chai';
|
||||||
var Terminal = require('../src/xterm');
|
import { CompositionHelper } from './CompositionHelper';
|
||||||
|
|
||||||
describe('CompositionHelper', function () {
|
describe('CompositionHelper', () => {
|
||||||
var terminal;
|
var terminal;
|
||||||
var compositionHelper;
|
var compositionHelper;
|
||||||
var compositionView;
|
var compositionView;
|
||||||
var textarea;
|
var textarea;
|
||||||
var handledText;
|
var handledText;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
compositionView = {
|
compositionView = {
|
||||||
classList: {
|
classList: {
|
||||||
add: function () {},
|
add: () => {},
|
||||||
remove: function () {},
|
remove: () => {},
|
||||||
},
|
},
|
||||||
getBoundingClientRect: function () {
|
getBoundingClientRect: () => {
|
||||||
return { width: 0 };
|
return { width: 0 };
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
@@ -32,7 +32,7 @@ describe('CompositionHelper', function () {
|
|||||||
};
|
};
|
||||||
terminal = {
|
terminal = {
|
||||||
element: {
|
element: {
|
||||||
querySelector: function () {
|
querySelector: () => {
|
||||||
return { offsetLeft: 0, offsetTop: 0 };
|
return { offsetLeft: 0, offsetTop: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -41,53 +41,26 @@ describe('CompositionHelper', function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
handledText = '';
|
handledText = '';
|
||||||
compositionHelper = new Terminal.CompositionHelper(textarea, compositionView, terminal);
|
compositionHelper = new CompositionHelper(textarea, compositionView, terminal);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Public API', function () {
|
describe('Input', () => {
|
||||||
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) {
|
it('Should insert simple characters', function (done) {
|
||||||
// First character 'ㅇ'
|
// First character 'ㅇ'
|
||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||||
textarea.value = 'ㅇ';
|
textarea.value = 'ㅇ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, 'ㅇ');
|
assert.equal(handledText, 'ㅇ');
|
||||||
// Second character 'ㅇ'
|
// Second character 'ㅇ'
|
||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||||
textarea.value = 'ㅇㅇ';
|
textarea.value = 'ㅇㅇ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, 'ㅇㅇ');
|
assert.equal(handledText, 'ㅇㅇ');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -101,29 +74,29 @@ describe('CompositionHelper', function () {
|
|||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||||
textarea.value = 'ㅇ';
|
textarea.value = 'ㅇ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: '아' });
|
compositionHelper.compositionupdate({ data: '아' });
|
||||||
textarea.value = '아';
|
textarea.value = '아';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: '앙' });
|
compositionHelper.compositionupdate({ data: '앙' });
|
||||||
textarea.value = '앙';
|
textarea.value = '앙';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, '앙');
|
assert.equal(handledText, '앙');
|
||||||
// Second character '앙'
|
// Second character '앙'
|
||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||||
textarea.value = '앙ㅇ';
|
textarea.value = '앙ㅇ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: '아' });
|
compositionHelper.compositionupdate({ data: '아' });
|
||||||
textarea.value = '앙아';
|
textarea.value = '앙아';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: '앙' });
|
compositionHelper.compositionupdate({ data: '앙' });
|
||||||
textarea.value = '앙앙';
|
textarea.value = '앙앙';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, '앙앙');
|
assert.equal(handledText, '앙앙');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -141,21 +114,21 @@ describe('CompositionHelper', function () {
|
|||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||||
textarea.value = 'ㅇ';
|
textarea.value = 'ㅇ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: '아' });
|
compositionHelper.compositionupdate({ data: '아' });
|
||||||
textarea.value = '아';
|
textarea.value = '아';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
// Start second character '아' in first character
|
// Start second character '아' in first character
|
||||||
compositionHelper.compositionupdate({ data: '앙' });
|
compositionHelper.compositionupdate({ data: '앙' });
|
||||||
textarea.value = '앙';
|
textarea.value = '앙';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: '아' });
|
compositionHelper.compositionupdate({ data: '아' });
|
||||||
textarea.value = '아아'
|
textarea.value = '아아'
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, '아아');
|
assert.equal(handledText, '아아');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -170,16 +143,16 @@ describe('CompositionHelper', function () {
|
|||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'd' });
|
compositionHelper.compositionupdate({ data: 'd' });
|
||||||
textarea.value = 'd';
|
textarea.value = 'd';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: 'だ' });
|
compositionHelper.compositionupdate({ data: 'だ' });
|
||||||
textarea.value = 'だ';
|
textarea.value = 'だ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
// Second character 'あ'
|
// Second character 'あ'
|
||||||
compositionHelper.compositionupdate({ data: 'だあ' });
|
compositionHelper.compositionupdate({ data: 'だあ' });
|
||||||
textarea.value = 'だあ';
|
textarea.value = 'だあ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, 'だあ');
|
assert.equal(handledText, 'だあ');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -193,20 +166,20 @@ describe('CompositionHelper', function () {
|
|||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'd' });
|
compositionHelper.compositionupdate({ data: 'd' });
|
||||||
textarea.value = 'd';
|
textarea.value = 'd';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: 'だ' });
|
compositionHelper.compositionupdate({ data: 'だ' });
|
||||||
textarea.value = 'だ';
|
textarea.value = 'だ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
// Second character 'ー'
|
// Second character 'ー'
|
||||||
compositionHelper.compositionupdate({ data: 'だー' });
|
compositionHelper.compositionupdate({ data: 'だー' });
|
||||||
textarea.value = 'だー';
|
textarea.value = 'だー';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
// Convert to katakana 'ダー'
|
// Convert to katakana 'ダー'
|
||||||
compositionHelper.compositionupdate({ data: 'ダー' });
|
compositionHelper.compositionupdate({ data: 'ダー' });
|
||||||
textarea.value = 'ダー';
|
textarea.value = 'ダー';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, 'ダー');
|
assert.equal(handledText, 'ダー');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -221,20 +194,20 @@ describe('CompositionHelper', function () {
|
|||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'い' });
|
compositionHelper.compositionupdate({ data: 'い' });
|
||||||
textarea.value = 'い';
|
textarea.value = 'い';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
// Second character 'ま'
|
// Second character 'ま'
|
||||||
compositionHelper.compositionupdate({ data: 'いm' });
|
compositionHelper.compositionupdate({ data: 'いm' });
|
||||||
textarea.value = 'いm';
|
textarea.value = 'いm';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionupdate({ data: 'いま' });
|
compositionHelper.compositionupdate({ data: 'いま' });
|
||||||
textarea.value = 'いま';
|
textarea.value = 'いま';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
// Convert to kanji '今'
|
// Convert to kanji '今'
|
||||||
compositionHelper.compositionupdate({ data: '今' });
|
compositionHelper.compositionupdate({ data: '今' });
|
||||||
textarea.value = '今';
|
textarea.value = '今';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, '今');
|
assert.equal(handledText, '今');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -249,11 +222,11 @@ describe('CompositionHelper', function () {
|
|||||||
compositionHelper.compositionstart();
|
compositionHelper.compositionstart();
|
||||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||||
textarea.value = 'ㅇ';
|
textarea.value = 'ㅇ';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
compositionHelper.compositionend();
|
compositionHelper.compositionend();
|
||||||
// Second character '1' (a non-composition character)
|
// Second character '1' (a non-composition character)
|
||||||
textarea.value = 'ㅇ1';
|
textarea.value = 'ㅇ1';
|
||||||
setTimeout(function() { // wait for any textarea updates
|
setTimeout(() => { // wait for any textarea updates
|
||||||
assert.equal(handledText, 'ㅇ1');
|
assert.equal(handledText, 'ㅇ1');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
|
||||||
|
interface IPosition {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export class CompositionHelper {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private isComposing: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position within the input textarea's value of the current composition.
|
||||||
|
*/
|
||||||
|
private compositionPosition: IPosition;;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||||
|
* in-progress composition.
|
||||||
|
*/
|
||||||
|
private isSendingComposition: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new CompositionHelper.
|
||||||
|
* @param textarea The textarea that xterm uses for input.
|
||||||
|
* @param compositionView The element to display the in-progress composition in.
|
||||||
|
* @param terminal The Terminal to forward the finished composition to.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private textarea: HTMLTextAreaElement,
|
||||||
|
private compositionView: HTMLElement,
|
||||||
|
private terminal: ITerminal
|
||||||
|
) {
|
||||||
|
this.isComposing = false;
|
||||||
|
this.isSendingComposition = false;
|
||||||
|
this.compositionPosition = { start: null, end: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the compositionstart event, activating the composition view.
|
||||||
|
*/
|
||||||
|
public compositionstart() {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public compositionupdate(ev: CompositionEvent) {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public compositionend() {
|
||||||
|
this.finalizeComposition(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the keydown event, routing any necessary events to the CompositionHelper functions.
|
||||||
|
* @param ev The keydown event.
|
||||||
|
* @return Whether the Terminal should continue processing the keydown event.
|
||||||
|
*/
|
||||||
|
public keydown(ev: KeyboardEvent) {
|
||||||
|
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 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.
|
||||||
|
*/
|
||||||
|
private finalizeComposition(waitForPropogation: boolean) {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
private handleAnyTextareaChanges() {
|
||||||
|
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).
|
||||||
|
* @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is
|
||||||
|
* necessary as the IME events across browsers are not consistently triggered.
|
||||||
|
*/
|
||||||
|
public updateCompositionElements(dontRecurse?: boolean) {
|
||||||
|
if (!this.isComposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
|
||||||
|
if (cursor) {
|
||||||
|
// Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
|
||||||
|
// the .xterm element.
|
||||||
|
var xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
|
||||||
|
var cursorTop = xtermRows.offsetTop + cursor.offsetTop;
|
||||||
|
|
||||||
|
this.compositionView.style.left = cursor.offsetLeft + 'px';
|
||||||
|
this.compositionView.style.top = cursorTop + 'px';
|
||||||
|
this.compositionView.style.height = cursor.offsetHeight + 'px';
|
||||||
|
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
|
||||||
|
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||||
|
// text is.
|
||||||
|
var compositionViewBounds = this.compositionView.getBoundingClientRect();
|
||||||
|
this.textarea.style.left = cursor.offsetLeft + 'px';
|
||||||
|
this.textarea.style.top = cursorTop + 'px';
|
||||||
|
this.textarea.style.width = compositionViewBounds.width + 'px';
|
||||||
|
this.textarea.style.height = compositionViewBounds.height + 'px';
|
||||||
|
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||||
|
}
|
||||||
|
if (!dontRecurse) {
|
||||||
|
setTimeout(this.updateCompositionElements.bind(this, true), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the textarea's position so that the cursor does not blink on IE.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private clearTextareaPosition() {
|
||||||
|
this.textarea.style.left = '';
|
||||||
|
this.textarea.style.top = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* xterm.js: xterm, in the browser
|
* @license MIT
|
||||||
* Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function EventEmitter() {
|
function EventEmitter() {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IBrowser {
|
||||||
|
isNode: boolean;
|
||||||
|
userAgent: string;
|
||||||
|
platform: string;
|
||||||
|
isFirefox: boolean;
|
||||||
|
isMSIE: boolean;
|
||||||
|
isMac: boolean;
|
||||||
|
isIpad: boolean;
|
||||||
|
isIphone: boolean;
|
||||||
|
isMSWindows: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITerminal {
|
||||||
|
element: HTMLElement;
|
||||||
|
rowContainer: HTMLElement;
|
||||||
|
textarea: HTMLTextAreaElement;
|
||||||
|
ydisp: number;
|
||||||
|
lines: string[];
|
||||||
|
rows: number;
|
||||||
|
browser: IBrowser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit the 'data' event and populate the given data.
|
||||||
|
* @param data The data to populate in the event.
|
||||||
|
*/
|
||||||
|
handler(data: string);
|
||||||
|
on(event: string, callback: () => void);
|
||||||
|
scrollDisp(disp: number, suppressScrollEvent: boolean);
|
||||||
|
cancel(ev: Event, force?: boolean);
|
||||||
|
}
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
/**
|
|
||||||
* xterm.js: xterm, in the browser
|
|
||||||
* Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
|
||||||
* Logic for the virtual scroll bar is included in this object.
|
|
||||||
* @param {Terminal} terminal The Terminal object.
|
|
||||||
* @param {HTMLElement} viewportElement The DOM element acting as the viewport
|
|
||||||
* @param {HTMLElement} charMeasureElement A DOM element used to measure the character size of
|
|
||||||
* the terminal.
|
|
||||||
*/
|
|
||||||
function Viewport(terminal, viewportElement, scrollArea, charMeasureElement) {
|
|
||||||
this.terminal = terminal;
|
|
||||||
this.viewportElement = viewportElement;
|
|
||||||
this.scrollArea = scrollArea;
|
|
||||||
this.charMeasureElement = charMeasureElement;
|
|
||||||
this.currentRowHeight = 0;
|
|
||||||
this.lastRecordedBufferLength = 0;
|
|
||||||
this.lastRecordedViewportHeight = 0;
|
|
||||||
|
|
||||||
this.terminal.on('scroll', this.syncScrollArea.bind(this));
|
|
||||||
this.terminal.on('resize', this.syncScrollArea.bind(this));
|
|
||||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
|
||||||
|
|
||||||
this.syncScrollArea();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
|
||||||
* necessary.
|
|
||||||
* @param {number|undefined} charSize A character size measurement bounding rect object, if it
|
|
||||||
* doesn't exist it will be created.
|
|
||||||
*/
|
|
||||||
Viewport.prototype.refresh = function(charSize) {
|
|
||||||
var size = charSize || this.charMeasureElement.getBoundingClientRect();
|
|
||||||
if (size.height > 0) {
|
|
||||||
var rowHeightChanged = size.height !== this.currentRowHeight;
|
|
||||||
if (rowHeightChanged) {
|
|
||||||
this.currentRowHeight = size.height;
|
|
||||||
this.viewportElement.style.lineHeight = size.height + 'px';
|
|
||||||
this.terminal.rowContainer.style.lineHeight = size.height + 'px';
|
|
||||||
}
|
|
||||||
var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
|
|
||||||
if (rowHeightChanged || viewportHeightChanged) {
|
|
||||||
this.lastRecordedViewportHeight = this.terminal.rows;
|
|
||||||
this.viewportElement.style.height = size.height * this.terminal.rows + 'px';
|
|
||||||
}
|
|
||||||
this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
|
||||||
*/
|
|
||||||
Viewport.prototype.syncScrollArea = function() {
|
|
||||||
if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
|
|
||||||
// If buffer height changed
|
|
||||||
this.lastRecordedBufferLength = this.terminal.lines.length;
|
|
||||||
this.refresh();
|
|
||||||
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
|
|
||||||
// If viewport height changed
|
|
||||||
this.refresh();
|
|
||||||
} else {
|
|
||||||
// If size has changed, refresh viewport
|
|
||||||
var size = this.charMeasureElement.getBoundingClientRect();
|
|
||||||
if (size.height !== this.currentRowHeight) {
|
|
||||||
this.refresh(size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync scrollTop
|
|
||||||
var scrollTop = this.terminal.ydisp * this.currentRowHeight;
|
|
||||||
if (this.viewportElement.scrollTop !== scrollTop) {
|
|
||||||
this.viewportElement.scrollTop = scrollTop;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles scroll events on the viewport, calculating the new viewport and requesting the
|
|
||||||
* terminal to scroll to it.
|
|
||||||
* @param {Event} ev The scroll event.
|
|
||||||
*/
|
|
||||||
Viewport.prototype.onScroll = function(ev) {
|
|
||||||
var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
|
||||||
var diff = newRow - this.terminal.ydisp;
|
|
||||||
this.terminal.scrollDisp(diff, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
|
|
||||||
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
|
|
||||||
* `Viewport`.
|
|
||||||
* @param {WheelEvent} ev The mouse wheel event.
|
|
||||||
*/
|
|
||||||
Viewport.prototype.onWheel = function(ev) {
|
|
||||||
if (ev.deltaY === 0) {
|
|
||||||
// Do nothing if it's not a vertical scroll event
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
|
||||||
var multiplier = 1;
|
|
||||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
|
||||||
multiplier = this.currentRowHeight;
|
|
||||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
||||||
multiplier = this.currentRowHeight * this.terminal.rows;
|
|
||||||
}
|
|
||||||
this.viewportElement.scrollTop += ev.deltaY * multiplier;
|
|
||||||
// Prevent the page from scrolling when the terminal scrolls
|
|
||||||
ev.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Viewport };
|
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
var assert = require('chai').assert;
|
import { assert } from 'chai';
|
||||||
var Terminal = require('../src/xterm');
|
import { Viewport } from './Viewport';
|
||||||
|
|
||||||
describe('Viewport', function () {
|
describe('Viewport', () => {
|
||||||
var terminal;
|
var terminal;
|
||||||
var viewportElement;
|
var viewportElement;
|
||||||
var charMeasureElement;
|
var charMeasureElement;
|
||||||
var viewport;
|
var viewport;
|
||||||
var scrollAreaElement;
|
var scrollAreaElement;
|
||||||
|
|
||||||
var CHARACTER_HEIGHT = 10;
|
const CHARACTER_HEIGHT = 10;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
terminal = {
|
terminal = {
|
||||||
lines: [],
|
lines: [],
|
||||||
rows: 0,
|
rows: 0,
|
||||||
ydisp: 0,
|
ydisp: 0,
|
||||||
on: function () {},
|
on: () => {},
|
||||||
rowContainer: {
|
rowContainer: {
|
||||||
style: {
|
style: {
|
||||||
lineHeight: 0
|
lineHeight: 0
|
||||||
@@ -23,7 +23,7 @@ describe('Viewport', function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
viewportElement = {
|
viewportElement = {
|
||||||
addEventListener: function () {},
|
addEventListener: () => {},
|
||||||
style: {
|
style: {
|
||||||
height: 0,
|
height: 0,
|
||||||
lineHeight: 0
|
lineHeight: 0
|
||||||
@@ -35,37 +35,31 @@ describe('Viewport', function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
charMeasureElement = {
|
charMeasureElement = {
|
||||||
getBoundingClientRect: function () {
|
getBoundingClientRect: () => {
|
||||||
return { width: null, height: CHARACTER_HEIGHT };
|
return { width: null, height: CHARACTER_HEIGHT };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
viewport = new Terminal.Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement);
|
viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Public API', function () {
|
describe('refresh', () => {
|
||||||
it('should define Viewport.prototype.onWheel', function () {
|
it('should set the line-height of the terminal', () => {
|
||||||
assert.isDefined(Terminal.Viewport.prototype.onWheel);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('refresh', function () {
|
|
||||||
it('should set the line-height of the terminal', function () {
|
|
||||||
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||||
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||||
charMeasureElement.getBoundingClientRect = function () {
|
charMeasureElement.getBoundingClientRect = () => {
|
||||||
return { width: null, height: 1 };
|
return { width: null, height: 1 };
|
||||||
};
|
};
|
||||||
viewport.refresh();
|
viewport.refresh();
|
||||||
assert.equal(viewportElement.style.lineHeight, '1px');
|
assert.equal(viewportElement.style.lineHeight, '1px');
|
||||||
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
||||||
});
|
});
|
||||||
it('should set the height of the viewport when the line-height changed', function () {
|
it('should set the height of the viewport when the line-height changed', () => {
|
||||||
terminal.lines.push('');
|
terminal.lines.push('');
|
||||||
terminal.lines.push('');
|
terminal.lines.push('');
|
||||||
terminal.rows = 1;
|
terminal.rows = 1;
|
||||||
viewport.refresh();
|
viewport.refresh();
|
||||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||||
charMeasureElement.getBoundingClientRect = function () {
|
charMeasureElement.getBoundingClientRect = () => {
|
||||||
return { width: null, height: 20 };
|
return { width: null, height: 20 };
|
||||||
};
|
};
|
||||||
viewport.refresh();
|
viewport.refresh();
|
||||||
@@ -73,8 +67,8 @@ describe('Viewport', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('syncScrollArea', function () {
|
describe('syncScrollArea', () => {
|
||||||
it('should sync the scroll area', function () {
|
it('should sync the scroll area', () => {
|
||||||
terminal.lines.push('');
|
terminal.lines.push('');
|
||||||
terminal.rows = 1;
|
terminal.rows = 1;
|
||||||
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITerminal } from './Interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
||||||
|
* Logic for the virtual scroll bar is included in this object.
|
||||||
|
*/
|
||||||
|
export class Viewport {
|
||||||
|
private currentRowHeight: number;
|
||||||
|
private lastRecordedBufferLength: number;
|
||||||
|
private lastRecordedViewportHeight: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Viewport.
|
||||||
|
* @param terminal The terminal this viewport belongs to.
|
||||||
|
* @param viewportElement The DOM element acting as the viewport.
|
||||||
|
* @param scrollArea The DOM element acting as the scroll area.
|
||||||
|
* @param charMeasureElement A DOM element used to measure the character size of. the terminal.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private terminal: ITerminal,
|
||||||
|
private viewportElement: HTMLElement,
|
||||||
|
private scrollArea: HTMLElement,
|
||||||
|
private charMeasureElement: HTMLElement
|
||||||
|
) {
|
||||||
|
this.currentRowHeight = 0;
|
||||||
|
this.lastRecordedBufferLength = 0;
|
||||||
|
this.lastRecordedViewportHeight = 0;
|
||||||
|
|
||||||
|
this.terminal.on('scroll', this.syncScrollArea.bind(this));
|
||||||
|
this.terminal.on('resize', this.syncScrollArea.bind(this));
|
||||||
|
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||||
|
|
||||||
|
this.syncScrollArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
||||||
|
* necessary.
|
||||||
|
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
|
||||||
|
* be created.
|
||||||
|
*/
|
||||||
|
private refresh(charSize?: ClientRect): void {
|
||||||
|
var size = charSize || this.charMeasureElement.getBoundingClientRect();
|
||||||
|
if (size.height > 0) {
|
||||||
|
var rowHeightChanged = size.height !== this.currentRowHeight;
|
||||||
|
if (rowHeightChanged) {
|
||||||
|
this.currentRowHeight = size.height;
|
||||||
|
this.viewportElement.style.lineHeight = size.height + 'px';
|
||||||
|
this.terminal.rowContainer.style.lineHeight = size.height + 'px';
|
||||||
|
}
|
||||||
|
var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows;
|
||||||
|
if (rowHeightChanged || viewportHeightChanged) {
|
||||||
|
this.lastRecordedViewportHeight = this.terminal.rows;
|
||||||
|
this.viewportElement.style.height = size.height * this.terminal.rows + 'px';
|
||||||
|
}
|
||||||
|
this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||||
|
*/
|
||||||
|
public syncScrollArea(): void {
|
||||||
|
if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
|
||||||
|
// If buffer height changed
|
||||||
|
this.lastRecordedBufferLength = this.terminal.lines.length;
|
||||||
|
this.refresh();
|
||||||
|
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
|
||||||
|
// If viewport height changed
|
||||||
|
this.refresh();
|
||||||
|
} else {
|
||||||
|
// If size has changed, refresh viewport
|
||||||
|
var size = this.charMeasureElement.getBoundingClientRect();
|
||||||
|
if (size.height !== this.currentRowHeight) {
|
||||||
|
this.refresh(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync scrollTop
|
||||||
|
var scrollTop = this.terminal.ydisp * this.currentRowHeight;
|
||||||
|
if (this.viewportElement.scrollTop !== scrollTop) {
|
||||||
|
this.viewportElement.scrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles scroll events on the viewport, calculating the new viewport and requesting the
|
||||||
|
* terminal to scroll to it.
|
||||||
|
* @param ev The scroll event.
|
||||||
|
*/
|
||||||
|
private onScroll(ev: Event) {
|
||||||
|
var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||||
|
var diff = newRow - this.terminal.ydisp;
|
||||||
|
this.terminal.scrollDisp(diff, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
|
||||||
|
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
|
||||||
|
* `Viewport`.
|
||||||
|
* @param ev The mouse wheel event.
|
||||||
|
*/
|
||||||
|
public onWheel(ev: WheelEvent) {
|
||||||
|
if (ev.deltaY === 0) {
|
||||||
|
// Do nothing if it's not a vertical scroll event
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||||
|
var multiplier = 1;
|
||||||
|
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||||
|
multiplier = this.currentRowHeight;
|
||||||
|
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||||
|
multiplier = this.currentRowHeight * this.terminal.rows;
|
||||||
|
}
|
||||||
|
this.viewportElement.scrollTop += ev.deltaY * multiplier;
|
||||||
|
// Prevent the page from scrolling when the terminal scrolls
|
||||||
|
ev.preventDefault();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
/*
|
/**
|
||||||
* Implements the attach method, that
|
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||||
* attaches the terminal to a WebSocket stream.
|
* @module xterm/addons/attach/attach
|
||||||
*
|
* @license MIT
|
||||||
* The bidirectional argument indicates, whether the terminal should
|
|
||||||
* send data to the socket as well and is true, by default.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (attach) {
|
(function (attach) {
|
||||||
@@ -11,12 +9,12 @@
|
|||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = attach(require('../../src/xterm'));
|
module.exports = attach(require('../../xterm'));
|
||||||
} else if (typeof define == 'function') {
|
} else if (typeof define == 'function') {
|
||||||
/*
|
/*
|
||||||
* Require.js is available
|
* Require.js is available
|
||||||
*/
|
*/
|
||||||
define(['../../src/xterm'], attach);
|
define(['../../xterm'], attach);
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* Plain browser environment
|
* Plain browser environment
|
||||||
@@ -26,12 +24,6 @@
|
|||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
|
||||||
* This module provides methods for attaching a terminal to a WebSocket
|
|
||||||
* stream.
|
|
||||||
*
|
|
||||||
* @module xterm/addons/attach/attach
|
|
||||||
*/
|
|
||||||
var exports = {};
|
var exports = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
/*
|
/**
|
||||||
* Fit terminal columns and rows to the dimensions of its
|
* Fit terminal columns and rows to the dimensions of its DOM element.
|
||||||
* DOM element.
|
|
||||||
*
|
*
|
||||||
* Approach:
|
* ## Approach
|
||||||
* - Rows: Truncate the division of the terminal parent element height
|
* - Rows: Truncate the division of the terminal parent element height by the terminal row height.
|
||||||
* by the terminal row height
|
|
||||||
*
|
*
|
||||||
* - Columns: Truncate the division of the terminal parent element width by
|
* - Columns: Truncate the division of the terminal parent element width by the terminal character
|
||||||
* the terminal character width (apply display: inline at the
|
* width (apply display: inline at the terminal row and truncate its width with the current
|
||||||
* terminal row and truncate its width with the current number
|
* number of columns).
|
||||||
* of columns)
|
* @module xterm/addons/fit/fit
|
||||||
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function (fit) {
|
(function (fit) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = fit(require('../../src/xterm'));
|
module.exports = fit(require('../../xterm'));
|
||||||
} else if (typeof define == 'function') {
|
} else if (typeof define == 'function') {
|
||||||
/*
|
/*
|
||||||
* Require.js is available
|
* Require.js is available
|
||||||
*/
|
*/
|
||||||
define(['../../src/xterm'], fit);
|
define(['../../xterm'], fit);
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* Plain browser environment
|
* Plain browser environment
|
||||||
@@ -29,11 +29,6 @@
|
|||||||
fit(window.Terminal);
|
fit(window.Terminal);
|
||||||
}
|
}
|
||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
/**
|
|
||||||
* This module provides methods for fitting a terminal's size to a parent container.
|
|
||||||
*
|
|
||||||
* @module xterm/addons/fit/fit
|
|
||||||
*/
|
|
||||||
var exports = {};
|
var exports = {};
|
||||||
|
|
||||||
exports.proposeGeometry = function (term) {
|
exports.proposeGeometry = function (term) {
|
||||||
@@ -1,26 +1,19 @@
|
|||||||
/*
|
/**
|
||||||
* Fullscreen addon for xterm.js
|
* Fullscreen addon for xterm.js
|
||||||
*
|
* @module xterm/addons/fullscreen/fullscreen
|
||||||
* Implements the toggleFullscreen function.
|
* @license MIT
|
||||||
*
|
|
||||||
* If the `fullscreen` argument has been supplied, then
|
|
||||||
* if it is true, the fullscreen mode gets turned on,
|
|
||||||
* if it is false or null, the fullscreen mode gets turned off.
|
|
||||||
*
|
|
||||||
* If the `fullscreen` argument has not been supplied, the
|
|
||||||
* fullscreen mode is being toggled.
|
|
||||||
*/
|
*/
|
||||||
(function (fullscreen) {
|
(function (fullscreen) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = fullscreen(require('../../src/xterm'));
|
module.exports = fullscreen(require('../../xterm'));
|
||||||
} else if (typeof define == 'function') {
|
} else if (typeof define == 'function') {
|
||||||
/*
|
/*
|
||||||
* Require.js is available
|
* Require.js is available
|
||||||
*/
|
*/
|
||||||
define(['../../src/xterm'], fullscreen);
|
define(['../../xterm'], fullscreen);
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* Plain browser environment
|
* Plain browser environment
|
||||||
@@ -30,6 +23,11 @@
|
|||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
var exports = {};
|
var exports = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the given terminal's fullscreen mode.
|
||||||
|
* @param {Xterm} term - The terminal to toggle full screen mode
|
||||||
|
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
||||||
|
*/
|
||||||
exports.toggleFullScreen = function (term, fullscreen) {
|
exports.toggleFullScreen = function (term, fullscreen) {
|
||||||
var fn;
|
var fn;
|
||||||
|
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
|
||||||
|
* @module xterm/addons/linkify/linkify
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
(function (linkify) {
|
(function (linkify) {
|
||||||
if (typeof exports === 'object' && typeof module === 'object') {
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
/*
|
/*
|
||||||
* CommonJS environment
|
* CommonJS environment
|
||||||
*/
|
*/
|
||||||
module.exports = linkify(require('../../src/xterm'));
|
module.exports = linkify(require('../../xterm'));
|
||||||
} else if (typeof define == 'function') {
|
} else if (typeof define == 'function') {
|
||||||
/*
|
/*
|
||||||
* Require.js is available
|
* Require.js is available
|
||||||
*/
|
*/
|
||||||
define(['../../src/xterm'], linkify);
|
define(['../../xterm'], linkify);
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* Plain browser environment
|
* Plain browser environment
|
||||||
@@ -18,12 +24,6 @@
|
|||||||
})(function (Xterm) {
|
})(function (Xterm) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
|
||||||
* This module provides methods for convertings valid URL substrings
|
|
||||||
* into HTML anchor elements (links), inside a terminal view.
|
|
||||||
*
|
|
||||||
* @module xterm/addons/linkify/linkify
|
|
||||||
*/
|
|
||||||
var exports = {},
|
var exports = {},
|
||||||
protocolClause = '(https?:\\/\\/)',
|
protocolClause = '(https?:\\/\\/)',
|
||||||
domainCharacterSet = '[\\da-z\\.-]+',
|
domainCharacterSet = '[\\da-z\\.-]+',
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "xterm.terminado",
|
||||||
|
"main": "terminado.js",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* This module provides methods for attaching a terminal to a terminado WebSocket stream.
|
||||||
|
*
|
||||||
|
* @module xterm/addons/terminado/terminado
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (attach) {
|
||||||
|
if (typeof exports === 'object' && typeof module === 'object') {
|
||||||
|
/*
|
||||||
|
* CommonJS environment
|
||||||
|
*/
|
||||||
|
module.exports = attach(require('../../xterm'));
|
||||||
|
} else if (typeof define == 'function') {
|
||||||
|
/*
|
||||||
|
* Require.js is available
|
||||||
|
*/
|
||||||
|
define(['../../xterm'], attach);
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* Plain browser environment
|
||||||
|
*/
|
||||||
|
attach(window.Terminal);
|
||||||
|
}
|
||||||
|
})(function (Xterm) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the given terminal to the given socket.
|
||||||
|
*
|
||||||
|
* @param {Xterm} term - The terminal to be attached to the given socket.
|
||||||
|
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||||
|
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||||
|
* to the socket as well.
|
||||||
|
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||||
|
* should happen instantly or at a maximum
|
||||||
|
* frequency of 1 rendering per 10ms.
|
||||||
|
*/
|
||||||
|
exports.terminadoAttach = function (term, socket, bidirectional, buffered) {
|
||||||
|
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||||
|
term.socket = socket;
|
||||||
|
|
||||||
|
term._flushBuffer = function () {
|
||||||
|
term.write(term._attachSocketBuffer);
|
||||||
|
term._attachSocketBuffer = null;
|
||||||
|
clearTimeout(term._attachSocketBufferTimer);
|
||||||
|
term._attachSocketBufferTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
term._pushToBuffer = function (data) {
|
||||||
|
if (term._attachSocketBuffer) {
|
||||||
|
term._attachSocketBuffer += data;
|
||||||
|
} else {
|
||||||
|
term._attachSocketBuffer = data;
|
||||||
|
setTimeout(term._flushBuffer, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
term._getMessage = function (ev) {
|
||||||
|
var data = JSON.parse(ev.data)
|
||||||
|
if( data[0] == "stdout" ) {
|
||||||
|
if (buffered) {
|
||||||
|
term._pushToBuffer(data[1]);
|
||||||
|
} else {
|
||||||
|
term.write(data[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
term._sendData = function (data) {
|
||||||
|
socket.send(JSON.stringify(['stdin', data]));
|
||||||
|
};
|
||||||
|
|
||||||
|
term._setSize = function (size) {
|
||||||
|
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.addEventListener('message', term._getMessage);
|
||||||
|
|
||||||
|
if (bidirectional) {
|
||||||
|
term.on('data', term._sendData);
|
||||||
|
}
|
||||||
|
term.on('resize', term._setSize);
|
||||||
|
|
||||||
|
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
|
||||||
|
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the given terminal from the given socket
|
||||||
|
*
|
||||||
|
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||||
|
* @param {WebSocket} socket - The socket from which to detach the current
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
exports.terminadoDetach = function (term, socket) {
|
||||||
|
term.off('data', term._sendData);
|
||||||
|
|
||||||
|
socket = (typeof socket == 'undefined') ? term.socket : socket;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.removeEventListener('message', term._getMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete term.socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the current terminal to the given socket
|
||||||
|
*
|
||||||
|
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||||
|
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||||
|
* to the socket as well.
|
||||||
|
* @param {boolean} buffered - Whether the rendering of incoming data
|
||||||
|
* should happen instantly or at a maximum
|
||||||
|
* frequency of 1 rendering per 10ms.
|
||||||
|
*/
|
||||||
|
Xterm.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
|
||||||
|
return exports.terminadoAttach(this, socket, bidirectional, buffered);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the current terminal from the given socket.
|
||||||
|
*
|
||||||
|
* @param {WebSocket} socket - The socket from which to detach the current
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
Xterm.prototype.terminadoDetach = function (socket) {
|
||||||
|
return exports.terminadoDetach(this, socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
var assert = require('chai').assert;
|
import { assert } from 'chai';
|
||||||
var Terminal = require('../src/xterm');
|
import * as Terminal from '../xterm';
|
||||||
var Clipboard = require('../src/handlers/Clipboard');
|
import * as Clipboard from './Clipboard';
|
||||||
|
|
||||||
|
|
||||||
describe('evaluateCopiedTextProcessing', function () {
|
describe('evaluateCopiedTextProcessing', function () {
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* xterm.js: xterm, in the browser
|
* Clipboard handler module: exports methods for handling all clipboard-related events in the
|
||||||
* Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License)
|
* terminal.
|
||||||
|
* @module xterm/handlers/Clipboard
|
||||||
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
import { ITerminal } from '../Interfaces';
|
||||||
* Clipboard handler module. This module contains methods for handling all
|
|
||||||
* clipboard-related events appropriately in the terminal.
|
// We are extending the ClipboardEvent interface, since TypeScript has not declared the
|
||||||
* @module xterm/handlers/Clipboard
|
// clientX and clientY properties that we use.
|
||||||
*/
|
interface IClipboardEvent extends ClipboardEvent {
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWindow extends Window {
|
||||||
|
clipboardData?: {
|
||||||
|
getData(format: string): string;
|
||||||
|
setData(format: string, data: string);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare var window: IWindow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares text copied from terminal selection, to be saved in the clipboard by:
|
* Prepares text copied from terminal selection, to be saved in the clipboard by:
|
||||||
@@ -16,14 +30,14 @@
|
|||||||
* @param {string} text The copied text that needs processing for storing in clipboard
|
* @param {string} text The copied text that needs processing for storing in clipboard
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function prepareTextForClipboard(text) {
|
export function prepareTextForClipboard(text: string): string {
|
||||||
var space = String.fromCharCode(32),
|
let space = String.fromCharCode(32),
|
||||||
nonBreakingSpace = String.fromCharCode(160),
|
nonBreakingSpace = String.fromCharCode(160),
|
||||||
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
|
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
|
||||||
processedText = text.split('\n').map(function (line) {
|
processedText = text.split('\n').map(function (line) {
|
||||||
// Strip all trailing white spaces and convert all non-breaking spaces
|
// Strip all trailing white spaces and convert all non-breaking spaces
|
||||||
// to regular spaces.
|
// to regular spaces.
|
||||||
var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
|
let processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
|
||||||
|
|
||||||
return processedLine;
|
return processedLine;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
@@ -35,11 +49,18 @@ function prepareTextForClipboard(text) {
|
|||||||
* Binds copy functionality to the given terminal.
|
* Binds copy functionality to the given terminal.
|
||||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
* @param {ClipboardEvent} ev The original copy event to be handled
|
||||||
*/
|
*/
|
||||||
function copyHandler (ev) {
|
export function copyHandler(ev: IClipboardEvent, term: ITerminal) {
|
||||||
var copiedText = window.getSelection().toString(),
|
// We cast `window` to `any` type, because TypeScript has not declared the `clipboardData`
|
||||||
|
// property that we use below for Internet Explorer.
|
||||||
|
let copiedText = window.getSelection().toString(),
|
||||||
text = prepareTextForClipboard(copiedText);
|
text = prepareTextForClipboard(copiedText);
|
||||||
|
|
||||||
ev.clipboardData.setData('text/plain', text);
|
if (term.browser.isMSIE) {
|
||||||
|
window.clipboardData.setData('Text', text);
|
||||||
|
} else {
|
||||||
|
ev.clipboardData.setData('text/plain', text);
|
||||||
|
}
|
||||||
|
|
||||||
ev.preventDefault(); // Prevent or the original text will be copied.
|
ev.preventDefault(); // Prevent or the original text will be copied.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +69,27 @@ function copyHandler (ev) {
|
|||||||
* @param {ClipboardEvent} ev The original paste event to be handled
|
* @param {ClipboardEvent} ev The original paste event to be handled
|
||||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
* @param {Terminal} term The terminal on which to apply the handled paste event
|
||||||
*/
|
*/
|
||||||
function pasteHandler(ev, term) {
|
export function pasteHandler(ev: IClipboardEvent, term: ITerminal) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (ev.clipboardData) {
|
|
||||||
var text = ev.clipboardData.getData('text/plain');
|
let text: string;
|
||||||
|
|
||||||
|
let dispatchPaste = function(text) {
|
||||||
term.handler(text);
|
term.handler(text);
|
||||||
term.textarea.value = '';
|
term.textarea.value = '';
|
||||||
return term.cancel(ev);
|
return term.cancel(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (term.browser.isMSIE) {
|
||||||
|
if (window.clipboardData) {
|
||||||
|
text = window.clipboardData.getData('Text');
|
||||||
|
dispatchPaste(text);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ev.clipboardData) {
|
||||||
|
text = ev.clipboardData.getData('text/plain');
|
||||||
|
dispatchPaste(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,42 +106,46 @@ function pasteHandler(ev, term) {
|
|||||||
* @param {ClipboardEvent} ev The original paste event to be handled
|
* @param {ClipboardEvent} ev The original paste event to be handled
|
||||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
* @param {Terminal} term The terminal on which to apply the handled paste event
|
||||||
*/
|
*/
|
||||||
function rightClickHandler(ev, term) {
|
export function rightClickHandler(ev: IClipboardEvent, term: ITerminal) {
|
||||||
var s = document.getSelection(),
|
let s = document.getSelection(),
|
||||||
sText = prepareTextForClipboard(s.toString()),
|
selectedText = prepareTextForClipboard(s.toString()),
|
||||||
r = s.getRangeAt(0);
|
clickIsOnSelection = false,
|
||||||
|
x = ev.clientX,
|
||||||
var x = ev.clientX,
|
|
||||||
y = ev.clientY;
|
y = ev.clientY;
|
||||||
|
|
||||||
var cr = r.getClientRects(),
|
if (s.rangeCount) {
|
||||||
clickIsOnSelection = false,
|
let r = s.getRangeAt(0),
|
||||||
i, rect;
|
cr = r.getClientRects(),
|
||||||
|
i, rect;
|
||||||
|
|
||||||
for (i=0; i<cr.length; i++) {
|
for (i=0; i<cr.length; i++) {
|
||||||
rect = cr[i];
|
rect = cr[i];
|
||||||
clickIsOnSelection = (
|
clickIsOnSelection = (
|
||||||
(x > rect.left) && (x < rect.right) &&
|
(x > rect.left) && (x < rect.right) &&
|
||||||
(y > rect.top) && (y < rect.bottom)
|
(y > rect.top) && (y < rect.bottom)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (clickIsOnSelection) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
// If we clicked on selection and selection is not a single space,
|
// If we clicked on selection and selection is not a single space,
|
||||||
// then mark the right click as copy-only. We check for the single
|
// then mark the right click as copy-only. We check for the single
|
||||||
// space selection, as this can happen when clicking on an
|
// space selection, as this can happen when clicking on an
|
||||||
// and there is not much pointing in copying a single space.
|
// and there is not much pointing in copying a single space.
|
||||||
// Single space is char
|
if (selectedText.match(/^\s$/) || !selectedText.length) {
|
||||||
if (clickIsOnSelection && (sText !== ' ')) {
|
clickIsOnSelection = false;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bring textarea at the cursor position
|
// Bring textarea at the cursor position
|
||||||
if (!clickIsOnSelection) {
|
if (!clickIsOnSelection) {
|
||||||
term.textarea.style.position = 'fixed';
|
term.textarea.style.position = 'fixed';
|
||||||
term.textarea.style.width = '10px';
|
term.textarea.style.width = '20px';
|
||||||
term.textarea.style.height = '10px';
|
term.textarea.style.height = '20px';
|
||||||
term.textarea.style.left = x + 'px';
|
term.textarea.style.left = (x - 10) + 'px';
|
||||||
term.textarea.style.top = y + 'px';
|
term.textarea.style.top = (y - 10) + 'px';
|
||||||
term.textarea.style.zIndex = 1000;
|
term.textarea.style.zIndex = '1000';
|
||||||
term.textarea.focus();
|
term.textarea.focus();
|
||||||
|
|
||||||
// Reset the terminal textarea's styling
|
// Reset the terminal textarea's styling
|
||||||
@@ -117,10 +156,6 @@ function rightClickHandler(ev, term) {
|
|||||||
term.textarea.style.left = null;
|
term.textarea.style.left = null;
|
||||||
term.textarea.style.top = null;
|
term.textarea.style.top = null;
|
||||||
term.textarea.style.zIndex = null;
|
term.textarea.style.zIndex = null;
|
||||||
}, 1);
|
}, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
prepareTextForClipboard, copyHandler, pasteHandler, rightClickHandler
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
var assert = require('chai').assert;
|
var assert = require('chai').assert;
|
||||||
var Terminal = require('../../src/xterm');
|
var Terminal = require('../../xterm');
|
||||||
var linkify = require('../../addons/linkify/linkify');
|
var linkify = require('../../addons/linkify/linkify');
|
||||||
|
|
||||||
describe('linkify addon', function () {
|
describe('linkify addon', function () {
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
var assert = require('chai').assert;
|
var assert = require('chai').assert;
|
||||||
var Terminal = require('../../src/xterm');
|
var Terminal = require('../../xterm');
|
||||||
|
|
||||||
describe('xterm.js addons', function() {
|
describe('xterm.js addons', function() {
|
||||||
it('should load addons with Terminal.loadAddon', function () {
|
it('should load addons with Terminal.loadAddon', function () {
|
||||||
Terminal.loadAddon('attach');
|
Terminal.loadAddon('attach');
|
||||||
// Test that function was loaded successfully
|
// Test that addon was loaded successfully
|
||||||
assert.equal(typeof Terminal.prototype.attach, 'function');
|
assert.equal(typeof Terminal.prototype.attach, 'function');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,7 +2,7 @@ var glob = require('glob');
|
|||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var pty = require('pty.js');
|
var pty = require('pty.js');
|
||||||
var sleep = require('sleep');
|
var sleep = require('sleep');
|
||||||
var Terminal = require('../src/xterm');
|
var Terminal = require('../xterm');
|
||||||
|
|
||||||
var CONSOLE_LOG = console.log;
|
var CONSOLE_LOG = console.log;
|
||||||
|
|
||||||
@@ -80,9 +80,10 @@ describe('xterm output comparison', function() {
|
|||||||
|
|
||||||
// omit stack trace for escape sequence files
|
// omit stack trace for escape sequence files
|
||||||
Error.stackTraceLimit = 0;
|
Error.stackTraceLimit = 0;
|
||||||
var files = glob.sync('test/escape_sequence_files/*.in');
|
var files = glob.sync('**/escape_sequence_files/*.in');
|
||||||
// only successful tests for now
|
// only successful tests for now
|
||||||
var successful = [0, 2, 6, 12, 13, 18, 20, 22, 27, 28];
|
var successful = [0, 2, 6, 12, 13, 18, 20, 22, 27, 28];
|
||||||
|
console.log(files);
|
||||||
for (var a in successful) {
|
for (var a in successful) {
|
||||||
var i = successful[a];
|
var i = successful[a];
|
||||||
(function(filename){
|
(function(filename){
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user