mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge remote-tracking branch 'ups/v3' into v3
This commit is contained in:
@@ -97,12 +97,10 @@ xterm.fit();
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
|
||||
|
||||
- Chrome 48+
|
||||
- Edge 13+
|
||||
- Firefox 44+
|
||||
- Internet Explorer 11+
|
||||
- Opera 35+
|
||||
- Safari 8+
|
||||
- Chrome latest
|
||||
- Edge latest
|
||||
- Firefox latest
|
||||
- Safari latest
|
||||
|
||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||
|
||||
@@ -130,6 +128,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
|
||||
- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+24
-25
@@ -2,9 +2,7 @@ var term,
|
||||
protocol,
|
||||
socketURL,
|
||||
socket,
|
||||
pid,
|
||||
charWidth,
|
||||
charHeight;
|
||||
pid;
|
||||
|
||||
var terminalContainer = document.getElementById('terminal-container'),
|
||||
actionElements = {
|
||||
@@ -21,11 +19,13 @@ var terminalContainer = document.getElementById('terminal-container'),
|
||||
colsElement = document.getElementById('cols'),
|
||||
rowsElement = document.getElementById('rows');
|
||||
|
||||
function setTerminalSize () {
|
||||
var cols = parseInt(colsElement.value, 10),
|
||||
rows = parseInt(rowsElement.value, 10),
|
||||
width = (cols * charWidth).toString() + 'px',
|
||||
height = (rows * charHeight).toString() + 'px';
|
||||
function setTerminalSize() {
|
||||
var cols = parseInt(colsElement.value, 10);
|
||||
var rows = parseInt(rowsElement.value, 10);
|
||||
var viewportElement = document.querySelector('.xterm-viewport');
|
||||
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
|
||||
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
|
||||
var height = (rows * term.charMeasure.height).toString() + 'px';
|
||||
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
@@ -92,27 +92,26 @@ function createTerminal() {
|
||||
term.open(terminalContainer);
|
||||
term.fit();
|
||||
|
||||
var initialGeometry = term.proposeGeometry(),
|
||||
cols = initialGeometry.cols,
|
||||
rows = initialGeometry.rows;
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(() => {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
|
||||
colsElement.value = cols;
|
||||
rowsElement.value = rows;
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) {
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
||||
|
||||
charWidth = Math.ceil(term.element.offsetWidth / cols);
|
||||
charHeight = Math.ceil(term.element.offsetHeight / rows);
|
||||
|
||||
res.text().then(function (pid) {
|
||||
window.pid = pid;
|
||||
socketURL += pid;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
res.text().then(function (pid) {
|
||||
window.pid = pid;
|
||||
socketURL += pid;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function runRealTerminal() {
|
||||
|
||||
@@ -14,9 +14,3 @@ h1 {
|
||||
margin: 0 auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#terminal-container .terminal {
|
||||
background-color: #111;
|
||||
color: #fafafa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
@@ -168,6 +168,10 @@ namespace methods_core {
|
||||
t.setOption('bellStyle', 'visual');
|
||||
t.setOption('bellStyle', 'sound');
|
||||
t.setOption('bellStyle', 'both');
|
||||
t.setOption('fontSize', 1);
|
||||
t.setOption('lineHeight', 1);
|
||||
t.setOption('fontFamily', 'foo');
|
||||
t.setOption('theme', {background: '#ff0000'});
|
||||
}
|
||||
}
|
||||
namespace scrolling {
|
||||
@@ -210,9 +214,13 @@ namespace methods_experimental {
|
||||
t.registerLinkMatcher(/foo/, () => true, {
|
||||
matchIndex: 1,
|
||||
priority: 1,
|
||||
validationCallback: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => {
|
||||
console.log(uri, element, callback);
|
||||
}
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => {
|
||||
console.log(uri, callback);
|
||||
},
|
||||
tooltipCallback: (e: MouseEvent, uri: string) => {
|
||||
console.log(e, uri);
|
||||
},
|
||||
leaveCallback: () => {}
|
||||
});
|
||||
t.deregisterLinkMatcher(1);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const buffer = require('vinyl-buffer');
|
||||
const coveralls = require('gulp-coveralls');
|
||||
const fs = require('fs-extra');
|
||||
const gulp = require('gulp');
|
||||
const path = require('path');
|
||||
const istanbul = require('gulp-istanbul');
|
||||
const merge = require('merge-stream');
|
||||
const mocha = require('gulp-mocha');
|
||||
@@ -96,6 +97,7 @@ gulp.task('browserify-addons', ['tsc'], function() {
|
||||
packageCache: {}
|
||||
};
|
||||
let searchBundle = browserify(searchOptions)
|
||||
.external(path.join(outDir, 'Terminal.js'))
|
||||
.bundle()
|
||||
.pipe(source('./addons/search/search.js'))
|
||||
.pipe(buffer())
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@
|
||||
"nodemon": "1.10.2",
|
||||
"sorcery": "^0.10.0",
|
||||
"tslint": "^4.0.2",
|
||||
"typescript": "~2.2.0",
|
||||
"typescript": "~2.4.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
+132
-74
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -6,8 +7,10 @@ import { ITerminal, IBuffer } from './Interfaces';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { LineData, CharData } from './Types';
|
||||
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
@@ -32,8 +35,8 @@ export class Buffer implements IBuffer {
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
* @param _terminal The terminal the Buffer will belong to.
|
||||
* @param _hasScrollback Whether the buffer should respecr the scrollback of
|
||||
* the terminal..
|
||||
* @param _hasScrollback Whether the buffer should respect the scrollback of
|
||||
* the terminal.
|
||||
*/
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
@@ -46,6 +49,16 @@ export class Buffer implements IBuffer {
|
||||
return this._lines;
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
|
||||
}
|
||||
|
||||
public get isCursorInViewport(): boolean {
|
||||
const absoluteY = this.ybase + this.y;
|
||||
const relativeY = absoluteY - this.ydisp;
|
||||
return (relativeY >= 0 && relativeY < this._terminal.rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the correct buffer length based on the rows provided, the terminal's
|
||||
* scrollback and whether this buffer is flagged to have scrollback or not.
|
||||
@@ -78,11 +91,10 @@ export class Buffer implements IBuffer {
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
this.x = 0;
|
||||
this.scrollBottom = 0;
|
||||
this.scrollTop = 0;
|
||||
this.tabs = {};
|
||||
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = this._terminal.rows - 1;
|
||||
this.setupTabStops();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,11 +103,6 @@ export class Buffer implements IBuffer {
|
||||
* @param newRows The new number of rows.
|
||||
*/
|
||||
public resize(newCols: number, newRows: number): void {
|
||||
// Don't resize the buffer if it's empty and hasn't been used yet.
|
||||
if (this._lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increase max length if needed before adjustments to allow space to fill
|
||||
// as required.
|
||||
const newMaxLength = this._getCorrectBufferLength(newRows);
|
||||
@@ -103,83 +110,88 @@ export class Buffer implements IBuffer {
|
||||
this._lines.maxLength = newMaxLength;
|
||||
}
|
||||
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this._lines.length; i++) {
|
||||
// TODO: This should be removed, with tests setup for the case that was
|
||||
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
|
||||
if (this._lines.get(i) === undefined) {
|
||||
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
|
||||
}
|
||||
while (this._lines.get(i).length < newCols) {
|
||||
this._lines.get(i).push(ch);
|
||||
// The following adjustments should only happen if the buffer has been
|
||||
// initialized/filled.
|
||||
if (this._lines.length > 0) {
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this._lines.length; i++) {
|
||||
// TODO: This should be removed, with tests setup for the case that was
|
||||
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
|
||||
if (this._lines.get(i) === undefined) {
|
||||
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
|
||||
}
|
||||
while (this._lines.get(i).length < newCols) {
|
||||
this._lines.get(i).push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize rows in both directions as needed
|
||||
let addToY = 0;
|
||||
if (this._terminal.rows < newRows) {
|
||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
||||
if (this._lines.length < newRows + this.ybase) {
|
||||
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
|
||||
// There is room above the buffer and there are no empty elements below the line,
|
||||
// scroll up
|
||||
this.ybase--;
|
||||
addToY++;
|
||||
if (this.ydisp > 0) {
|
||||
// Viewport is at the top of the buffer, must increase downwards
|
||||
this.ydisp--;
|
||||
// Resize rows in both directions as needed
|
||||
let addToY = 0;
|
||||
if (this._terminal.rows < newRows) {
|
||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
||||
if (this._lines.length < newRows + this.ybase) {
|
||||
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
|
||||
// There is room above the buffer and there are no empty elements below the line,
|
||||
// scroll up
|
||||
this.ybase--;
|
||||
addToY++;
|
||||
if (this.ydisp > 0) {
|
||||
// Viewport is at the top of the buffer, must increase downwards
|
||||
this.ydisp--;
|
||||
}
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // (this._terminal.rows >= newRows)
|
||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
||||
if (this._lines.length > newRows + this.ybase) {
|
||||
if (this._lines.length > this.ybase + this.y + 1) {
|
||||
// The line is a blank line below the cursor, remove it
|
||||
this._lines.pop();
|
||||
} else {
|
||||
// The line is the cursor, scroll down
|
||||
this.ybase++;
|
||||
this.ydisp++;
|
||||
}
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // (this._terminal.rows >= newRows)
|
||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
||||
if (this._lines.length > newRows + this.ybase) {
|
||||
if (this._lines.length > this.ybase + this.y + 1) {
|
||||
// The line is a blank line below the cursor, remove it
|
||||
this._lines.pop();
|
||||
} else {
|
||||
// The line is the cursor, scroll down
|
||||
this.ybase++;
|
||||
this.ydisp++;
|
||||
}
|
||||
|
||||
// Reduce max length if needed after adjustments, this is done after as it
|
||||
// would otherwise cut data from the bottom of the buffer.
|
||||
if (newMaxLength < this._lines.maxLength) {
|
||||
// Trim from the top of the buffer and adjust ybase and ydisp.
|
||||
const amountToTrim = this._lines.length - newMaxLength;
|
||||
if (amountToTrim > 0) {
|
||||
this._lines.trimStart(amountToTrim);
|
||||
this.ybase = Math.max(this.ybase - amountToTrim, 0);
|
||||
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
|
||||
}
|
||||
this._lines.maxLength = newMaxLength;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce max length if needed after adjustments, this is done after as it
|
||||
// would otherwise cut data from the bottom of the buffer.
|
||||
if (newMaxLength < this._lines.maxLength) {
|
||||
// Trim from the top of the buffer and adjust ybase and ydisp.
|
||||
const amountToTrim = this._lines.length - newMaxLength;
|
||||
if (amountToTrim > 0) {
|
||||
this._lines.trimStart(amountToTrim);
|
||||
this.ybase = Math.max(this.ybase - amountToTrim, 0);
|
||||
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
|
||||
// Make sure that the cursor stays on screen
|
||||
if (this.y >= newRows) {
|
||||
this.y = newRows - 1;
|
||||
}
|
||||
this._lines.maxLength = newMaxLength;
|
||||
if (addToY) {
|
||||
this.y += addToY;
|
||||
}
|
||||
|
||||
if (this.x >= newCols) {
|
||||
this.x = newCols - 1;
|
||||
}
|
||||
|
||||
this.scrollTop = 0;
|
||||
}
|
||||
|
||||
// Make sure that the cursor stays on screen
|
||||
if (this.y >= newRows) {
|
||||
this.y = newRows - 1;
|
||||
}
|
||||
if (addToY) {
|
||||
this.y += addToY;
|
||||
}
|
||||
|
||||
if (this.x >= newCols) {
|
||||
this.x = newCols - 1;
|
||||
}
|
||||
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = newRows - 1;
|
||||
}
|
||||
|
||||
@@ -199,6 +211,9 @@ export class Buffer implements IBuffer {
|
||||
let widthAdjustedStartCol = startCol;
|
||||
let widthAdjustedEndCol = endCol;
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
return '';
|
||||
}
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||
@@ -230,4 +245,47 @@ export class Buffer implements IBuffer {
|
||||
|
||||
return lineString.substring(widthAdjustedStartCol, finalEndCol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the tab stops.
|
||||
* @param i The index to start setting up tab stops from.
|
||||
*/
|
||||
public setupTabStops(i?: number): void {
|
||||
if (i != null) {
|
||||
if (!this.tabs[i]) {
|
||||
i = this.prevStop(i);
|
||||
}
|
||||
} else {
|
||||
this.tabs = {};
|
||||
i = 0;
|
||||
}
|
||||
|
||||
for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) {
|
||||
this.tabs[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the cursor to the previous tab stop from the given position (default is current).
|
||||
* @param x The position to move the cursor to the previous tab stop.
|
||||
*/
|
||||
public prevStop(x?: number): number {
|
||||
if (x == null) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[--x] && x > 0);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the cursor one tab stop forward from the given position (default is current).
|
||||
* @param x The position to move the cursor one tab stop forward.
|
||||
*/
|
||||
public nextStop(x?: number): number {
|
||||
if (x == null) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -28,6 +29,8 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||
this._alt = new Buffer(this._terminal, false);
|
||||
this._activeBuffer = this._normal;
|
||||
|
||||
this.setupTabStops();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,4 +90,13 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
this._normal.resize(newCols, newRows);
|
||||
this._alt.resize(newCols, newRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the tab stops.
|
||||
* @param i The index to start setting up tab stops from.
|
||||
*/
|
||||
public setupTabStops(i?: number): void {
|
||||
this._normal.setupTabStops(i);
|
||||
this._alt.setupTabStops(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -42,6 +43,16 @@ describe('CompositionHelper', () => {
|
||||
},
|
||||
handler: (text: string) => {
|
||||
handledText += text;
|
||||
},
|
||||
buffer: {
|
||||
isCursorInViewport: true
|
||||
},
|
||||
charMeasure: {
|
||||
height: 10,
|
||||
width: 10
|
||||
},
|
||||
options: {
|
||||
lineHeight: 1
|
||||
}
|
||||
};
|
||||
handledText = '';
|
||||
|
||||
+11
-10
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -193,26 +194,26 @@ export class CompositionHelper {
|
||||
if (!this.isComposing) {
|
||||
return;
|
||||
}
|
||||
const 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.
|
||||
const xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
|
||||
const cursorTop = xtermRows.offsetTop + cursor.offsetTop;
|
||||
|
||||
this.compositionView.style.left = cursor.offsetLeft + 'px';
|
||||
if (this.terminal.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
|
||||
const cursorTop = this.terminal.buffer.y * cellHeight;
|
||||
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
|
||||
|
||||
this.compositionView.style.left = cursorLeft + 'px';
|
||||
this.compositionView.style.top = cursorTop + 'px';
|
||||
this.compositionView.style.height = cursor.offsetHeight + 'px';
|
||||
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
|
||||
this.compositionView.style.height = cellHeight + 'px';
|
||||
this.compositionView.style.lineHeight = cellHeight + 'px';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this.compositionView.getBoundingClientRect();
|
||||
this.textarea.style.left = cursor.offsetLeft + 'px';
|
||||
this.textarea.style.left = cursorLeft + '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(true), 0);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
+18
-15
@@ -1,4 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -36,13 +38,14 @@ export class InputHandler implements IInputHandler {
|
||||
// dont overflow left
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
|
||||
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
|
||||
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2])
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
|
||||
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0);
|
||||
}
|
||||
} else {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0);
|
||||
}
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
@@ -81,21 +84,21 @@ export class InputHandler implements IInputHandler {
|
||||
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
||||
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
|
||||
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
|
||||
}
|
||||
|
||||
// insert empty cell at cursor
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]);
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
|
||||
}
|
||||
}
|
||||
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width];
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
|
||||
this._terminal.buffer.x++;
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
|
||||
// fullwidth char - set next cell width to zero and advance cursor
|
||||
if (ch_width === 2) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0];
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
|
||||
this._terminal.buffer.x++;
|
||||
}
|
||||
}
|
||||
@@ -157,7 +160,7 @@ export class InputHandler implements IInputHandler {
|
||||
* Horizontal Tab (HT) (Ctrl-I).
|
||||
*/
|
||||
public tab(): void {
|
||||
this._terminal.buffer.x = this._terminal.nextStop();
|
||||
this._terminal.buffer.x = this._terminal.buffer.nextStop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +191,7 @@ export class InputHandler implements IInputHandler {
|
||||
|
||||
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
let j = this._terminal.buffer.x;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
|
||||
@@ -349,7 +352,7 @@ export class InputHandler implements IInputHandler {
|
||||
public cursorForwardTab(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
while (param--) {
|
||||
this._terminal.buffer.x = this._terminal.nextStop();
|
||||
this._terminal.buffer.x = this._terminal.buffer.nextStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +470,7 @@ export class InputHandler implements IInputHandler {
|
||||
while (param--) {
|
||||
// test: echo -e '\e[44m\e[1M\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
this._terminal.buffer.lines.splice(row - 1, 1);
|
||||
this._terminal.buffer.lines.splice(row, 1);
|
||||
this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true));
|
||||
}
|
||||
|
||||
@@ -487,7 +490,7 @@ export class InputHandler implements IInputHandler {
|
||||
}
|
||||
|
||||
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param--) {
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
|
||||
@@ -535,7 +538,7 @@ export class InputHandler implements IInputHandler {
|
||||
|
||||
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
let j = this._terminal.buffer.x;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
this._terminal.buffer.lines.get(row)[j++] = ch;
|
||||
@@ -548,7 +551,7 @@ export class InputHandler implements IInputHandler {
|
||||
public cursorBackwardTab(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
while (param--) {
|
||||
this._terminal.buffer.x = this._terminal.prevStop();
|
||||
this._terminal.buffer.x = this._terminal.buffer.prevStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +592,7 @@ export class InputHandler implements IInputHandler {
|
||||
public repeatPrecedingCharacter(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y);
|
||||
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1];
|
||||
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
|
||||
|
||||
while (param--) {
|
||||
line[this._terminal.buffer.x++] = ch;
|
||||
|
||||
+70
-12
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
|
||||
import { IColorSet, IRenderer } from './renderer/Interfaces';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
|
||||
export interface IBrowser {
|
||||
isNode: boolean;
|
||||
@@ -17,24 +20,34 @@ export interface IBrowser {
|
||||
isMSWindows: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminal extends IEventEmitter {
|
||||
export interface IBufferAccessor {
|
||||
buffer: IBuffer;
|
||||
}
|
||||
|
||||
export interface IElementAccessor {
|
||||
element: HTMLElement;
|
||||
rowContainer: HTMLElement;
|
||||
selectionContainer: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ILinkifierAccessor {
|
||||
linkifier: ILinkifier;
|
||||
}
|
||||
|
||||
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
textarea: HTMLTextAreaElement;
|
||||
renderer: IRenderer;
|
||||
rows: number;
|
||||
cols: number;
|
||||
browser: IBrowser;
|
||||
writeBuffer: string[];
|
||||
children: HTMLElement[];
|
||||
cursorHidden: boolean;
|
||||
cursorState: number;
|
||||
defAttr: number;
|
||||
options: ITerminalOptions;
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
isFocused: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
@@ -47,6 +60,7 @@ export interface ITerminal extends IEventEmitter {
|
||||
reset(): void;
|
||||
showCursor(): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
|
||||
refresh(start: number, end: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,14 +106,12 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
convertEol: boolean;
|
||||
updateRange(y: number): void;
|
||||
scroll(isWrapped?: boolean): void;
|
||||
nextStop(x?: number): number;
|
||||
setgLevel(g: number): void;
|
||||
eraseAttr(): number;
|
||||
eraseRight(x: number, y: number): void;
|
||||
eraseLine(y: number): void;
|
||||
eraseLeft(x: number, y: number): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
||||
prevStop(x?: number): number;
|
||||
is(term: string): boolean;
|
||||
send(data: string): void;
|
||||
setgCharset(g: number, charset: Charset): void;
|
||||
@@ -117,20 +129,23 @@ export interface ITerminalOptions {
|
||||
bellSound?: string;
|
||||
bellStyle?: string;
|
||||
cancelEvents?: boolean;
|
||||
colors?: string[];
|
||||
cols?: number;
|
||||
convertEol?: boolean;
|
||||
cursorBlink?: boolean;
|
||||
cursorStyle?: string;
|
||||
debug?: boolean;
|
||||
disableStdin?: boolean;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
geometry?: [number, number];
|
||||
handler?: (data: string) => void;
|
||||
lineHeight?: number;
|
||||
rows?: number;
|
||||
screenKeys?: boolean;
|
||||
scrollback?: number;
|
||||
tabStopWidth?: number;
|
||||
termName?: string;
|
||||
theme?: ITheme;
|
||||
useFlowControl?: boolean;
|
||||
}
|
||||
|
||||
@@ -145,7 +160,10 @@ export interface IBuffer {
|
||||
scrollTop: number;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
isCursorInViewport: boolean;
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
}
|
||||
|
||||
export interface IBufferSet {
|
||||
@@ -157,11 +175,17 @@ export interface IBufferSet {
|
||||
activateAltBuffer(): void;
|
||||
}
|
||||
|
||||
export interface IMouseHelper {
|
||||
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
|
||||
}
|
||||
|
||||
export interface IViewport {
|
||||
syncScrollArea(): void;
|
||||
onWheel(ev: WheelEvent): void;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
onTouchMove(ev: TouchEvent): void;
|
||||
onThemeChanged(colors: IColorSet): void;
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
@@ -186,12 +210,14 @@ export interface ICompositionHelper {
|
||||
export interface ICharMeasure {
|
||||
width: number;
|
||||
height: number;
|
||||
measure(): void;
|
||||
measure(options: ITerminalOptions): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
linkifyRow(rowIndex: number): void;
|
||||
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
@@ -232,6 +258,14 @@ export interface ILinkMatcherOptions {
|
||||
* false if invalid.
|
||||
*/
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link.
|
||||
*/
|
||||
tooltipCallback?: LinkMatcherHandler;
|
||||
/**
|
||||
* A callback that fires when the mouse leaves a link that was hovered.
|
||||
*/
|
||||
leaveCallback?: () => void;
|
||||
/**
|
||||
* The priority of the link matcher, this defines the order in which the link
|
||||
* matcher is evaluated relative to others, from highest to lowest. The
|
||||
@@ -291,3 +325,27 @@ export interface IInputHandler {
|
||||
/** CSI s */ saveCursor(params?: number[]): void;
|
||||
/** CSI u */ restoreCursor(params?: number[]): void;
|
||||
}
|
||||
|
||||
export interface ITheme {
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
cursor?: string;
|
||||
cursorAccent?: string;
|
||||
selection?: string;
|
||||
black?: string;
|
||||
red?: string;
|
||||
green?: string;
|
||||
yellow?: string;
|
||||
blue?: string;
|
||||
magenta?: string;
|
||||
cyan?: string;
|
||||
white?: string;
|
||||
brightBlack?: string;
|
||||
brightRed?: string;
|
||||
brightGreen?: string;
|
||||
brightYellow?: string;
|
||||
brightBlue?: string;
|
||||
brightMagenta?: string;
|
||||
brightCyan?: string;
|
||||
brightWhite?: string;
|
||||
}
|
||||
|
||||
+106
-79
@@ -1,43 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal, ILinkifier } from './Interfaces';
|
||||
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { LinkMatcher } from './Types';
|
||||
import { LinkMatcher, LineData } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
|
||||
import { MockBuffer } from './utils/TestUtils.test';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor() {
|
||||
constructor(_terminal: IBufferAccessor & IElementAccessor) {
|
||||
super(_terminal);
|
||||
Linkifier.TIME_BEFORE_LINKIFY = 0;
|
||||
super();
|
||||
}
|
||||
|
||||
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
|
||||
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
|
||||
}
|
||||
|
||||
class TestMouseZoneManager implements IMouseZoneManager {
|
||||
public clears: number = 0;
|
||||
public zones: IMouseZone[] = [];
|
||||
add(zone: IMouseZone): void {
|
||||
this.zones.push(zone);
|
||||
}
|
||||
clearAll(): void {
|
||||
this.clears++;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let container: HTMLElement;
|
||||
let rows: HTMLElement[];
|
||||
let terminal: IBufferAccessor & IElementAccessor;
|
||||
let linkifier: TestLinkifier;
|
||||
let mouseZoneManager: TestMouseZoneManager;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
linkifier = new TestLinkifier();
|
||||
terminal = {
|
||||
buffer: new MockBuffer(),
|
||||
element: <HTMLElement>{}
|
||||
};
|
||||
terminal.buffer.lines = new CircularList<LineData>(20);
|
||||
terminal.buffer.ydisp = 0;
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
});
|
||||
|
||||
function addRow(html: string): void {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = html;
|
||||
container.appendChild(element);
|
||||
rows.push(element);
|
||||
function stringToRow(text: string): LineData {
|
||||
let result: LineData = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addRow(text: string): void {
|
||||
terminal.buffer.lines.push(stringToRow(text));
|
||||
}
|
||||
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRows();
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones.length, links.length);
|
||||
links.forEach((l, i) => {
|
||||
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
describe('before attachToDom', () => {
|
||||
@@ -52,78 +99,42 @@ describe('Linkifier', () => {
|
||||
|
||||
describe('after attachToDom', () => {
|
||||
beforeEach(() => {
|
||||
rows = [];
|
||||
linkifier.attachToDom(document, rows);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
linkifier.attachToDom(mouseZoneManager);
|
||||
});
|
||||
|
||||
function clickElement(element: Node): void {
|
||||
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): void {
|
||||
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): void {
|
||||
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 allow ~ character in URI path', (done) => {
|
||||
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link matcher', () => {
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone): void {
|
||||
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);
|
||||
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
|
||||
});
|
||||
it('should match a single link at the start of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
|
||||
assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], 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);
|
||||
assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done);
|
||||
});
|
||||
it('should match a single link at the end of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
|
||||
assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], 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);
|
||||
assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], 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);
|
||||
assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], 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);
|
||||
assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done);
|
||||
});
|
||||
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
|
||||
// This is a regression test for an issue that came about when using
|
||||
// an oh-my-zsh theme that added the large blue diamond unicode
|
||||
// character (U+1F537) which caused the path to be duplicated. See #642.
|
||||
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
|
||||
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,26 +142,42 @@ describe('Linkifier', () => {
|
||||
it('should enable link if true', done => {
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
validationCallback: (url, cb) => {
|
||||
assert.equal(mouseZoneManager.zones.length, 0);
|
||||
cb(true);
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||
assert.equal(mouseZoneManager.zones.length, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, 5);
|
||||
assert.equal(mouseZoneManager.zones[0].y, 1);
|
||||
// Fires done()
|
||||
mouseZoneManager.zones[0].clickCallback(<any>{});
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
linkifier.linkifyRows();
|
||||
});
|
||||
|
||||
it('should validate the uri, not the row', done => {
|
||||
addRow('abc test abc');
|
||||
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||
validationCallback: (uri, cb) => {
|
||||
assert.equal(uri, 'test');
|
||||
done();
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRows();
|
||||
});
|
||||
|
||||
it('should disable link if false', done => {
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
validationCallback: (url, cb) => {
|
||||
assert.equal(mouseZoneManager.zones.length, 0);
|
||||
cb(false);
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||
assert.equal(mouseZoneManager.zones.length, 0);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
// Allow time for the click to be performed
|
||||
linkifier.linkifyRows();
|
||||
// Allow time for the validation callback to be performed
|
||||
setTimeout(() => done(), 10);
|
||||
});
|
||||
|
||||
@@ -158,7 +185,7 @@ describe('Linkifier', () => {
|
||||
addRow('test test');
|
||||
let count = 0;
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
validationCallback: (url, cb) => {
|
||||
count += 1;
|
||||
if (count === 2) {
|
||||
done();
|
||||
@@ -166,7 +193,7 @@ describe('Linkifier', () => {
|
||||
cb(false);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
linkifier.linkifyRows();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user