Merge branch 'master' into column-select

This commit is contained in:
Viraj Sinha
2018-06-28 15:59:27 -07:00
committed by GitHub
31 changed files with 281 additions and 153 deletions
+1
View File
@@ -32,6 +32,7 @@
</div>
<hr/>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+26 -12
View File
@@ -33,23 +33,23 @@ function setPadding() {
term.fit();
}
paddingElement.addEventListener('change', setPadding);
createTerminal();
actionElements.findNext.addEventListener('keypress', function (e) {
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
createTerminal();
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
@@ -76,6 +76,15 @@ function createTerminal() {
term.fit();
term.focus();
const buttonHandler = () => {
term.dispose();
term = null;
window.term = null;
socket = null;
document.getElementById('dispose').removeEventListener('click', buttonHandler);
};
document.getElementById('dispose').addEventListener('click', buttonHandler);
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
initOptions(term);
@@ -124,7 +133,7 @@ function runFakeTerminal() {
term.writeln('');
term.prompt();
term.on('key', function (key, ev) {
term._core.register(term.addDisposableListener('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
@@ -139,11 +148,11 @@ function runFakeTerminal() {
} else if (printable) {
term.write(key);
}
});
}));
term.on('paste', function (data, ev) {
term._core.register(term.addDisposableListener('paste', function (data, ev) {
term.write(data);
});
}));
}
function initOptions(term) {
@@ -213,14 +222,14 @@ function initOptions(term) {
// Attach listeners
booleanOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
});
});
numberOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
@@ -231,13 +240,18 @@ function initOptions(term) {
});
Object.keys(stringOptions).forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
});
});
}
function addDomListener(element, type, handler) {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize() {
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
+18 -20
View File
@@ -6,9 +6,9 @@
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';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
const MAX_ROWS_TO_READ = 20;
@@ -17,7 +17,7 @@ const enum BoundaryPosition {
BOTTOM
}
export class AccessibilityManager implements IDisposable {
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
@@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable {
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
@@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable {
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
@@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable {
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()));
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(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()));
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this.register(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.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this.register(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()));
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
super.dispose();
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
+1 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
const INIT_COLS = 80;
+6 -7
View File
@@ -3,10 +3,10 @@
* @license MIT
*/
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IDisposable, IMarker } from 'xterm';
import { IMarker } from 'xterm';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
@@ -320,14 +320,14 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
marker.register(this.lines.addDisposableListener('trim', amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
marker.dispose();
}
}));
marker.on('dispose', () => this._removeMarker(marker));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
@@ -342,7 +342,6 @@ export class Marker extends EventEmitter implements IMarker {
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public disposables: IDisposable[] = [];
public get id(): number { return this._id; }
@@ -357,8 +356,8 @@ export class Marker extends EventEmitter implements IMarker {
return;
}
this.isDisposed = true;
this.disposables.forEach(d => d.dispose());
this.disposables.length = 0;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
+22 -1
View File
@@ -4,6 +4,7 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { Disposable } from './common/Lifecycle';
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
@@ -207,7 +208,7 @@ class DcsDummy implements IDcsHandler {
* NOTE: The parameter element notation is currently not supported.
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser implements IEscapeSequenceParser {
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
@@ -236,6 +237,8 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
@@ -260,6 +263,24 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
this._errorHandler = this._errorHandlerFb;
}
public dispose(): void {
this._printHandlerFb = null;
this._executeHandlerFb = null;
this._csiHandlerFb = null;
this._escHandlerFb = null;
this._oscHandlerFb = null;
this._dcsHandlerFb = null;
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
this._csiHandlers = null;
this._escHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
this._errorHandler = null;
}
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
this._printHandler = callback;
}
+5 -1
View File
@@ -5,11 +5,13 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
export class EventEmitter implements IEventEmitter, IDisposable {
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
super();
// Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset).
this._events = this._events || {};
@@ -26,6 +28,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
return {
dispose: () => {
@@ -76,6 +79,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
}
public dispose(): void {
super.dispose();
this._events = {};
}
}
+11 -1
View File
@@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -111,13 +112,17 @@ class DECRQSS implements IDcsHandler {
* Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
* each function's header comment.
*/
export class InputHandler implements IInputHandler {
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
this.register(this._parser);
this._surrogateHigh = '';
/**
@@ -285,6 +290,11 @@ export class InputHandler implements IInputHandler {
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
}
public dispose(): void {
super.dispose();
this._terminal = null;
}
public parse(data: string): void {
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
+4 -2
View File
@@ -4,11 +4,11 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -21,6 +21,8 @@ class TestLinkifier extends Linkifier {
}
class TestMouseZoneManager implements IMouseZoneManager {
dispose(): void {
}
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
+2 -2
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
/**
+6 -1
View File
@@ -6,7 +6,7 @@
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -123,6 +123,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._columnSelectRequiredModifiers = this._initColumnSelectModifierKeys();
}
public dispose(): void {
super.dispose();
this._removeMouseDownListeners();
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
+73 -81
View File
@@ -22,7 +22,7 @@
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer';
@@ -36,17 +36,17 @@ import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import * as Browser from './shared/utils/Browser';
import * as Dom from './utils/Dom';
import { addDisposableDomListener } from './ui/Lifecycle';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper';
import { clone } from './utils/Clone';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './input/MouseZoneManager';
import { MouseZoneManager } from './ui/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
import { ITheme, IMarker, IDisposable } from 'xterm';
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
import { DomRenderer } from './renderer/dom/DomRenderer';
@@ -114,8 +114,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public element: HTMLElement;
public screenElement: HTMLElement;
private _disposables: IDisposable[];
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
@@ -236,8 +234,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public dispose(): void {
super.dispose();
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
this._customKeyEventHandler = null;
removeTerminalFromCache(this);
this.handler = () => {};
this.write = () => {};
@@ -254,8 +251,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
private _setup(): void {
this._disposables = [];
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
if (this.options[key] == null) {
this.options[key] = DEFAULT_OPTIONS[key];
@@ -308,6 +303,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._userScrolling = false;
this._inputHandler = new InputHandler(this);
this.register(this._inputHandler);
// Reuse renderer if the Terminal is being recreated via a reset call.
this.renderer = this.renderer || null;
this.selectionManager = this.selectionManager || null;
@@ -528,30 +524,30 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._bindKeys();
// Bind clipboard functionality
on(this.element, 'copy', (event: ClipboardEvent) => {
this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => {
// If mouse events are active it means the selection manager is disabled and
// copy should be handled by the host program.
if (!this.hasSelection()) {
return;
}
copyHandler(event, this, this.selectionManager);
});
}));
const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this);
on(this.textarea, 'paste', pasteHandlerWrapper);
on(this.element, 'paste', pasteHandlerWrapper);
this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
// Handle right click context menus
if (Browser.isFirefox) {
// Firefox doesn't appear to fire the contextmenu event on right click
on(this.element, 'mousedown', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
}
});
}));
} else {
on(this.element, 'contextmenu', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
});
}));
}
// Move the textarea under the cursor when middle clicking on Linux to ensure
@@ -560,11 +556,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (Browser.isLinux) {
// Use auxclick event over mousedown the latter doesn't seem to work. Note
// that the regular click event doesn't fire for the middle mouse button.
on(this.element, 'auxclick', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
if (event.button === 1) {
moveTextAreaUnderMouseCursor(event, this.textarea);
}
});
}));
}
}
@@ -573,35 +569,35 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/
private _bindKeys(): void {
const self = this;
on(this.element, 'keydown', function (ev: KeyboardEvent): void {
this.register(addDisposableDomListener(this.element, 'keydown', function (ev: KeyboardEvent): void {
if (document.activeElement !== this) {
return;
}
self._keyDown(ev);
}, true);
}, true));
on(this.element, 'keypress', function (ev: KeyboardEvent): void {
this.register(addDisposableDomListener(this.element, 'keypress', function (ev: KeyboardEvent): void {
if (document.activeElement !== this) {
return;
}
self._keyPress(ev);
}, true);
}, true));
on(this.element, 'keyup', (ev: KeyboardEvent) => {
this.register(addDisposableDomListener(this.element, 'keyup', (ev: KeyboardEvent) => {
if (!wasMondifierKeyOnlyEvent(ev)) {
this.focus();
}
self._keyUp(ev);
}, true);
}, 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());
this.on('refresh', () => this._compositionHelper.updateCompositionElements());
this.on('refresh', (data) => this._queueLinkification(data.start, data.end));
this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true));
this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true));
this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()));
this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)));
this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()));
this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements()));
this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end)));
}
/**
@@ -622,6 +618,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
this.register(this._screenDprMonitor);
// Create main element container
this.element = this._document.createElement('div');
@@ -651,7 +648,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
fragment.appendChild(this.screenElement);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.register(this._mouseZoneManager);
this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this._mouseZoneManager);
this.textarea = document.createElement('textarea');
@@ -663,8 +661,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.textarea.addEventListener('focus', () => this._onTextAreaFocus());
this.textarea.addEventListener('blur', () => this._onTextAreaBlur());
this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._compositionView = document.createElement('div');
@@ -682,37 +680,39 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
this.register(this.renderer);
this.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
this.register(this.viewport);
this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove()));
this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows)));
this.register(this.addDisposableListener('blur', () => this.renderer.onBlur()));
this.register(this.addDisposableListener('focus', () => this.renderer.onFocus()));
this.register(this.addDisposableListener('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.
this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged());
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged()));
this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea()));
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode));
this.selectionManager.on('newselection', text => {
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode)));
this.register(this.selectionManager.addDisposableListener('newselection', text => {
// If there's a new selection, put it into the textarea, focus and select it
// in order to register it as a selection on the OS. This event is fired
// only on Linux to enable middle click to paste selection.
this.textarea.value = text;
this.textarea.focus();
this.textarea.select();
});
this.on('scroll', () => {
}));
this.register(this.addDisposableListener('scroll', () => {
this.viewport.syncScrollArea();
this.selectionManager.refresh();
});
this._viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
this.mouseHelper = new MouseHelper(this.renderer);
@@ -982,7 +982,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return button;
}
on(el, 'mousedown', (ev: MouseEvent) => {
this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
// Prevent the focus on the textarea from getting lost
// and make sure we get focused on mousedown
@@ -1007,30 +1007,37 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return this.cancel(ev);
}
// TODO: these event listeners should be managed by the disposable, the Terminal reference may
// be kept aroud if Terminal.dispose is fired when the mouse is down
// bind events
if (this.normalMouse) on(this._document, 'mousemove', sendMove);
if (this.normalMouse) {
this._document.addEventListener('mousemove', sendMove);
}
// x10 compatibility mode can't send button releases
if (!this.x10Mouse) {
const handler = (ev: MouseEvent) => {
sendButton(ev);
// TODO: Seems dangerous calling this on document?
if (this.normalMouse) off(this._document, 'mousemove', sendMove);
off(this._document, 'mouseup', handler);
if (this.normalMouse) {
this._document.removeEventListener('mousemove', sendMove);
}
this._document.removeEventListener('mouseup', handler);
return this.cancel(ev);
};
// TODO: Seems dangerous calling this on document?
on(this._document, 'mouseup', handler);
this._document.addEventListener('mouseup', handler);
}
return this.cancel(ev);
});
}));
// if (this.normalMouse) {
// on(this.document, 'mousemove', sendMove);
// }
on(el, 'wheel', (ev: WheelEvent) => {
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (!this.mouseEvents) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -1055,27 +1062,27 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this.x10Mouse || this._vt300Mouse || this._decLocator) return;
sendButton(ev);
ev.preventDefault();
});
}));
// allow wheel scrolling in
// the shell for example
on(el, 'wheel', (ev: WheelEvent) => {
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (this.mouseEvents) return;
this.viewport.onWheel(ev);
return this.cancel(ev);
});
}));
on(el, 'touchstart', (ev: TouchEvent) => {
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchStart(ev);
return this.cancel(ev);
});
}));
on(el, 'touchmove', (ev: TouchEvent) => {
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchMove(ev);
return this.cancel(ev);
});
}));
}
/**
@@ -1937,21 +1944,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Helpers
*/
function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean, passive?: boolean): void {
if (!Array.isArray(el)) {
el = [el];
}
el.forEach((element: HTMLElement) => {
element.addEventListener(type, handler, { capture: capture || false, passive: passive || false });
});
}
// TODO: Remove once everything is typed
const on = globalOn;
function off(el: any, type: string, handler: (event: Event) => any, capture: boolean = false): void {
el.removeEventListener(type, handler, capture);
}
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl
+4 -4
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -86,7 +86,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
tabSet(): void;
}
export interface IViewport {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
@@ -468,7 +468,7 @@ export interface IDcsHandler {
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser {
export interface IEscapeSequenceParser extends IDisposable {
/**
* Reset the parser to its initial state (handlers are kept).
*/
+7 -3
View File
@@ -5,7 +5,9 @@
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -13,7 +15,7 @@ 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 {
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
@@ -39,11 +41,13 @@ export class Viewport implements IViewport {
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
// 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));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
+5 -1
View File
@@ -5,9 +5,13 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
_core: {
register<T extends IDisposable>(d: T): void;
};
__socket?: WebSocket;
__attachSocketBuffer?: string;
+19 -5
View File
@@ -5,7 +5,7 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/**
@@ -87,14 +87,28 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
socket.send(data);
};
socket.addEventListener('message', addonTerminal.__getMessage);
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData);
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
}
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));
}
function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {
socket.addEventListener(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
socket.removeEventListener(type, handler);
handler = null;
}
};
}
/**
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
/**
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
* object and its components.
*/
export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = [];
constructor() {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
/**
* Registers a disposable object.
* @param d The disposable to register.
*/
public register<T extends IDisposable>(d: T): void {
this._disposables.push(d);
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ export class Terminal implements ITerminalApi {
this._core.emit(type, data);
}
public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable {
return this.addDisposableListener(type, handler);
return this._core.addDisposableListener(type, handler);
}
public resize(columns: number, rows: number): void {
this._core.resize(columns, rows);

Some files were not shown because too many files have changed in this diff Show More