Merge remote-tracking branch 'ups/master' into 591_element_on_validation

This commit is contained in:
Daniel Imms
2017-04-04 09:50:04 -07:00
11 changed files with 305 additions and 180 deletions
+4 -22
View File
@@ -1,4 +1,4 @@
# xterm.js
# [![xterm.js logo](logo.png)](https://xtermjs.org)
[![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
@@ -30,6 +30,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
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.
@@ -121,27 +123,7 @@ The existing releases are available at this GitHub repo's [Releases](https://git
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) before.
The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features).
It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are:
#### [SourceLair](https://www.sourcelair.com)
Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser.
#### [Visual Studio Code](http://code.visualstudio.com/)
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set.
#### [Eclipse Che](http://www.eclipse.org/che)
You can start Eclipse Che with `docker run eclipse/che start`.
#### [Codenvy](http://www.codenvy.io)
You can create a trial account or install an enterprise version with `docker run codenvy/cli start`.
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
## License Agreement
+20 -19
View File
@@ -13,27 +13,28 @@ const ts = require('gulp-typescript');
let buildDir = process.env.BUILD_DIR || 'build';
let tsProject = ts.createProject('tsconfig.json');
let srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
/**
* Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript
* file compiled.
*/
gulp.task('tsc', function () {
// Remove the lib/ directory to prevent confusion if files were deleted in src/
fs.emptyDirSync('lib');
// Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/
fs.emptyDirSync(`${outDir}`);
// Build all TypeScript files (including tests) to lib/, based on the configuration defined in
// Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in
// `tsconfig.json`.
let tsProject = ts.createProject('tsconfig.json');
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject());
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib'));
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir));
// Copy all addons from src/ to lib/
let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons'));
// Copy all addons from ${srcDir}/ to ${outDir}/
let copyAddons = gulp.src(`${srcDir}/addons/**/*`).pipe(gulp.dest(`${outDir}/addons`));
// Copy stylesheets from src/ to lib/
let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib'));
// Copy stylesheets from ${srcDir}/ to ${outDir}/
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
return merge(tsc, copyAddons, copyStylesheets);
});
@@ -49,7 +50,7 @@ gulp.task('browserify', ['tsc'], function() {
let browserifyOptions = {
basedir: buildDir,
debug: true,
entries: ['../lib/xterm.js'],
entries: [`../${outDir}/xterm.js`],
standalone: 'Terminal',
cache: {},
packageCache: {}
@@ -62,17 +63,17 @@ gulp.task('browserify', ['tsc'], function() {
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
// Copy all add-ons from lib/ to buildDir
let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`));
// Copy all add-ons from ${outDir}/ to buildDir
let copyAddons = gulp.src(`${outDir}/addons/**/*`).pipe(gulp.dest(`${buildDir}/addons`));
// Copy stylesheets from src/ to lib/
let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir));
// Copy stylesheets from ${outDir}/ to ${buildDir}/
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir));
return merge(bundleStream, copyAddons, copyStylesheets);
});
gulp.task('instrument-test', function () {
return gulp.src(['lib/**/*.js'])
return gulp.src([`${outDir}/**/*.js`])
// Covering files
.pipe(istanbul())
// Force `require` to return covered files
@@ -80,7 +81,7 @@ gulp.task('instrument-test', function () {
});
gulp.task('mocha', ['instrument-test'], function () {
return gulp.src(['lib/*test.js', 'lib/**/*test.js'], {read: false})
return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false})
.pipe(mocha())
.pipe(istanbul.writeReports());
});
@@ -88,11 +89,11 @@ gulp.task('mocha', ['instrument-test'], function () {
/**
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
* (Without this task the source maps produced for the JavaScript bundle points into the
* compiled JavaScript files in lib/).
* compiled JavaScript files in ${outDir}/).
*/
gulp.task('sorcery', ['browserify'], function () {
var chain = sorcery.loadSync(`${buildDir}/xterm.js`);
var map = chain.apply();
chain.apply();
chain.writeSync();
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

+1 -1
View File
@@ -68,7 +68,7 @@
"scripts": {
"prestart": "npm run build",
"start": "node demo/app",
"dev": "nodemon -e js,ts --watch src --watch demo --exec npm start",
"dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start",
"lint": "tslint src/*.ts src/**/*.ts",
"test": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
+139 -51
View File
@@ -8,9 +8,9 @@ import { Linkifier } from './Linkifier';
import { LinkMatcher } from './Types';
class TestLinkifier extends Linkifier {
constructor(document: Document, rows: HTMLElement[]) {
constructor() {
Linkifier.TIME_BEFORE_LINKIFY = 0;
super(document, rows);
super();
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
@@ -25,75 +25,163 @@ describe('Linkifier', () => {
let linkifier: TestLinkifier;
beforeEach(done => {
rows = [];
jsdom.env('', (err, w) => {
window = w;
document = window.document;
linkifier = new TestLinkifier(document, rows);
container = document.createElement('div');
document.body.appendChild(container);
linkifier = new TestLinkifier();
done();
});
});
function addRow(text: string) {
function addRow(html: string) {
const element = document.createElement('div');
element.textContent = text;
element.innerHTML = html;
container.appendChild(element);
rows.push(element);
}
function clickElement(element: Node) {
const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe('validationCallback', () => {
it('should enable link if true', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (url, element, cb) => {
cb(true);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
}
describe('before attachToDom', () => {
it('should allow link matcher registration', done => {
assert.doesNotThrow(() => {
const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {});
assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId));
done();
});
linkifier.linkifyRow(0);
});
it('should disable link if false', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
cb(false);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
}
});
linkifier.linkifyRow(0);
// Allow time for the click to be performed
setTimeout(() => done(), 10);
});
});
describe('priority', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
describe('after attachToDom', () => {
beforeEach(() => {
rows = [];
linkifier.attachToDom(document, rows);
container = document.createElement('div');
document.body.appendChild(container);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
function clickElement(element: Node) {
const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
describe('http links', () => {
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done));
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
describe('link matcher', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRow(0);
// Allow linkify to happen
setTimeout(() => {
assert.equal(rows[0].innerHTML, expectedHtml);
done();
}, 0);
}
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done);
});
it('should match a single link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
});
it('should match a single link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done);
});
it('should match a single link at the end of a text node', done => {
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
});
it('should match a link after a link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done);
});
it('should match a link after a link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done);
});
it('should match a link immediately after a link at the end of a text node', done => {
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done);
});
});
describe('validationCallback', () => {
it('should enable link if true', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (url, element, cb) => {
cb(true);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
}
});
linkifier.linkifyRow(0);
});
it('should disable link if false', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
cb(false);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
}
});
linkifier.linkifyRow(0);
// Allow time for the click to be performed
setTimeout(() => done(), 10);
});
it('should trigger for multiple link matches on one row', done => {
addRow('test test');
let count = 0;
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
count += 1;
if (count === 2) {
done();
}
cb(false);
}
});
linkifier.linkifyRow(0);
});
});
describe('priority', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
});
});
});
});
+79 -50
View File
@@ -16,8 +16,8 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;\\=\\.\\-]*';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
@@ -37,8 +37,8 @@ const HYPERTEXT_LINK_MATCHER_ID = 0;
export class Linkifier {
/**
* The time to wait after a row is changed before it is linkified. This prevents
* the costly operation of searching every row multiple times, pntentially a
* huge aount of times.
* the costly operation of searching every row multiple times, potentially a
* huge amount of times.
*/
protected static TIME_BEFORE_LINKIFY = 200;
@@ -49,19 +49,32 @@ export class Linkifier {
private _rowTimeoutIds: number[];
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
constructor(document: Document, rows: HTMLElement[]) {
this._document = document;
this._rows = rows;
constructor() {
this._rowTimeoutIds = [];
this._linkMatchers = [];
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
* Attaches the linkifier to the DOM, enabling linkification.
* @param document The document object.
* @param rows The array of rows to apply links to.
*/
public attachToDom(document: Document, rows: HTMLElement[]) {
this._document = document;
this._rows = rows;
}
/**
* Queues a row for linkification.
* @param {number} rowIndex The index of the row to linkify.
*/
public linkifyRow(rowIndex: number): void {
// Don't attempt linkify if not yet attached to DOM
if (!this._document) {
return;
}
const timeoutId = this._rowTimeoutIds[rowIndex];
if (timeoutId) {
clearTimeout(timeoutId);
@@ -164,16 +177,18 @@ export class Linkifier {
const text = row.textContent;
for (let i = 0; i < this._linkMatchers.length; i++) {
const matcher = this._linkMatchers[i];
const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex);
if (uri) {
const linkElement = this._doLinkifyRow(rowIndex, uri, matcher.handler, matcher.id === HYPERTEXT_LINK_MATCHER_ID);
const linkElements = this._doLinkifyRow(row, matcher);
if (linkElements.length > 0) {
// Fire validation callback
if (linkElement && matcher.validationCallback) {
matcher.validationCallback(uri, linkElement, isValid => {
if (!isValid) {
linkElement.classList.add(INVALID_LINK_CLASS);
}
});
if (matcher.validationCallback) {
for (let j = 0; j < linkElements.length; j++) {
const element = linkElements[j];
matcher.validationCallback(element.textContent, element, isValid => {
if (!isValid) {
element.classList.add(INVALID_LINK_CLASS);
}
});
}
}
// Only allow a single LinkMatcher to trigger on any given row.
return;
@@ -183,54 +198,61 @@ export class Linkifier {
/**
* Linkifies a row given a specific handler.
* @param {number} rowIndex The index of the row to linkify.
* @param {string} uri The uri that has been found.
* @param {handler} handler The handler to trigger when the link is triggered.
* @param {HTMLElement} row The row to linkify.
* @param {LinkMatcher} matcher The link matcher for this line.
* @return The link element if it was added, otherwise undefined.
*/
private _doLinkifyRow(rowIndex: number, uri: string, handler: LinkMatcherHandler, isHttpLinkMatcher: boolean): HTMLElement {
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
// Iterate over nodes as we want to consider text nodes
const nodes = this._rows[rowIndex].childNodes;
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
const nodes = row.childNodes;
// Find the first match
let match = row.textContent.match(matcher.regex);
if (!match || match.length === 0) {
return result;
}
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
// Set the next searches start index
let rowStartIndex = match.index + uri.length;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const searchIndex = node.textContent.indexOf(uri);
if (searchIndex >= 0) {
const linkElement = this._createAnchorElement(uri, handler, isHttpLinkMatcher);
const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher);
if (node.textContent.length === uri.length) {
// Matches entire string
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
this._replaceNode(node, linkElement);
} else {
const element = (<HTMLElement>node);
if (element.nodeName === 'A') {
// This row has already been linkified
return;
return result;
}
element.innerHTML = '';
element.appendChild(linkElement);
}
} else {
// Matches part of string
this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
// No need to consider the new nodes
i += nodesAdded;
}
return linkElement;
result.push(linkElement);
// Find the next match
match = row.textContent.substring(rowStartIndex).match(matcher.regex);
if (!match || match.length === 0) {
return result;
}
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
rowStartIndex += match.index + uri.length;
}
}
}
/**
* Finds a link match in a piece of text.
* @param {string} text The text to search.
* @param {number} matchIndex The regex match index of the link.
* @return {string} The matching URI or null if not found.
*/
private _findLinkMatch(text: string, regex: RegExp, matchIndex?: number): string {
const match = text.match(regex);
if (!match || match.length === 0) {
return null;
}
return match[typeof matchIndex !== 'number' ? 0 : matchIndex];
return result;
}
/**
@@ -241,6 +263,7 @@ export class Linkifier {
private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement {
const element = this._document.createElement('a');
element.textContent = uri;
element.draggable = false;
if (isHypertextLinkHandler) {
element.href = uri;
// Force link on another tab so work is not lost
@@ -282,8 +305,9 @@ export class Linkifier {
* @param {Node} newNode The new node to insert.
* @param {string} substring The substring to replace.
* @param {number} substringIndex The index of the substring within the string.
* @return The number of nodes to skip when searching for the next uri.
*/
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): void {
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
let node = targetNode;
if (node.nodeType !== 3/*Node.TEXT_NODE*/) {
node = node.childNodes[0];
@@ -292,7 +316,7 @@ export class Linkifier {
// The targetNode will be either a text node or a <span>. The text node
// (targetNode or its only-child) needs to be replaced with newNode plus new
// text nodes potentially on either side.
if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) {
if (node.childNodes.length === 0 && node.nodeType !== 3/*Node.TEXT_NODE*/) {
throw new Error('targetNode must be a text node or only contain a single text node');
}
@@ -303,18 +327,23 @@ export class Linkifier {
const rightText = fullText.substring(substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(node, newNode, rightTextNode);
} else if (substringIndex === targetNode.textContent.length - substring.length) {
return 0;
}
if (substringIndex === targetNode.textContent.length - substring.length) {
// Replace with <textnode><newNode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
this._replaceNode(node, leftTextNode, newNode);
} else {
// Replace with <textnode><newNode><textnode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
const rightText = fullText.substring(substringIndex + substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(node, leftTextNode, newNode, rightTextNode);
return 0;
}
// Replace with <textnode><newNode><textnode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
const rightText = fullText.substring(substringIndex + substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(node, leftTextNode, newNode, rightTextNode);
return 1;
}
}
+10 -8
View File
@@ -34,7 +34,7 @@ export class Renderer {
// Figure out whether boldness affects
// the character width of monospace fonts.
if (brokenBold === null) {
brokenBold = checkBoldBroken((<any>this._terminal).document);
brokenBold = checkBoldBroken((<any>this._terminal).element);
}
// TODO: Pull more DOM interactions into Renderer.constructor, element for
@@ -291,14 +291,16 @@ export class Renderer {
// if bold is broken, we can't
// use it in the terminal.
function checkBoldBroken(document) {
const body = document.getElementsByTagName('body')[0];
function checkBoldBroken(terminal) {
const document = terminal.ownerDocument;
const el = document.createElement('span');
el.innerHTML = 'hello world';
body.appendChild(el);
const w1 = el.scrollWidth;
terminal.appendChild(el);
const w1 = el.offsetWidth;
const h1 = el.offsetHeight;
el.style.fontWeight = 'bold';
const w2 = el.scrollWidth;
body.removeChild(el);
return w1 !== w2;
const w2 = el.offsetWidth;
const h2 = el.offsetHeight;
terminal.removeChild(el);
return w1 !== w2 || h1 !== h2;
}
+1 -1
View File
@@ -56,7 +56,7 @@
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
characterHeight = parseInt(subjectRow.offsetHeight);
characterHeight = subjectRow.getBoundingClientRect().height;
subjectRow.innerHTML = contentBuffer;
rows = parseInt(availableHeight / characterHeight);
+2 -1
View File
@@ -21,7 +21,8 @@ describe('xterm.js', function() {
};
xterm.element = {
classList: {
toggle: function(){}
toggle: function(){},
remove: function(){}
}
};
});
+9 -16
View File
@@ -86,7 +86,7 @@
text-decoration: none;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
.terminal.focus:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
background-color: #fff;
color: #000;
}
@@ -97,16 +97,9 @@
background-color: transparent;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink .terminal-cursor {
animation: xterm-cursor-blink 1.2s infinite step-end;
}
@keyframes xterm-cursor-blink {
0% { }
50% {
background-color: transparent;
color: inherit;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink-on .terminal-cursor {
background-color: transparent;
color: inherit;
}
.terminal.xterm-cursor-style-bar .terminal-cursor,
@@ -132,13 +125,13 @@
right: 0;
height: 1px;
}
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before,
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before {
background-color: transparent;
}
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
animation: xterm-cursor-non-bar-blink 1.2s infinite step-end;
}
@keyframes xterm-cursor-non-bar-blink {
0% { background-color: #fff; }
50% { background-color: transparent; }
background-color: #fff;
}
.terminal .composition-view {
+40 -11
View File
@@ -52,6 +52,13 @@ var WRITE_BUFFER_PAUSE_THRESHOLD = 5;
*/
var WRITE_BATCH_SIZE = 300;
/**
* The time between cursor blinks. This is driven by JS rather than a CSS
* animation due to a bug in Chromium that causes it to use excessive CPU time.
* See https://github.com/Microsoft/vscode/issues/22900
*/
var CURSOR_BLINK_INTERVAL = 600;
/**
* Terminal
*/
@@ -159,6 +166,7 @@ function Terminal(options) {
this.scrollTop = 0;
this.scrollBottom = this.rows - 1;
this.customKeydownHandler = null;
this.cursorBlinkInterval = null;
// modes
this.applicationKeypad = false;
@@ -211,7 +219,7 @@ function Terminal(options) {
this.parser = new Parser(this.inputHandler, this);
// Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
this.renderer = this.renderer || null;
this.linkifier = this.linkifier || null;;
this.linkifier = this.linkifier || new Linkifier();
// user input states
this.writeBuffer = [];
@@ -423,7 +431,7 @@ Terminal.prototype.setOption = function(key, value) {
this[key] = value;
this.options[key] = value;
switch (key) {
case 'cursorBlink': this.element.classList.toggle('xterm-cursor-blink', value); break;
case 'cursorBlink': this.setCursorBlinking(value); break;
case 'cursorStyle':
// Style 'block' applies with no class
this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline');
@@ -433,6 +441,29 @@ Terminal.prototype.setOption = function(key, value) {
}
};
Terminal.prototype.restartCursorBlinking = function () {
this.setCursorBlinking(this.options.cursorBlink);
};
Terminal.prototype.setCursorBlinking = function (enabled) {
this.element.classList.toggle('xterm-cursor-blink', enabled);
this.clearCursorBlinkingInterval();
if (enabled) {
var self = this;
this.cursorBlinkInterval = setInterval(function () {
self.element.classList.toggle('xterm-cursor-blink-on');
}, CURSOR_BLINK_INTERVAL);
}
};
Terminal.prototype.clearCursorBlinkingInterval = function () {
this.element.classList.remove('xterm-cursor-blink-on');
if (this.cursorBlinkInterval) {
clearInterval(this.cursorBlinkInterval);
this.cursorBlinkInterval = null;
}
};
/**
* Binds the desired focus behavior on a given terminal object.
*
@@ -445,6 +476,7 @@ Terminal.bindFocus = function (term) {
}
term.element.classList.add('focus');
term.showCursor();
term.restartCursorBlinking.apply(term);
Terminal.focus = term;
term.emit('focus', {terminal: term});
});
@@ -469,6 +501,7 @@ Terminal.bindBlur = function (term) {
term.send(C0.ESC + '[O');
}
term.element.classList.remove('focus');
term.clearCursorBlinkingInterval.apply(term);
Terminal.focus = null;
term.emit('blur', {terminal: term});
});
@@ -594,7 +627,7 @@ Terminal.prototype.open = function(parent) {
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.classList.add('xterm-theme-' + this.theme);
this.element.classList.toggle('xterm-cursor-blink', this.options.cursorBlink);
this.setCursorBlinking(this.options.cursorBlink);
this.element.style.height
this.element.setAttribute('tabindex', 0);
@@ -612,7 +645,7 @@ Terminal.prototype.open = function(parent) {
this.rowContainer.classList.add('xterm-rows');
this.element.appendChild(this.rowContainer);
this.children = [];
this.linkifier = new Linkifier(document, this.children);
this.linkifier.attachToDom(document, this.children);
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
@@ -1351,6 +1384,8 @@ Terminal.prototype.keyDown = function(ev) {
return false;
}
this.restartCursorBlinking();
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) {
if (this.ybase !== this.ydisp) {
this.scrollToBottom();
@@ -1820,14 +1855,8 @@ Terminal.prototype.resize = function(x, y) {
this.lines.get(i).push(ch);
}
}
} else { // (j > x)
i = this.lines.length;
while (i--) {
while (this.lines.get(i).length > x) {
this.lines.get(i).pop();
}
}
}
this.cols = x;
this.setupStops(this.cols);