mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
+25
-9
@@ -43,7 +43,8 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal.focus {
|
||||
.terminal.focus,
|
||||
.terminal:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -103,6 +104,29 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.terminal .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.terminal .xterm-rows {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.terminal .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.terminal .xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: -9999em;
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine default colors for xterm.js
|
||||
*/
|
||||
@@ -2169,11 +2193,3 @@
|
||||
.terminal .xterm-bg-color-255 {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
/**
|
||||
* All terminal rows should have explicitly declared height,
|
||||
* in order to allow child elements to adjust.
|
||||
*/
|
||||
.terminal .xterm-rows > div {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
+161
-23
@@ -219,7 +219,7 @@
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
||||
@@ -275,7 +275,7 @@
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply any changes made to the textarea after the current event chain is allowed to complete.
|
||||
@@ -296,7 +296,7 @@
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Positions the composition view on top of the cursor and the textarea just below it (so the
|
||||
@@ -322,8 +322,124 @@
|
||||
CompositionHelper.prototype.clearTextareaPosition = function() {
|
||||
this.textarea.style.left = '';
|
||||
this.textarea.style.top = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (size.height !== this.currentRowHeight) {
|
||||
this.currentRowHeight = size.height;
|
||||
this.viewportElement.style.lineHeight = size.height + 'px';
|
||||
this.terminal.rowContainer.style.lineHeight = size.height + 'px';
|
||||
}
|
||||
if (this.lastRecordedViewportHeight !== this.terminal.rows) {
|
||||
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.isApplicationMode) {
|
||||
this.lastRecordedBufferLength = this.terminal.rows;
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
|
||||
this.lastRecordedBufferLength = this.terminal.lines.length;
|
||||
this.refresh();
|
||||
} else {
|
||||
// If size has changed, refresh viewport
|
||||
var size = this.charMeasureElement.getBoundingClientRect();
|
||||
if (size.height !== this.currentRowHeight) {
|
||||
this.refresh(size);
|
||||
}
|
||||
}
|
||||
var scrollTop = this.terminal.ydisp * this.currentRowHeight;
|
||||
if (this.viewportElement.scrollTop !== scrollTop) {
|
||||
this.viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the application mode of the viewport.
|
||||
* @param {boolean} isApplicationMode Sets whether the terminal is in application mode. true
|
||||
* for application mode (DECKPAM) and false for normal mode (DECKPNM).
|
||||
*/
|
||||
Viewport.prototype.setApplicationMode = function(isApplicationMode) {
|
||||
this.isApplicationMode = isApplicationMode;
|
||||
this.syncScrollArea();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
};
|
||||
|
||||
/**
|
||||
* States
|
||||
*/
|
||||
@@ -787,7 +903,6 @@
|
||||
return row;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Opens the terminal within an element.
|
||||
*
|
||||
@@ -841,8 +956,17 @@
|
||||
this.element.classList.add('terminal');
|
||||
this.element.classList.add('xterm');
|
||||
this.element.classList.add('xterm-theme-' + this.theme);
|
||||
|
||||
this.element.style.height
|
||||
this.element.setAttribute('tabindex', 0);
|
||||
|
||||
this.viewportElement = document.createElement('div');
|
||||
this.viewportElement.classList.add('xterm-viewport');
|
||||
this.element.appendChild(this.viewportElement);
|
||||
this.viewportScrollArea = document.createElement('div');
|
||||
this.viewportScrollArea.classList.add('xterm-scroll-area');
|
||||
this.viewportElement.appendChild(this.viewportScrollArea);
|
||||
|
||||
/*
|
||||
* Create the container that will hold the lines of the terminal and then
|
||||
* produce the lines the lines.
|
||||
@@ -879,11 +1003,17 @@
|
||||
this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this);
|
||||
this.helperContainer.appendChild(this.compositionView);
|
||||
|
||||
this.charMeasureElement = document.createElement('div');
|
||||
this.charMeasureElement.classList.add('xterm-char-measure-element');
|
||||
this.charMeasureElement.innerHTML = 'W';
|
||||
this.helperContainer.appendChild(this.charMeasureElement);
|
||||
|
||||
for (; i < this.rows; i++) {
|
||||
this.insertRow();
|
||||
}
|
||||
this.parent.appendChild(this.element);
|
||||
|
||||
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement);
|
||||
|
||||
// Draw the screen.
|
||||
this.refresh(0, this.rows - 1);
|
||||
@@ -949,11 +1079,10 @@
|
||||
*/
|
||||
Terminal.prototype.bindMouse = function() {
|
||||
var el = this.element, self = this, pressed = 32;
|
||||
var wheelEvent = ('onmousewheel' in this.context) ? 'mousewheel' : 'DOMMouseScroll';
|
||||
|
||||
// mouseup, mousedown, mousewheel
|
||||
// mouseup, mousedown, wheel
|
||||
// left click: ^[[M 3<^[[M#3<
|
||||
// mousewheel up: ^[[M`3>
|
||||
// wheel up: ^[[M`3>
|
||||
function sendButton(ev) {
|
||||
var button
|
||||
, pos;
|
||||
@@ -976,7 +1105,7 @@
|
||||
// button, just in case.
|
||||
pressed = 32;
|
||||
break;
|
||||
case wheelEvent:
|
||||
case 'wheel':
|
||||
// nothing. don't
|
||||
// interfere with
|
||||
// `pressed`.
|
||||
@@ -1137,7 +1266,7 @@
|
||||
? 64
|
||||
: 65;
|
||||
break;
|
||||
case 'mousewheel':
|
||||
case 'wheel':
|
||||
button = ev.wheelDeltaY > 0
|
||||
? 64
|
||||
: 65;
|
||||
@@ -1207,9 +1336,7 @@
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
type: (ev.overrideType || ev.type) === wheelEvent
|
||||
? 'mousewheel'
|
||||
: (ev.overrideType || ev.type)
|
||||
type: 'wheel'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1250,7 +1377,7 @@
|
||||
// on(self.document, 'mousemove', sendMove);
|
||||
//}
|
||||
|
||||
on(el, wheelEvent, function(ev) {
|
||||
on(el, 'wheel', function(ev) {
|
||||
if (!self.mouseEvents) return;
|
||||
if (self.x10Mouse
|
||||
|| self.vt300Mouse
|
||||
@@ -1259,16 +1386,12 @@
|
||||
return self.cancel(ev);
|
||||
});
|
||||
|
||||
// allow mousewheel scrolling in
|
||||
// allow wheel scrolling in
|
||||
// the shell for example
|
||||
on(el, wheelEvent, function(ev) {
|
||||
on(el, 'wheel', function(ev) {
|
||||
if (self.mouseEvents) return;
|
||||
if (self.applicationKeypad) return;
|
||||
if (ev.type === 'DOMMouseScroll') {
|
||||
self.scrollDisp(ev.detail < 0 ? -1 : 1);
|
||||
} else {
|
||||
self.scrollDisp(ev.wheelDeltaY > 0 ? -1 : 1);
|
||||
}
|
||||
self.viewport.onWheel(ev);
|
||||
return self.cancel(ev);
|
||||
});
|
||||
};
|
||||
@@ -1574,13 +1697,18 @@
|
||||
// this.maxRange();
|
||||
this.updateRange(this.scrollTop);
|
||||
this.updateRange(this.scrollBottom);
|
||||
|
||||
this.emit('scroll', this.ydisp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal
|
||||
* @param {number} disp The number of lines to scroll down (negatives scroll up).
|
||||
* @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used
|
||||
* to avoid unwanted events being handled by the veiwport when the event was triggered from the
|
||||
* viewport originally.
|
||||
*/
|
||||
Terminal.prototype.scrollDisp = function(disp) {
|
||||
Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
|
||||
this.ydisp += disp;
|
||||
|
||||
if (this.ydisp > this.ybase) {
|
||||
@@ -1589,6 +1717,10 @@
|
||||
this.ydisp = 0;
|
||||
}
|
||||
|
||||
if (!suppressScrollEvent) {
|
||||
this.emit('scroll', this.ydisp);
|
||||
}
|
||||
|
||||
this.refresh(0, this.rows - 1);
|
||||
};
|
||||
|
||||
@@ -1604,6 +1736,7 @@
|
||||
|
||||
if (this.ybase !== this.ydisp) {
|
||||
this.ydisp = this.ybase;
|
||||
this.emit('scroll', this.ydisp);
|
||||
this.maxRange();
|
||||
}
|
||||
|
||||
@@ -1930,17 +2063,19 @@
|
||||
this.tabSet();
|
||||
break;
|
||||
|
||||
// ESC = Application Keypad (DECPAM).
|
||||
// ESC = Application Keypad (DECKPAM).
|
||||
case '=':
|
||||
this.log('Serial port requested application keypad.');
|
||||
this.applicationKeypad = true;
|
||||
this.viewport.setApplicationMode(true);
|
||||
this.state = normal;
|
||||
break;
|
||||
|
||||
// ESC > Normal Keypad (DECPNM).
|
||||
// ESC > Normal Keypad (DECKPNM).
|
||||
case '>':
|
||||
this.log('Switching back to normal keypad.');
|
||||
this.applicationKeypad = false;
|
||||
this.viewport.setApplicationMode(false);
|
||||
this.state = normal;
|
||||
break;
|
||||
|
||||
@@ -4184,6 +4319,7 @@
|
||||
case 66:
|
||||
this.log('Serial port requested application keypad.');
|
||||
this.applicationKeypad = true;
|
||||
this.viewport.setApplicationMode(true);
|
||||
break;
|
||||
case 9: // X10 Mouse
|
||||
// no release, no motion, no wheel, no modifiers.
|
||||
@@ -4382,6 +4518,7 @@
|
||||
break;
|
||||
case 66:
|
||||
this.log('Switching back to normal keypad.');
|
||||
this.viewport.setApplicationMode(false);
|
||||
this.applicationKeypad = false;
|
||||
break;
|
||||
case 9: // X10 Mouse
|
||||
@@ -5251,6 +5388,7 @@
|
||||
|
||||
Terminal.EventEmitter = EventEmitter;
|
||||
Terminal.CompositionHelper = CompositionHelper;
|
||||
Terminal.Viewport = Viewport;
|
||||
Terminal.inherits = inherits;
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,14 +19,14 @@ describe('CompositionHelper', function () {
|
||||
top: 0
|
||||
},
|
||||
textContent: ''
|
||||
}
|
||||
};
|
||||
textarea = {
|
||||
value: '',
|
||||
style: {
|
||||
left: 0,
|
||||
top: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
terminal = {
|
||||
element: {
|
||||
querySelector: function () {
|
||||
@@ -36,7 +36,7 @@ describe('CompositionHelper', function () {
|
||||
handler: function (text) {
|
||||
handledText += text;
|
||||
}
|
||||
}
|
||||
};
|
||||
handledText = '';
|
||||
compositionHelper = new Terminal.CompositionHelper(textarea, compositionView, terminal);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
var assert = require('chai').assert;
|
||||
var Terminal = require('../src/xterm');
|
||||
|
||||
describe('Viewport', function () {
|
||||
var terminal;
|
||||
var viewportElement;
|
||||
var charMeasureElement;
|
||||
var viewport;
|
||||
|
||||
var CHARACTER_HEIGHT = 10;
|
||||
|
||||
beforeEach(function () {
|
||||
terminal = {
|
||||
lines: [],
|
||||
rows: 0,
|
||||
ydisp: 0,
|
||||
on: function () {},
|
||||
rowContainer: {
|
||||
style: {
|
||||
lineHeight: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
viewportElement = {
|
||||
addEventListener: function () {},
|
||||
style: {
|
||||
height: 0,
|
||||
lineHeight: 0
|
||||
}
|
||||
};
|
||||
scrollAreaElement = {
|
||||
style: {
|
||||
height: 0
|
||||
}
|
||||
};
|
||||
charMeasureElement = {
|
||||
getBoundingClientRect: function () {
|
||||
return { width: null, height: CHARACTER_HEIGHT };
|
||||
}
|
||||
};
|
||||
viewport = new Terminal.Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement);
|
||||
});
|
||||
|
||||
describe('Public API', function () {
|
||||
it('should define Viewport.prototype.onWheel', function () {
|
||||
assert.isDefined(Terminal.Viewport.prototype.onWheel);
|
||||
});
|
||||
it('should define Viewport.prototype.setApplicationMode', function () {
|
||||
assert.isDefined(Terminal.Viewport.prototype.setApplicationMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setApplicationMode', function () {
|
||||
it('should restrict the scroll area to the viewport', function () {
|
||||
terminal.lines.push('');
|
||||
terminal.lines.push('');
|
||||
terminal.rows = 1;
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
viewport.setApplicationMode(true);
|
||||
assert.equal(scrollAreaElement.style.height, CHARACTER_HEIGHT + 'px');
|
||||
viewport.setApplicationMode(false);
|
||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh', function () {
|
||||
it('should set the line-height of the terminal', function () {
|
||||
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
|
||||
charMeasureElement.getBoundingClientRect = function () {
|
||||
return { width: null, height: 1 };
|
||||
};
|
||||
viewport.refresh();
|
||||
assert.equal(viewportElement.style.lineHeight, '1px');
|
||||
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncScrollArea', function () {
|
||||
it('should sync the scroll area', function () {
|
||||
terminal.lines.push('');
|
||||
terminal.rows = 1;
|
||||
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
terminal.lines.push('');
|
||||
viewport.syncScrollArea();
|
||||
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
|
||||
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user