Merge remote-tracking branch 'origin/master' into 426_alt_scroll
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as Strings from './Strings';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { isMac } from './shared/utils/Browser';
|
||||
import { RenderDebouncer } from './utils/RenderDebouncer';
|
||||
import { addDisposableListener } from './utils/Dom';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-';
|
||||
|
||||
enum BoundaryPosition {
|
||||
Top,
|
||||
Bottom
|
||||
}
|
||||
|
||||
export class AccessibilityManager implements IDisposable {
|
||||
private _accessibilityTreeRoot: HTMLElement;
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[] = [];
|
||||
private _liveRegion: HTMLElement;
|
||||
private _liveRegionLineCount: number = 0;
|
||||
|
||||
private _renderRowsDebouncer: RenderDebouncer;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
/**
|
||||
* This queue has a character pushed to it for keys that are pressed, if the
|
||||
* next character added to the terminal is equal to the key char then it is
|
||||
* not announced (added to live region) because it has already been announced
|
||||
* by the textarea event (which cannot be canceled). There are some race
|
||||
* condition cases if there is typing while data is streaming, but this covers
|
||||
* the main case of typing into the prompt and inputting the answer to a
|
||||
* question (Y/N, etc.).
|
||||
*/
|
||||
private _charsToConsume: string[] = [];
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.classList.add('xterm-accessibility-tree');
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
|
||||
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top);
|
||||
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom);
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
this._liveRegion.classList.add('live-region');
|
||||
this._liveRegion.setAttribute('aria-live', 'assertive');
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this._disposables.push(this._renderRowsDebouncer);
|
||||
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
|
||||
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
|
||||
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
|
||||
// should be listened to instead? That would mean that the order of events are always
|
||||
// guarenteed
|
||||
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
|
||||
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the dprchange event isn't supported
|
||||
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._terminal.element.removeChild(this._accessibilityTreeRoot);
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
this._disposables = null;
|
||||
this._accessibilityTreeRoot = null;
|
||||
this._rowContainer = null;
|
||||
this._liveRegion = null;
|
||||
this._rowContainer = null;
|
||||
this._rowElements = null;
|
||||
}
|
||||
|
||||
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = <HTMLElement>e.target;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2];
|
||||
|
||||
// Don't scroll if the buffer top has reached the end in that direction
|
||||
const posInSet = boundaryElement.getAttribute('aria-posinset');
|
||||
const lastRowPos = position === BoundaryPosition.Top ? '1' : `${this._terminal.buffer.lines.length}`;
|
||||
if (posInSet === lastRowPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't scroll when the last focused item was not the second row (focus is going the other
|
||||
// direction)
|
||||
if (e.relatedTarget !== beforeBoundaryElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old boundary element from array
|
||||
let topBoundaryElement: HTMLElement;
|
||||
let bottomBoundaryElement: HTMLElement;
|
||||
if (position === BoundaryPosition.Top) {
|
||||
topBoundaryElement = boundaryElement;
|
||||
bottomBoundaryElement = this._rowElements.pop();
|
||||
this._rowContainer.removeChild(bottomBoundaryElement);
|
||||
} else {
|
||||
topBoundaryElement = this._rowElements.shift();
|
||||
bottomBoundaryElement = boundaryElement;
|
||||
this._rowContainer.removeChild(topBoundaryElement);
|
||||
}
|
||||
|
||||
// Remove listeners from old boundary elements
|
||||
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
|
||||
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Add new element to array/DOM
|
||||
if (position === BoundaryPosition.Top) {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.unshift(newElement);
|
||||
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
|
||||
} else {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.push(newElement);
|
||||
this._rowContainer.appendChild(newElement);
|
||||
}
|
||||
|
||||
// Add listeners to new boundary elements
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Scroll up
|
||||
this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1);
|
||||
|
||||
// Focus new boundary before element
|
||||
this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus();
|
||||
|
||||
// Prevent the standard behavior
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(cols: number, rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Grow rows as required
|
||||
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
// Shrink rows as required
|
||||
while (this._rowElements.length > rows) {
|
||||
this._rowContainer.removeChild(this._rowElements.pop());
|
||||
}
|
||||
|
||||
// Add bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
public _createAccessibilityTreeNode(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('role', 'listitem');
|
||||
element.tabIndex = -1;
|
||||
this._refreshRowDimensions(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _onTab(spaceCount: number): void {
|
||||
for (let i = 0; i < spaceCount; i++) {
|
||||
this._onChar(' ');
|
||||
}
|
||||
}
|
||||
|
||||
private _onChar(char: string): void {
|
||||
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
|
||||
if (this._charsToConsume.length > 0) {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
const shiftedChar = this._charsToConsume.shift();
|
||||
if (shiftedChar !== char) {
|
||||
this._announceCharacter(char);
|
||||
}
|
||||
} else {
|
||||
this._announceCharacter(char);
|
||||
}
|
||||
|
||||
if (char === '\n') {
|
||||
this._liveRegionLineCount++;
|
||||
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
|
||||
this._liveRegion.textContent += Strings.tooMuchOutput;
|
||||
}
|
||||
}
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
|
||||
setTimeout(() => {
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clearLiveRegion(): void {
|
||||
this._liveRegion.textContent = '';
|
||||
this._liveRegionLineCount = 0;
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.parentNode) {
|
||||
this._accessibilityTreeRoot.removeChild(this._liveRegion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onKey(keyChar: string): void {
|
||||
this._clearLiveRegion();
|
||||
this._charsToConsume.push(keyChar);
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
this._renderRowsDebouncer.refresh(start, end);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const setSize = buffer.lines.length.toString();
|
||||
for (let i = start; i <= end; i++) {
|
||||
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
||||
const posInSet = (buffer.ydisp + i + 1).toString();
|
||||
const element = this._rowElements[i];
|
||||
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
||||
return;
|
||||
}
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
|
||||
}
|
||||
|
||||
private _announceCharacter(char: string): void {
|
||||
if (char === ' ') {
|
||||
// Always use nbsp for spaces in order to preserve the space between characters in
|
||||
// voiceover's caption window
|
||||
this._liveRegion.innerHTML += ' ';
|
||||
} else {
|
||||
this._liveRegion.textContent += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-5
@@ -3,10 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEventEmitter } from 'xterm';
|
||||
import { XtermListener } from './Types';
|
||||
import { IEventEmitter, IDisposable } from 'xterm';
|
||||
|
||||
export class EventEmitter implements IEventEmitter {
|
||||
private _events: {[type: string]: ((...args: any[]) => void)[]};
|
||||
private _events: {[type: string]: XtermListener[]};
|
||||
|
||||
constructor() {
|
||||
// Restore the previous events if available, this will happen if the
|
||||
@@ -14,12 +15,31 @@ export class EventEmitter implements IEventEmitter {
|
||||
this._events = this._events || {};
|
||||
}
|
||||
|
||||
public on(type: string, listener: ((...args: any[]) => void)): void {
|
||||
public on(type: string, listener: XtermListener): void {
|
||||
this._events[type] = this._events[type] || [];
|
||||
this._events[type].push(listener);
|
||||
}
|
||||
|
||||
public off(type: string, listener: ((...args: any[]) => void)): void {
|
||||
/**
|
||||
* Adds a disposabe listener to the EventEmitter, returning the disposable.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
*/
|
||||
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
this.on(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
this.off(type, handler);
|
||||
handler = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public off(type: string, listener: XtermListener): void {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
}
|
||||
@@ -51,7 +71,7 @@ export class EventEmitter implements IEventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public listeners(type: string): ((...args: any[]) => void)[] {
|
||||
public listeners(type: string): XtermListener[] {
|
||||
return this._events[type] || [];
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ export class InputHandler implements IInputHandler {
|
||||
char = this._terminal.charset[char];
|
||||
}
|
||||
|
||||
if (this._terminal.options.screenReaderMode) {
|
||||
this._terminal.emit('a11y.char', char);
|
||||
}
|
||||
|
||||
let row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
// insert combining char in last cell
|
||||
@@ -162,7 +166,11 @@ export class InputHandler implements IInputHandler {
|
||||
* Horizontal Tab (HT) (Ctrl-I).
|
||||
*/
|
||||
public tab(): void {
|
||||
const originalX = this._terminal.buffer.x;
|
||||
this._terminal.buffer.x = this._terminal.buffer.nextStop();
|
||||
if (this._terminal.options.screenReaderMode) {
|
||||
this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -150,6 +150,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
validationCallback: options.validationCallback,
|
||||
hoverTooltipCallback: options.tooltipCallback,
|
||||
hoverLeaveCallback: options.leaveCallback,
|
||||
willLinkActivate: options.willLinkActivate,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
@@ -290,6 +291,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
},
|
||||
e => {
|
||||
if (matcher.willLinkActivate) {
|
||||
return matcher.willLinkActivate(e, uri);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestSelectionManager extends SelectionManager {
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
||||
|
||||
// Disable DOM interaction
|
||||
public enable(): void {}
|
||||
|
||||
+45
-9
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData } from './Types';
|
||||
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData, XtermListener } from './Types';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './shared/utils/Browser';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
@@ -101,7 +101,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: (...args: any[]) => void;
|
||||
private _trimListener: XtermListener;
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
@@ -253,6 +253,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current click was inside the current selection
|
||||
* @param event The mouse event
|
||||
*/
|
||||
public isClickInSelection(event: MouseEvent): boolean {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
||||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
|
||||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects word at the current mouse event coordinates.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
public selectWordAtCursor(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._selectWordAt(coords, false);
|
||||
this._model.selectionEnd = null;
|
||||
this.refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all text within the terminal.
|
||||
*/
|
||||
@@ -278,7 +309,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
|
||||
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
@@ -298,7 +329,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
|
||||
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
|
||||
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
@@ -445,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords);
|
||||
this._selectWordAt(coords, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +633,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
||||
const bufferLine = this._buffer.lines.get(coords[1]);
|
||||
if (!bufferLine) {
|
||||
return null;
|
||||
@@ -708,15 +739,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
|
||||
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
|
||||
|
||||
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the word at the coordinates specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
* @param allowWhitespaceOnlySelection If whitespace should be selected
|
||||
*/
|
||||
protected _selectWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||
if (wordPosition) {
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
@@ -728,7 +764,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
const wordPosition = this._getWordAt(coords, true);
|
||||
if (wordPosition) {
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISoundManager } from './Types';
|
||||
|
||||
// Source: https://freesound.org/people/altemark/sounds/45759/
|
||||
// This sound is released under the Creative Commons Attribution 3.0 Unported
|
||||
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
|
||||
// made, apart from the conversion to base64.
|
||||
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
|
||||
|
||||
export class SoundManager implements ISoundManager {
|
||||
private _audioContext: AudioContext;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
}
|
||||
|
||||
public playBellSound(): void {
|
||||
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
||||
if (!this._audioContext && audioContextCtor) {
|
||||
this._audioContext = new audioContextCtor();
|
||||
}
|
||||
|
||||
if (this._audioContext) {
|
||||
const bellAudioSource = this._audioContext.createBufferSource();
|
||||
const context = this._audioContext;
|
||||
this._audioContext.decodeAudioData(this.base64ToArrayBuffer(this.removeMimeType(this._terminal.options.bellSound)), (buffer) => {
|
||||
bellAudioSource.buffer = buffer;
|
||||
bellAudioSource.connect(context.destination);
|
||||
bellAudioSource.start(0);
|
||||
});
|
||||
} else {
|
||||
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
|
||||
}
|
||||
}
|
||||
|
||||
private base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
const binaryString = window.atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
private removeMimeType(dataURI: string): string {
|
||||
// Split the input to get the mime-type and the data itself
|
||||
const splitUri = dataURI.split(',');
|
||||
|
||||
// Return only the data
|
||||
return splitUri[1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export let blankLine = 'Blank line';
|
||||
export let promptLabel = 'Terminal input';
|
||||
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
|
||||
@@ -578,6 +578,99 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
|
||||
});
|
||||
|
||||
// Characters using ctrl+alt sequences
|
||||
it('should return proper sequence for ctrl+alt+a', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
|
||||
});
|
||||
|
||||
// Characters using alt sequences (numbers)
|
||||
it('should return proper sequences for alt+0', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
|
||||
});
|
||||
it('should return proper sequences for alt+1', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
|
||||
});
|
||||
it('should return proper sequences for alt+2', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
|
||||
});
|
||||
it('should return proper sequences for alt+3', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
|
||||
});
|
||||
it('should return proper sequences for alt+4', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
|
||||
});
|
||||
it('should return proper sequences for alt+5', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
|
||||
});
|
||||
it('should return proper sequences for alt+6', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
|
||||
});
|
||||
it('should return proper sequences for alt+7', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
|
||||
});
|
||||
it('should return proper sequences for alt+8', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
|
||||
});
|
||||
it('should return proper sequences for alt+9', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
|
||||
});
|
||||
|
||||
// Characters using alt sequences (special chars)
|
||||
it('should return proper sequences for alt+;', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
|
||||
});
|
||||
it('should return proper sequences for alt+=', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
|
||||
});
|
||||
it('should return proper sequences for alt+,', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
|
||||
});
|
||||
it('should return proper sequences for alt+-', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
|
||||
});
|
||||
it('should return proper sequences for alt+.', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
|
||||
});
|
||||
it('should return proper sequences for alt+/', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
|
||||
});
|
||||
it('should return proper sequences for alt+~', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
|
||||
});
|
||||
it('should return proper sequences for alt+[', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
|
||||
});
|
||||
it('should return proper sequences for alt+\\', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
|
||||
});
|
||||
it('should return proper sequences for alt+]', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
|
||||
});
|
||||
it('should return proper sequences for alt+\'', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Third level shift', () => {
|
||||
|
||||
+107
-61
@@ -39,12 +39,43 @@ import { Linkifier } from './Linkifier';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import * as Browser from './shared/utils/Browser';
|
||||
import * as Strings from './Strings';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import { CHARSETS } from './Charsets';
|
||||
import { BELL_SOUND } from './utils/Sounds';
|
||||
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
|
||||
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
|
||||
import { MouseZoneManager } from './input/MouseZoneManager';
|
||||
import { ITheme } from 'xterm';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
|
||||
import { ITheme, ILocalizableStrings } from 'xterm';
|
||||
|
||||
// reg + shift key mappings for digits and special chars
|
||||
const KEYCODE_KEY_MAPPINGS = {
|
||||
// digits 0-9
|
||||
48: ['0', ')'],
|
||||
49: ['1', '!'],
|
||||
50: ['2', '@'],
|
||||
51: ['3', '#'],
|
||||
52: ['4', '$'],
|
||||
53: ['5', '%'],
|
||||
54: ['6', '^'],
|
||||
55: ['7', '&'],
|
||||
56: ['8', '*'],
|
||||
57: ['9', '('],
|
||||
|
||||
// special chars
|
||||
186: [';', ':'],
|
||||
187: ['=', '+'],
|
||||
188: [',', '<'],
|
||||
189: ['-', '_'],
|
||||
190: ['.', '>'],
|
||||
191: ['/', '?'],
|
||||
192: ['`', '~'],
|
||||
219: ['[', '{'],
|
||||
220: ['\\', '|'],
|
||||
221: [']', '}'],
|
||||
222: ['\'', '"']
|
||||
};
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -69,7 +100,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
termName: 'xterm',
|
||||
cursorBlink: false,
|
||||
cursorStyle: 'block',
|
||||
bellSound: BELL_SOUND,
|
||||
bellSound: DEFAULT_BELL_SOUND,
|
||||
bellStyle: 'none',
|
||||
enableBold: true,
|
||||
fontFamily: 'courier-new, courier, monospace',
|
||||
@@ -80,6 +111,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
letterSpacing: 0,
|
||||
scrollback: 1000,
|
||||
screenKeys: false,
|
||||
screenReaderMode: false,
|
||||
debug: false,
|
||||
macOptionIsMeta: false,
|
||||
cancelEvents: false,
|
||||
@@ -87,7 +119,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
useFlowControl: false,
|
||||
allowTransparency: false,
|
||||
tabStopWidth: 8,
|
||||
theme: null
|
||||
theme: null,
|
||||
rightClickSelectsWord: Browser.isMac
|
||||
// programFeatures: false,
|
||||
// focusKeys: false,
|
||||
};
|
||||
@@ -95,6 +128,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
|
||||
public textarea: HTMLTextAreaElement;
|
||||
public element: HTMLElement;
|
||||
public screenElement: HTMLElement;
|
||||
|
||||
/**
|
||||
* The HTMLElement that the terminal is created in, set by Terminal.open.
|
||||
@@ -108,7 +142,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
private helperContainer: HTMLElement;
|
||||
private compositionView: HTMLElement;
|
||||
private charSizeStyleElement: HTMLStyleElement;
|
||||
private bellAudioElement: HTMLAudioElement;
|
||||
|
||||
private visualBellTimer: number;
|
||||
|
||||
public browser: IBrowser = <any>Browser;
|
||||
@@ -190,6 +224,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
private userScrolling: boolean;
|
||||
|
||||
private inputHandler: InputHandler;
|
||||
public soundManager: SoundManager;
|
||||
private parser: Parser;
|
||||
public renderer: IRenderer;
|
||||
public selectionManager: SelectionManager;
|
||||
@@ -200,6 +235,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
public charMeasure: CharMeasure;
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
public mouseHelper: MouseHelper;
|
||||
private _accessibilityManager: AccessibilityManager;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
public cols: number;
|
||||
public rows: number;
|
||||
@@ -292,6 +329,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.selectionManager = this.selectionManager || null;
|
||||
this.linkifier = this.linkifier || new Linkifier(this);
|
||||
this._mouseZoneManager = this._mouseZoneManager || null;
|
||||
this.soundManager = this.soundManager || new SoundManager(this);
|
||||
|
||||
// Create the terminal's buffers and set the current buffer
|
||||
this.buffers = new BufferSet(this);
|
||||
@@ -308,6 +346,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
return this.buffers.active;
|
||||
}
|
||||
|
||||
public static get strings(): ILocalizableStrings {
|
||||
return Strings;
|
||||
}
|
||||
|
||||
/**
|
||||
* back_color_erase feature for xterm.
|
||||
*/
|
||||
@@ -440,9 +482,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.buffers.resize(this.cols, this.rows);
|
||||
this.viewport.syncScrollArea();
|
||||
break;
|
||||
case 'screenReaderMode':
|
||||
if (value) {
|
||||
if (!this._accessibilityManager) {
|
||||
this._accessibilityManager = new AccessibilityManager(this);
|
||||
}
|
||||
} else {
|
||||
if (this._accessibilityManager) {
|
||||
this._accessibilityManager.dispose();
|
||||
this._accessibilityManager = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'tabStopWidth': this.buffers.setupTabStops(); break;
|
||||
case 'bellSound':
|
||||
case 'bellStyle': this.syncBellSound(); break;
|
||||
}
|
||||
// Inform renderer of changes
|
||||
if (this.renderer) {
|
||||
@@ -474,6 +526,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
* Binds the desired blur behavior on a given terminal object.
|
||||
*/
|
||||
private _onTextAreaBlur(): void {
|
||||
// Text can safely be removed on blur. Doing it earlier could interfere with
|
||||
// screen readers reading it out.
|
||||
this.textarea.value = '';
|
||||
this.refresh(this.buffer.y, this.buffer.y);
|
||||
if (this.sendFocus) {
|
||||
this.send(C0.ESC + '[O');
|
||||
@@ -506,12 +561,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// Firefox doesn't appear to fire the contextmenu event on right click
|
||||
on(this.element, 'mousedown', (event: MouseEvent) => {
|
||||
if (event.button === 2) {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
on(this.element, 'contextmenu', (event: MouseEvent) => {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -554,16 +609,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
}
|
||||
}, true);
|
||||
|
||||
on(this.textarea, 'keydown', (ev: KeyboardEvent) => {
|
||||
this._keyDown(ev);
|
||||
}, true);
|
||||
|
||||
on(this.textarea, 'keypress', (ev: KeyboardEvent) => {
|
||||
this._keyPress(ev);
|
||||
// Truncate the textarea's value, since it is not needed
|
||||
this.textarea.value = '';
|
||||
}, true);
|
||||
|
||||
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true);
|
||||
on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true);
|
||||
on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart());
|
||||
on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e));
|
||||
on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend());
|
||||
@@ -591,6 +638,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.document = this.parent.ownerDocument;
|
||||
this.body = <HTMLBodyElement>this.document.body;
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
|
||||
|
||||
// Create main element container
|
||||
this.element = this.document.createElement('div');
|
||||
this.element.classList.add('terminal');
|
||||
@@ -608,18 +658,24 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.viewportScrollArea.classList.add('xterm-scroll-area');
|
||||
this.viewportElement.appendChild(this.viewportScrollArea);
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this);
|
||||
this.on('scroll', () => this._mouseZoneManager.clearAll());
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
this.screenElement = document.createElement('div');
|
||||
this.screenElement.classList.add('xterm-screen');
|
||||
// Create the container that will hold helpers like the textarea for
|
||||
// capturing DOM Events. Then produce the helpers.
|
||||
this.helperContainer = document.createElement('div');
|
||||
this.helperContainer.classList.add('xterm-helpers');
|
||||
fragment.appendChild(this.helperContainer);
|
||||
this.screenElement.appendChild(this.helperContainer);
|
||||
fragment.appendChild(this.screenElement);
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this);
|
||||
this.on('scroll', () => this._mouseZoneManager.clearAll());
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
this.textarea = document.createElement('textarea');
|
||||
this.textarea.classList.add('xterm-helper-textarea');
|
||||
// TODO: New API to set title? This could say "Terminal bash input", etc.
|
||||
this.textarea.setAttribute('aria-label', Strings.promptLabel);
|
||||
this.textarea.setAttribute('aria-multiline', 'false');
|
||||
this.textarea.setAttribute('autocorrect', 'off');
|
||||
this.textarea.setAttribute('autocapitalize', 'off');
|
||||
this.textarea.setAttribute('spellcheck', 'false');
|
||||
@@ -637,9 +693,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.helperContainer.appendChild(this.charSizeStyleElement);
|
||||
this.charMeasure = new CharMeasure(document, this.helperContainer);
|
||||
|
||||
// Preload audio, this relied on helperContainer
|
||||
this.syncBellSound();
|
||||
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
@@ -652,6 +705,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
|
||||
this.on('blur', () => this.renderer.onBlur());
|
||||
this.on('focus', () => this.renderer.onFocus());
|
||||
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
|
||||
// dprchange should handle this case, we need this as well for browsers that don't support the
|
||||
// matchMedia query.
|
||||
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
|
||||
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
|
||||
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
|
||||
|
||||
@@ -674,6 +731,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
|
||||
this.mouseHelper = new MouseHelper(this.renderer);
|
||||
|
||||
if (this.options.screenReaderMode) {
|
||||
// Note that this must be done *after* the renderer is created in order to
|
||||
// ensure the correct order of the dprchange event
|
||||
this._accessibilityManager = new AccessibilityManager(this);
|
||||
}
|
||||
|
||||
// Measure the character size
|
||||
this.charMeasure.measure(this.options);
|
||||
|
||||
@@ -734,7 +797,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
button = getButton(ev);
|
||||
|
||||
// get mouse coordinates
|
||||
pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
sendEvent(button, pos);
|
||||
@@ -760,7 +823,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
|
||||
function sendMove(ev: MouseEvent): void {
|
||||
let button = pressed;
|
||||
let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
// buttons marked as motions
|
||||
@@ -1054,7 +1117,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
*/
|
||||
public refresh(start: number, end: number): void {
|
||||
if (this.renderer) {
|
||||
this.renderer.queueRefresh(start, end);
|
||||
this.renderer.refreshRows(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1616,8 +1679,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// page up
|
||||
if (ev.shiftKey) {
|
||||
result.scrollLines = -(this.rows - 1);
|
||||
} else if (modifiers) {
|
||||
result.key = C0.ESC + '[5;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[5~';
|
||||
}
|
||||
@@ -1626,8 +1687,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// page down
|
||||
if (ev.shiftKey) {
|
||||
result.scrollLines = this.rows - 1;
|
||||
} else if (modifiers) {
|
||||
result.key = C0.ESC + '[6;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[6~';
|
||||
}
|
||||
@@ -1741,14 +1800,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// ^] - Operating System Command (OSC)
|
||||
result.key = String.fromCharCode(29);
|
||||
}
|
||||
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.metaKey) {
|
||||
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
|
||||
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
|
||||
result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32);
|
||||
} else if (ev.keyCode === 192) {
|
||||
result.key = C0.ESC + '`';
|
||||
} else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
|
||||
result.key = C0.ESC + (ev.keyCode - 48);
|
||||
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
|
||||
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
|
||||
if (key) {
|
||||
result.key = C0.ESC + key;
|
||||
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
|
||||
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
|
||||
result.key = C0.ESC + String.fromCharCode(keyCode);
|
||||
}
|
||||
} else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
|
||||
if (ev.keyCode === 65) { // cmd + a
|
||||
@@ -1844,7 +1904,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
*/
|
||||
public bell(): void {
|
||||
this.emit('bell');
|
||||
if (this.soundBell()) this.bellAudioElement.play();
|
||||
if (this.soundBell()) {
|
||||
this.soundManager.playBellSound();
|
||||
}
|
||||
|
||||
if (this.visualBell()) {
|
||||
this.element.classList.add('visual-bell-active');
|
||||
@@ -1901,7 +1963,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.rows = y;
|
||||
this.buffers.setupTabStops(this.cols);
|
||||
|
||||
this.charMeasure.measure(this.options);
|
||||
if (this.charMeasure) {
|
||||
this.charMeasure.measure(this.options);
|
||||
}
|
||||
|
||||
this.refresh(0, this.rows - 1);
|
||||
this.emit('resize', {cols: x, rows: y});
|
||||
@@ -2163,24 +2227,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// return this.options.bellStyle === 'sound' ||
|
||||
// this.options.bellStyle === 'both';
|
||||
}
|
||||
|
||||
private syncBellSound(): void {
|
||||
// Don't update anything if the terminal has not been opened yet
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.soundBell() && this.bellAudioElement) {
|
||||
this.bellAudioElement.setAttribute('src', this.options.bellSound);
|
||||
} else if (this.soundBell()) {
|
||||
this.bellAudioElement = document.createElement('audio');
|
||||
this.bellAudioElement.setAttribute('preload', 'auto');
|
||||
this.bellAudioElement.setAttribute('src', this.options.bellSound);
|
||||
this.helperContainer.appendChild(this.bellAudioElement);
|
||||
} else if (this.bellAudioElement) {
|
||||
this.helperContainer.removeChild(this.bellAudioElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+20
-1
@@ -3,12 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter as IPublicEventEmitter, IEventEmitter } from 'xterm';
|
||||
import { IColorSet, IRenderer } from './renderer/Types';
|
||||
import { IMouseZoneManager } from './input/Types';
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export type XtermListener = (...args: any[]) => void;
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
export type LineData = CharData[];
|
||||
|
||||
@@ -85,6 +87,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
}
|
||||
|
||||
export interface IViewport {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(): void;
|
||||
onWheel(ev: WheelEvent): void;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
@@ -161,6 +164,7 @@ export interface ILinkMatcher {
|
||||
matchIndex?: number;
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
priority?: number;
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ICharset {
|
||||
@@ -174,6 +178,7 @@ export interface ILinkHoverEvent {
|
||||
}
|
||||
|
||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
screenElement: HTMLElement;
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
renderer: IRenderer;
|
||||
@@ -187,6 +192,7 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
buffers: IBufferSet;
|
||||
isFocused: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
viewport: IViewport;
|
||||
bracketedPasteMode: boolean;
|
||||
applicationCursor: boolean;
|
||||
|
||||
@@ -285,6 +291,8 @@ export interface ISelectionManager {
|
||||
disable(): void;
|
||||
enable(): void;
|
||||
setSelection(row: number, col: number, length: number): void;
|
||||
isClickInSelection(event: MouseEvent): boolean;
|
||||
selectWordAtCursor(event: MouseEvent): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
@@ -321,6 +329,13 @@ export interface ILinkMatcherOptions {
|
||||
* default value is 0.
|
||||
*/
|
||||
priority?: number;
|
||||
/**
|
||||
* A callback that fires when the mousedown and click events occur that
|
||||
* determines whether a link will be activated upon click. This enables
|
||||
* only activating a link when a certain modifier is held down, if not the
|
||||
* mouse event will continue propagation (eg. double click to select word).
|
||||
*/
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface IBrowser {
|
||||
@@ -334,3 +349,7 @@ export interface IBrowser {
|
||||
isIphone: boolean;
|
||||
isMSWindows: boolean;
|
||||
}
|
||||
|
||||
export interface ISoundManager {
|
||||
playBellSound(): void;
|
||||
}
|
||||
|
||||
+15
-7
@@ -7,11 +7,14 @@ import { IColorSet } from './renderer/Types';
|
||||
import { ITerminal, IViewport } from './Types';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
/**
|
||||
* 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 implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private currentRowHeight: number = 0;
|
||||
private lastRecordedBufferLength: number = 0;
|
||||
private lastRecordedViewportHeight: number = 0;
|
||||
@@ -31,6 +34,10 @@ export class Viewport implements IViewport {
|
||||
private scrollArea: HTMLElement,
|
||||
private charMeasure: CharMeasure
|
||||
) {
|
||||
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this.viewportElement.offsetWidth - this.scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
@@ -48,13 +55,8 @@ export class Viewport implements IViewport {
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
|
||||
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
|
||||
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
|
||||
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
|
||||
}
|
||||
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
|
||||
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight);
|
||||
if (this.lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this.lastRecordedBufferHeight = newBufferHeight;
|
||||
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
|
||||
@@ -93,6 +95,12 @@ export class Viewport implements IViewport {
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private onScroll(ev: Event): void {
|
||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||
// which causes the terminal to scroll the buffer to the top
|
||||
if (!this.viewportElement.offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||
const diff = newRow - this.terminal.buffer.ydisp;
|
||||
this.terminal.scrollLines(diff, true);
|
||||
|
||||
+10
-5
@@ -28,17 +28,22 @@ export function proposeGeometry(term: Terminal): IGeometry {
|
||||
}
|
||||
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(term.element);
|
||||
const elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
|
||||
const elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
|
||||
const geometry = {
|
||||
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,10 +116,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
|
||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||
selectionManager.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
|
||||
@@ -151,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
// components from handling the mouse event.
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone) {
|
||||
// TODO: When link modifier support is added, the event should only be
|
||||
// cancelled when the modifier is held (see #1021)
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (zone.willLinkActivate(e)) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
private _findZoneEventAt(e: MouseEvent): IMouseZone {
|
||||
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
|
||||
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
@@ -189,9 +189,10 @@ export class MouseZone implements IMouseZone {
|
||||
public x2: number,
|
||||
public y: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverCallback?: (e: MouseEvent) => any,
|
||||
public tooltipCallback?: (e: MouseEvent) => any,
|
||||
public leaveCallback?: () => void
|
||||
public hoverCallback: (e: MouseEvent) => any,
|
||||
public tooltipCallback: (e: MouseEvent) => any,
|
||||
public leaveCallback: () => void,
|
||||
public willLinkActivate: (e: MouseEvent) => boolean
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -13,7 +13,8 @@ export interface IMouseZone {
|
||||
x2: number;
|
||||
y: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback?: (e: MouseEvent) => any;
|
||||
tooltipCallback?: (e: MouseEvent) => any;
|
||||
leaveCallback?: () => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
|
||||
+14
-40
@@ -13,13 +13,12 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { RenderDebouncer } from '../utils/RenderDebouncer';
|
||||
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
|
||||
import { ITheme } from 'xterm';
|
||||
|
||||
export class Renderer extends EventEmitter implements IRenderer {
|
||||
/** A queue of the rows to be refreshed */
|
||||
private _refreshRowsQueue: {start: number, end: number}[] = [];
|
||||
private _refreshAnimationFrame = null;
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
@@ -38,10 +37,10 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
}
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors, this._terminal.options.allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: null,
|
||||
@@ -61,6 +60,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this._updateDimensions();
|
||||
this.onOptionsChanged();
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
|
||||
|
||||
@@ -120,6 +120,10 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
}
|
||||
|
||||
// Resize the screen
|
||||
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this.emit('resize', {
|
||||
width: this.dimensions.canvasWidth,
|
||||
height: this.dimensions.canvasHeight
|
||||
@@ -168,47 +172,19 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
* @param {number} start The start row.
|
||||
* @param {number} end The end row.
|
||||
*/
|
||||
public queueRefresh(start: number, end: number): void {
|
||||
public refreshRows(start: number, end: number): void {
|
||||
if (this._isPaused) {
|
||||
this._needsFullRefresh = true;
|
||||
return;
|
||||
}
|
||||
this._refreshRowsQueue.push({ start: start, end: end });
|
||||
if (!this._refreshAnimationFrame) {
|
||||
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
|
||||
}
|
||||
this._renderDebouncer.refresh(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the refresh loop callback, calling refresh only if a refresh is
|
||||
* necessary before queueing up the next one.
|
||||
*/
|
||||
private _refreshLoop(): void {
|
||||
let start;
|
||||
let end;
|
||||
if (this._refreshRowsQueue.length > 4) {
|
||||
// Just do a full refresh when 5+ refreshes are queued
|
||||
start = 0;
|
||||
end = this._terminal.rows - 1;
|
||||
} else {
|
||||
// Get start and end rows that need refreshing
|
||||
start = this._refreshRowsQueue[0].start;
|
||||
end = this._refreshRowsQueue[0].end;
|
||||
for (let i = 1; i < this._refreshRowsQueue.length; i++) {
|
||||
if (this._refreshRowsQueue[i].start < start) {
|
||||
start = this._refreshRowsQueue[i].start;
|
||||
}
|
||||
if (this._refreshRowsQueue[i].end > end) {
|
||||
end = this._refreshRowsQueue[i].end;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._refreshRowsQueue = [];
|
||||
this._refreshAnimationFrame = null;
|
||||
|
||||
// Render
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this._terminal.rows - 1);
|
||||
private _renderRows(start: number, end: number): void {
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
this._terminal.emit('refresh', {start, end});
|
||||
}
|
||||
@@ -271,7 +247,5 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// differ.
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface IRenderer extends IEventEmitter {
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
queueRefresh(start: number, end: number): void;
|
||||
refreshRows(start: number, end: number): void;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* Adds a disposabe listener to a node in the DOM, returning the disposable.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
*/
|
||||
export function addDisposableListener(
|
||||
node: Element | Window | Document,
|
||||
type: string,
|
||||
handler: (e: any) => void,
|
||||
useCapture?: boolean
|
||||
): IDisposable {
|
||||
node.addEventListener(type, handler, useCapture);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
node.removeEventListener(type, handler, useCapture);
|
||||
node = null;
|
||||
handler = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ITerminal } from '../Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
*/
|
||||
export class RenderDebouncer implements IDisposable {
|
||||
private _rowStart: number;
|
||||
private _rowEnd: number;
|
||||
private _animationFrame: number = null;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _callback: (start: number, end: number) => void
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(rowStart?: number, rowEnd?: number): void {
|
||||
rowStart = rowStart || 0;
|
||||
rowEnd = rowEnd || this._terminal.rows - 1;
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
if (this._animationFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
// Clamp values
|
||||
this._rowStart = Math.max(this._rowStart, 0);
|
||||
this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1);
|
||||
|
||||
// Run render callback
|
||||
this._callback(this._rowStart, this._rowEnd);
|
||||
|
||||
// Reset debouncer
|
||||
this._rowStart = null;
|
||||
this._rowEnd = null;
|
||||
this._animationFrame = null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// Source: https://freesound.org/people/altemark/sounds/45759/
|
||||
// This sound is released under the Creative Commons Attribution 3.0 Unported
|
||||
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
|
||||
// made, apart from the conversion to base64.
|
||||
export const BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
|
||||
@@ -4,12 +4,13 @@
|
||||
*/
|
||||
|
||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions } from '../Types';
|
||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types';
|
||||
import { Buffer } from '../Buffer';
|
||||
import * as Browser from '../shared/utils/Browser';
|
||||
import { ITheme } from 'xterm';
|
||||
import { ITheme, IDisposable } from 'xterm';
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
static string: any;
|
||||
getOption(key: any): any {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -80,6 +81,7 @@ export class MockTerminal implements ITerminal {
|
||||
isFocused: boolean;
|
||||
options: ITerminalOptions = {};
|
||||
element: HTMLElement;
|
||||
screenElement: HTMLElement;
|
||||
rowContainer: HTMLElement;
|
||||
selectionContainer: HTMLElement;
|
||||
selectionManager: ISelectionManager;
|
||||
@@ -96,6 +98,7 @@ export class MockTerminal implements ITerminal {
|
||||
scrollback: number;
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
viewport: IViewport;
|
||||
applicationCursor: boolean;
|
||||
handler(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
@@ -103,12 +106,18 @@ export class MockTerminal implements ITerminal {
|
||||
on(event: string, callback: () => void): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
off(type: string, listener: (...args: any[]) => void): void {
|
||||
off(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
scrollLines(disp: number, suppressScrollEvent: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
scrollToRow(absoluteRow: number): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
cancel(ev: Event, force?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -248,15 +257,18 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
setOption(key: string, value: any): void {
|
||||
this.options[key] = value;
|
||||
}
|
||||
on(type: string, listener: (...args: any[]) => void): void {
|
||||
on(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
off(type: string, listener: (...args: any[]) => void): void {
|
||||
off(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
emit(type: string, data?: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockBuffer implements IBuffer {
|
||||
@@ -284,15 +296,18 @@ export class MockBuffer implements IBuffer {
|
||||
|
||||
export class MockRenderer implements IRenderer {
|
||||
colorManager: IColorManager;
|
||||
on(type: string, listener: (...args: any[]) => void): void {
|
||||
on(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
off(type: string, listener: (...args: any[]) => void): void {
|
||||
off(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
emit(type: string, data?: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dimensions: IRenderDimensions;
|
||||
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
|
||||
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
|
||||
@@ -304,10 +319,11 @@ export class MockRenderer implements IRenderer {
|
||||
onOptionsChanged(): void {}
|
||||
onWindowResize(devicePixelRatio: number): void {}
|
||||
clear(): void {}
|
||||
queueRefresh(start: number, end: number): void {}
|
||||
refreshRows(start: number, end: number): void {}
|
||||
}
|
||||
|
||||
export class MockViewport implements IViewport {
|
||||
scrollBarWidth: number = 0;
|
||||
onThemeChanged(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
+33
-1
@@ -96,9 +96,18 @@
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm canvas {
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@@ -123,3 +132,26 @@
|
||||
.xterm:not(.enable-mouse-events) {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] {
|
||||
outline: 1px solid #F80;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user