import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './EventEmitter';
import { Viewport } from './Viewport';
-import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard';
+import { moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
import { CircularList } from './utils/CircularList';
import { C0 } from './EscapeSequences';
import { InputHandler } from './InputHandler';
import { Parser } from './Parser';
import { Renderer } from './Renderer';
import { Linkifier } from './Linkifier';
+import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './utils/Browser';
-import * as Keyboard from './utils/Keyboard';
+import * as Mouse from './utils/Mouse';
import { CHARSETS } from './Charsets';
import { getRawByteCoords } from './utils/Mouse';
this.parser = new Parser(this.inputHandler, this);
// Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
this.renderer = this.renderer || null;
+ this.selectionManager = this.selectionManager || null;
this.linkifier = this.linkifier || new Linkifier();
// user input states
while (i--) {
this.lines.push(this.blankLine());
}
+ // Ensure the selection manager has the correct buffer
+ if (this.selectionManager) {
+ this.selectionManager.setBuffer(this.lines);
+ }
this.tabs;
this.setupStops();
Terminal.bindBlur(this);
// Bind clipboard functionality
- on(this.element, 'copy', function (ev) {
- copyHandler.call(this, ev, term);
- });
- on(this.textarea, 'paste', function (ev) {
- pasteHandler.call(this, ev, term);
- });
- on(this.element, 'paste', function (ev) {
- pasteHandler.call(this, ev, term);
+ on(this.element, 'copy', event => {
+ // If mouse events are active it means the selection manager is disabled and
+ // copy should be handled by the host program.
+ if (this.mouseEvents) {
+ return;
+ }
+ copyHandler(event, term, this.selectionManager);
});
+ const pasteHandlerWrapper = event => pasteHandler(event, term);
+ on(this.textarea, 'paste', pasteHandlerWrapper);
+ on(this.element, 'paste', pasteHandlerWrapper);
- function rightClickHandlerWrapper (ev) {
- rightClickHandler.call(this, ev, term);
- }
-
+ // Handle right click context menus
if (term.browser.isFirefox) {
- on(this.element, 'mousedown', function (ev) {
+ on(this.element, 'mousedown', event => {
if (ev.button == 2) {
- rightClickHandlerWrapper(ev);
+ moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
}
});
} else {
- on(this.element, 'contextmenu', rightClickHandlerWrapper);
+ on(this.element, 'contextmenu', event => {
+ moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
+ });
+ }
+
+ // Move the textarea under the cursor when middle clicking on Linux to ensure
+ // middle click to paste selection works. This only appears to work in Chrome
+ // at the time is writing.
+ if (term.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, 'click', event => {
+ if (event.button === 1) {
+ moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
+ }
+ });
}
};
this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea);
+ // Create the selection container. This needs to be added before the
+ // rowContainer as the selection must be below the text.
+ this.selectionContainer = document.createElement('div');
+ this.selectionContainer.classList.add('xterm-selection');
+ this.element.appendChild(this.selectionContainer);
+
// Create the container that will hold the lines of the terminal and then
// produce the lines the lines.
this.rowContainer = document.createElement('div');
this.charMeasure = new CharMeasure(document, this.helperContainer);
this.charMeasure.on('charsizechanged', function () {
- self.updateCharSizeCSS();
+ self.updateCharSizeStyles();
});
this.charMeasure.measure();
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
this.renderer = new Renderer(this);
+ this.selectionManager = new SelectionManager(this, this.lines, this.rowContainer, this.charMeasure);
+ this.selectionManager.on('refresh', data => {
+ this.renderer.refreshSelection(data.start, data.end);
+ });
+ this.selectionManager.on('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.selectionManager.refresh());
+ this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
// Setup loop that draws to screen
this.refresh(0, this.rows - 1);
* Updates the helper CSS class with any changes necessary after the terminal's
* character width has been changed.
*/
-Terminal.prototype.updateCharSizeCSS = function() {
+Terminal.prototype.updateCharSizeStyles = function() {
this.charSizeStyleElement.textContent =
`.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
- `.xterm-normal-char{width:${this.charMeasure.width}px;}`
+ `.xterm-normal-char{width:${this.charMeasure.width}px;}` +
+ `.xterm-rows > div{height:${this.charMeasure.height}px;}`;
}
/**
/**
* Scroll the terminal down 1 row, creating a blank line.
+ * @param {boolean} isWrapped Whether the new line is wrapped from the previous
+ * line.
*/
-Terminal.prototype.scroll = function() {
+Terminal.prototype.scroll = function(isWrapped) {
var row;
// Make room for the new row in lines
if (row === this.lines.length) {
// Optimization: pushing is faster than splicing when they amount to the same behavior
- this.lines.push(this.blankLine());
+ this.lines.push(this.blankLine(undefined, isWrapped));
} else {
// add our new line
- this.lines.splice(row, 0, this.blankLine());
+ this.lines.splice(row, 0, this.blankLine(undefined, isWrapped));
}
if (this.scrollTop !== 0) {
*/
Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
if (disp < 0) {
+ if (this.ydisp === 0) {
+ return;
+ }
this.userScrolling = true;
} else if (disp + this.ydisp >= this.ybase) {
this.userScrolling = false;
}
}
+/**
+ * Gets whether the terminal has an active selection.
+ */
+Terminal.prototype.hasSelection = function() {
+ return this.selectionManager.hasSelection;
+}
+
+/**
+ * Gets the terminal's current selection, this is useful for implementing copy
+ * behavior outside of xterm.js.
+ */
+Terminal.prototype.getSelection = function() {
+ return this.selectionManager.selectionText;
+}
+
+/**
+ * Clears the current terminal selection.
+ */
+Terminal.prototype.clearSelection = function() {
+ this.selectionManager.clearSelection();
+}
+
+/**
+ * Selects all text within the terminal.
+ */
+Terminal.prototype.selectAll = function() {
+ this.selectionManager.selectAll();
+}
+
/**
* Handle a keydown event
* Key Resources:
} else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
result.key = C0.ESC + (ev.keyCode - 48);
}
+ } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
+ if (ev.keyCode === 65) { // cmd + a
+ this.selectAll();
+ }
}
break;
}
/**
* Return the data array of a blank line
* @param {number} cur First bunch of data for each "blank" character.
+ * @param {boolean} isWrapped Whether the new line is wrapped from the previous line.
*/
-Terminal.prototype.blankLine = function(cur) {
+Terminal.prototype.blankLine = function(cur, isWrapped) {
var attr = cur
? this.eraseAttr()
: this.defAttr;
, line = []
, i = 0;
+ // TODO: It is not ideal that this is a property on an array, a buffer line
+ // class should be added that will hold this data and other useful functions.
+ if (isWrapped) {
+ line.isWrapped = isWrapped;
+ }
+
for (; i < this.cols; i++) {
line[i] = ch;
}