diff --git a/bin/build b/bin/build index 00a73236..d1b403d2 100755 --- a/bin/build +++ b/bin/build @@ -4,7 +4,13 @@ tsc # Concat all xterm.js files into a single file and output as a UMD to dist/xterm.js -browserify out/xterm.js --standalone Terminal -p [ tsify ] --outfile dist/xterm.js +browserify ./out/xterm.js --standalone Terminal --debug --outfile ./dist/xterm.js +cat ./dist/xterm.js | exorcist ./dist/xterm.js.map -b ./dist > ./dist/xterm.temp.js +rm ./dist/xterm.js +mv ./dist/xterm.temp.js ./dist/xterm.js + +# Resolve the chain of sourcemaps so that ./dist/xterm.js.map points at ./src +sorcery -i dist/xterm.js # Copy all CSS files from src/ to dist/ cd src diff --git a/package.json b/package.json index 93ed9a7b..0c338451 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,13 @@ "repository": "https://github.com/sourcelair/xterm.js", "license": "MIT", "devDependencies": { + "@types/chai": "^3.4.34", + "@types/mocha": "^2.2.33", "@types/node": "^6.0.41", "browserify": "^13.1.0", "chai": "3.5.0", "docdash": "0.4.0", + "exorcist": "^0.4.0", "express": "4.13.4", "express-ws": "2.0.0-rc.1", "glob": "^7.0.5", @@ -22,7 +25,7 @@ "nodemon": "1.10.2", "pty.js": "0.3.1", "sleep": "^3.0.1", - "tsify": "^1.0.7", + "sorcery": "^0.10.0", "typescript": "^2.0.3" }, "scripts": { diff --git a/src/Interfaces.ts b/src/Interfaces.ts new file mode 100644 index 00000000..14355e57 --- /dev/null +++ b/src/Interfaces.ts @@ -0,0 +1,14 @@ +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +export interface ITerminal { + rowContainer: HTMLElement; + ydisp: number; + lines: string[]; + rows: number; + + on(event: string, callback: () => void); + scrollDisp(disp: number, suppressScrollEvent: boolean); +} diff --git a/src/Viewport.js b/src/Viewport.js deleted file mode 100644 index b47e8053..00000000 --- a/src/Viewport.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * xterm.js: xterm, in the browser - * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) - */ - -/** - * Represents the viewport of a terminal, the visible area within the larger buffer of output. - * Logic for the virtual scroll bar is included in this object. - * @param {Terminal} terminal The Terminal object. - * @param {HTMLElement} viewportElement The DOM element acting as the viewport - * @param {HTMLElement} charMeasureElement A DOM element used to measure the character size of - * the terminal. - */ -function Viewport(terminal, viewportElement, scrollArea, charMeasureElement) { - this.terminal = terminal; - this.viewportElement = viewportElement; - this.scrollArea = scrollArea; - this.charMeasureElement = charMeasureElement; - this.currentRowHeight = 0; - this.lastRecordedBufferLength = 0; - this.lastRecordedViewportHeight = 0; - - this.terminal.on('scroll', this.syncScrollArea.bind(this)); - this.terminal.on('resize', this.syncScrollArea.bind(this)); - this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); - - this.syncScrollArea(); -} - -/** - * Refreshes row height, setting line-height, viewport height and scroll area height if - * necessary. - * @param {number|undefined} charSize A character size measurement bounding rect object, if it - * doesn't exist it will be created. - */ -Viewport.prototype.refresh = function(charSize) { - var size = charSize || this.charMeasureElement.getBoundingClientRect(); - if (size.height > 0) { - var rowHeightChanged = size.height !== this.currentRowHeight; - if (rowHeightChanged) { - this.currentRowHeight = size.height; - this.viewportElement.style.lineHeight = size.height + 'px'; - this.terminal.rowContainer.style.lineHeight = size.height + 'px'; - } - var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; - if (rowHeightChanged || viewportHeightChanged) { - this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; - } - this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px'; - } -}; - -/** - * Updates dimensions and synchronizes the scroll area if necessary. - */ -Viewport.prototype.syncScrollArea = function() { - if (this.lastRecordedBufferLength !== this.terminal.lines.length) { - // If buffer height changed - this.lastRecordedBufferLength = this.terminal.lines.length; - this.refresh(); - } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { - // If viewport height changed - this.refresh(); - } else { - // If size has changed, refresh viewport - var size = this.charMeasureElement.getBoundingClientRect(); - if (size.height !== this.currentRowHeight) { - this.refresh(size); - } - } - - // Sync scrollTop - var scrollTop = this.terminal.ydisp * this.currentRowHeight; - if (this.viewportElement.scrollTop !== scrollTop) { - this.viewportElement.scrollTop = scrollTop; - } -}; - -/** - * Handles scroll events on the viewport, calculating the new viewport and requesting the - * terminal to scroll to it. - * @param {Event} ev The scroll event. - */ -Viewport.prototype.onScroll = function(ev) { - var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); - var diff = newRow - this.terminal.ydisp; - this.terminal.scrollDisp(diff, true); -}; - -/** - * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual - * scrolling to `onScroll`, this event needs to be attached manually by the consumer of - * `Viewport`. - * @param {WheelEvent} ev The mouse wheel event. - */ -Viewport.prototype.onWheel = function(ev) { - if (ev.deltaY === 0) { - // Do nothing if it's not a vertical scroll event - return; - } - // Fallback to WheelEvent.DOM_DELTA_PIXEL - var multiplier = 1; - if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { - multiplier = this.currentRowHeight; - } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { - multiplier = this.currentRowHeight * this.terminal.rows; - } - this.viewportElement.scrollTop += ev.deltaY * multiplier; - // Prevent the page from scrolling when the terminal scrolls - ev.preventDefault(); -}; - -export { Viewport }; diff --git a/src/test/viewport-test.js b/src/Viewport.test.ts similarity index 67% rename from src/test/viewport-test.js rename to src/Viewport.test.ts index 376fcdc8..5b106b43 100644 --- a/src/test/viewport-test.js +++ b/src/Viewport.test.ts @@ -1,21 +1,21 @@ -var assert = require('chai').assert; -var Terminal = require('../xterm'); +import { assert } from 'chai'; +import { Viewport } from './Viewport'; -describe('Viewport', function () { +describe('Viewport', () => { var terminal; var viewportElement; var charMeasureElement; var viewport; var scrollAreaElement; - var CHARACTER_HEIGHT = 10; + const CHARACTER_HEIGHT = 10; - beforeEach(function () { + beforeEach(() => { terminal = { lines: [], rows: 0, ydisp: 0, - on: function () {}, + on: () => {}, rowContainer: { style: { lineHeight: 0 @@ -23,7 +23,7 @@ describe('Viewport', function () { } }; viewportElement = { - addEventListener: function () {}, + addEventListener: () => {}, style: { height: 0, lineHeight: 0 @@ -35,37 +35,31 @@ describe('Viewport', function () { } }; charMeasureElement = { - getBoundingClientRect: function () { + getBoundingClientRect: () => { return { width: null, height: CHARACTER_HEIGHT }; } }; - viewport = new Terminal.Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement); + viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement); }); - describe('Public API', function () { - it('should define Viewport.prototype.onWheel', function () { - assert.isDefined(Terminal.Viewport.prototype.onWheel); - }); - }); - - describe('refresh', function () { - it('should set the line-height of the terminal', function () { + describe('refresh', () => { + it('should set the line-height of the terminal', () => { assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px'); assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = function () { + charMeasureElement.getBoundingClientRect = () => { return { width: null, height: 1 }; }; viewport.refresh(); assert.equal(viewportElement.style.lineHeight, '1px'); assert.equal(terminal.rowContainer.style.lineHeight, '1px'); }); - it('should set the height of the viewport when the line-height changed', function () { + it('should set the height of the viewport when the line-height changed', () => { terminal.lines.push(''); terminal.lines.push(''); terminal.rows = 1; viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = function () { + charMeasureElement.getBoundingClientRect = () => { return { width: null, height: 20 }; }; viewport.refresh(); @@ -73,8 +67,8 @@ describe('Viewport', function () { }); }); - describe('syncScrollArea', function () { - it('should sync the scroll area', function () { + describe('syncScrollArea', () => { + it('should sync the scroll area', () => { terminal.lines.push(''); terminal.rows = 1; assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px'); diff --git a/src/Viewport.ts b/src/Viewport.ts new file mode 100644 index 00000000..11889dae --- /dev/null +++ b/src/Viewport.ts @@ -0,0 +1,120 @@ +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + */ + +import { ITerminal } from './Interfaces'; + +/** + * Represents the viewport of a terminal, the visible area within the larger buffer of output. + * Logic for the virtual scroll bar is included in this object. + * @param viewportElement The DOM element acting as the viewport. + * @param scrollArea The DOM element acting as the scroll area. + * @param charMeasureElement A DOM element used to measure the character size of. the terminal. + */ +export class Viewport { + private currentRowHeight: number; + private lastRecordedBufferLength: number; + private lastRecordedViewportHeight: number; + + constructor( + private terminal: ITerminal, + private viewportElement: HTMLElement, + private scrollArea: HTMLElement, + private charMeasureElement: HTMLElement + ) { + this.currentRowHeight = 0; + this.lastRecordedBufferLength = 0; + this.lastRecordedViewportHeight = 0; + + this.terminal.on('scroll', this.syncScrollArea.bind(this)); + this.terminal.on('resize', this.syncScrollArea.bind(this)); + this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); + + this.syncScrollArea(); + } + + /** + * Refreshes row height, setting line-height, viewport height and scroll area height if + * necessary. + * @param charSize A character size measurement bounding rect object, if it doesn't exist it will + * be created. + */ + private refresh(charSize?: ClientRect): void { + var size = charSize || this.charMeasureElement.getBoundingClientRect(); + if (size.height > 0) { + var rowHeightChanged = size.height !== this.currentRowHeight; + if (rowHeightChanged) { + this.currentRowHeight = size.height; + this.viewportElement.style.lineHeight = size.height + 'px'; + this.terminal.rowContainer.style.lineHeight = size.height + 'px'; + } + var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; + if (rowHeightChanged || viewportHeightChanged) { + this.lastRecordedViewportHeight = this.terminal.rows; + this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; + } + this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px'; + } + } + + /** + * Updates dimensions and synchronizes the scroll area if necessary. + */ + public syncScrollArea(): void { + if (this.lastRecordedBufferLength !== this.terminal.lines.length) { + // If buffer height changed + this.lastRecordedBufferLength = this.terminal.lines.length; + this.refresh(); + } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { + // If viewport height changed + this.refresh(); + } else { + // If size has changed, refresh viewport + var size = this.charMeasureElement.getBoundingClientRect(); + if (size.height !== this.currentRowHeight) { + this.refresh(size); + } + } + + // Sync scrollTop + var scrollTop = this.terminal.ydisp * this.currentRowHeight; + if (this.viewportElement.scrollTop !== scrollTop) { + this.viewportElement.scrollTop = scrollTop; + } + } + + /** + * Handles scroll events on the viewport, calculating the new viewport and requesting the + * terminal to scroll to it. + * @param ev The scroll event. + */ + private onScroll(ev: Event) { + var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); + var diff = newRow - this.terminal.ydisp; + this.terminal.scrollDisp(diff, true); + } + + /** + * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual + * scrolling to `onScroll`, this event needs to be attached manually by the consumer of + * `Viewport`. + * @param ev The mouse wheel event. + */ + public onWheel(ev: WheelEvent) { + if (ev.deltaY === 0) { + // Do nothing if it's not a vertical scroll event + return; + } + // Fallback to WheelEvent.DOM_DELTA_PIXEL + var multiplier = 1; + if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { + multiplier = this.currentRowHeight; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + multiplier = this.currentRowHeight * this.terminal.rows; + } + this.viewportElement.scrollTop += ev.deltaY * multiplier; + // Prevent the page from scrolling when the terminal scrolls + ev.preventDefault(); + }; +} diff --git a/src/test/test.js b/src/test/test.js index b4330050..abdc8c17 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -193,6 +193,7 @@ describe('xterm.js', function() { }; var event = { type: 'keydown', + keyCode: 0, preventDefault: function(){}, stopPropagation: function(){} }; @@ -218,7 +219,7 @@ describe('xterm.js', function() { assert.equal(xterm.ydisp, startYDisp); xterm.scrollDisp(-1); assert.equal(xterm.ydisp, startYDisp - 1); - xterm.keyDown(); + xterm.keyDown({ keyCode: 0 }); assert.equal(xterm.ydisp, startYDisp - 1); }); }); diff --git a/src/utils/Keyboard.test.ts b/src/utils/Keyboard.test.ts new file mode 100644 index 00000000..618d3331 --- /dev/null +++ b/src/utils/Keyboard.test.ts @@ -0,0 +1,21 @@ +import { assert } from 'chai'; +import * as Keyboard from './Keyboard'; + +describe('Keyboard', () => { + describe('isModifierOnlyKeyboardEvent', () => { + it('should return true when only modifier keys are used', () => { + // Note that KeyboardEvent.keyCode is deprecated but we're using it to improve browser + // compatibility. This helper returns the `any` type because KeyboardEvent doesn't exist under + // NodeJS. + function createEvent(keyCode: number): any { + return { keyCode }; + } + assert.isTrue(Keyboard.isModifierOnlyKeyboardEvent(createEvent(16))); + assert.isTrue(Keyboard.isModifierOnlyKeyboardEvent(createEvent(17))); + assert.isTrue(Keyboard.isModifierOnlyKeyboardEvent(createEvent(18))); + assert.isTrue(Keyboard.isModifierOnlyKeyboardEvent(createEvent(91))); + assert.isFalse(Keyboard.isModifierOnlyKeyboardEvent(createEvent(19))); + assert.isFalse(Keyboard.isModifierOnlyKeyboardEvent(createEvent(90))); + }); + }); +}); diff --git a/src/utils/Keyboard.ts b/src/utils/Keyboard.ts new file mode 100644 index 00000000..e43cba45 --- /dev/null +++ b/src/utils/Keyboard.ts @@ -0,0 +1,22 @@ +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company (MIT License) + */ + +/** + * Keyboard utilities module. This module contains utilities for dealing with keyboard interaction. + * @module xterm/utils/Keyboard + */ + +/** + * Gets whether a KeyboardEvent is made up entirely of modifier keys. + * + * @param event The event to check. + * @return Whether the KeyboardEvent is made up entirely of modifier keys. + */ +export function isModifierOnlyKeyboardEvent(event: KeyboardEvent): boolean { + return event.keyCode === 16 || // Shift + event.keyCode === 17 || // Control + event.keyCode === 18 || // Alt + event.keyCode === 91; // Meta +} diff --git a/src/xterm.js b/src/xterm.js index d47da683..c88423d4 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -36,6 +36,7 @@ import { EventEmitter } from './EventEmitter.js'; import { Viewport } from './Viewport.js'; import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js'; import * as Browser from './utils/Browser'; +import * as Keyboard from './utils/Keyboard'; /** * Terminal Emulation References: @@ -2430,7 +2431,7 @@ Terminal.prototype.keyDown = function(ev) { } // Scroll down to prompt, whenever the user presses a key. - if (this.ybase !== this.ydisp) { + if (!Keyboard.isModifierOnlyKeyboardEvent(ev) && this.ybase !== this.ydisp) { this.scrollToBottom(); } diff --git a/tsconfig.json b/tsconfig.json index beb83261..01e72bc1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "target": "es5", "rootDir": "src", "allowJs": true, - "outDir": "out" + "outDir": "out", + "sourceMap": true }, "exclude": [ "addons",