Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow right-click paste (again) #293

Merged
merged 4 commits into from
Oct 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jsdoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"source": {
"include": [
"src/xterm.js",
"src/handlers/Clipboard.js",
"addons/attach/attach.js",
"addons/fit/fit.js",
"addons/fullscreen/fullscreen.js",
Expand Down
126 changes: 126 additions & 0 deletions src/handlers/Clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* xterm.js: xterm, in the browser
* Copyright (c) 2016, SourceLair Private Company <www.sourcelair.com> (MIT License)
*/

/**
* Clipboard handler module. This module contains methods for handling all
* clipboard-related events appropriately in the terminal.
* @module xterm/handlers/Clipboard
*/

/**
* Prepares text copied from terminal selection, to be saved in the clipboard by:
* 1. stripping all trailing white spaces
* 2. converting all non-breaking spaces to regular spaces
* @param {string} text The copied text that needs processing for storing in clipboard
* @returns {string}
*/
function prepareTextForClipboard(text) {
var space = String.fromCharCode(32),
nonBreakingSpace = String.fromCharCode(160),
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
processedText = text.split('\n').map(function (line) {
// Strip all trailing white spaces and convert all non-breaking spaces
// to regular spaces.
var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);

return processedLine;
}).join('\n');

return processedText;
}

/**
* Binds copy functionality to the given terminal.
* @param {ClipboardEvent} ev The original copy event to be handled
*/
function copyHandler (ev) {
var copiedText = window.getSelection().toString(),
text = prepareTextForClipboard(copiedText);

ev.clipboardData.setData('text/plain', text);
ev.preventDefault(); // Prevent or the original text will be copied.
}

/**
* Redirect the clipboard's data to the terminal's input handler.
* @param {ClipboardEvent} ev The original paste event to be handled
* @param {Terminal} term The terminal on which to apply the handled paste event
*/
function pasteHandler(ev, term) {
ev.stopPropagation();
if (ev.clipboardData) {
var text = ev.clipboardData.getData('text/plain');
term.handler(text);
term.textarea.value = '';
return term.cancel(ev);
}
}

/**
* Bind to right-click event and allow right-click copy and paste.
*
* **Logic**
* If text is selected and right-click happens on selected text, then
* do nothing to allow seamless copying.
* If no text is selected or right-click is outside of the selection
* area, then bring the terminal's input below the cursor, in order to
* trigger the event on the textarea and allow-right click paste, without
* caring about disappearing selection.
* @param {ClipboardEvent} ev The original paste event to be handled
* @param {Terminal} term The terminal on which to apply the handled paste event
*/
function rightClickHandler(ev, term) {
var s = document.getSelection(),
sText = prepareTextForClipboard(s.toString()),
r = s.getRangeAt(0);

var x = ev.clientX,
y = ev.clientY;

var cr = r.getClientRects(),
clickIsOnSelection = false,
i, rect;

for (i=0; i<cr.length; i++) {
rect = cr[i];
clickIsOnSelection = (
(x > rect.left) && (x < rect.right) &&
(y > rect.top) && (y < rect.bottom)
);
// If we clicked on selection and selection is not a single space,
// then mark the right click as copy-only. We check for the single
// space selection, as this can happen when clicking on an &nbsp;
// and there is not much pointing in copying a single space.
// Single space is char
if (clickIsOnSelection && (sText !== ' ')) {
break;
}
}

// Bring textarea at the cursor position
if (!clickIsOnSelection) {
term.textarea.style.position = 'fixed';
term.textarea.style.width = '10px';
term.textarea.style.height = '10px';
term.textarea.style.left = x + 'px';
term.textarea.style.top = y + 'px';
term.textarea.style.zIndex = 1000;
term.textarea.focus();

// Reset the terminal textarea's styling
setTimeout(function () {
term.textarea.style.position = null;
term.textarea.style.width = null;
term.textarea.style.height = null;
term.textarea.style.left = null;
term.textarea.style.top = null;
term.textarea.style.zIndex = null;
}, 1);
}
}

export {
prepareTextForClipboard, copyHandler, pasteHandler, rightClickHandler
};
4 changes: 2 additions & 2 deletions src/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@
}

.terminal .xterm-helper-textarea {
position: absolute;
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
left: -9999em;
position: absolute;
opacity: 0;
left: -9999em;
width: 0;
height: 0;
z-index: -10;
Expand Down
61 changes: 10 additions & 51 deletions src/xterm.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import { CompositionHelper } from './CompositionHelper.js';
import { EventEmitter } from './EventEmitter.js';
import { Viewport } from './Viewport.js';
import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js';

/**
* Terminal Emulation References:
Expand Down Expand Up @@ -431,52 +432,20 @@ Terminal.bindBlur = function (term) {
* Initialize default behavior
*/
Terminal.prototype.initGlobal = function() {
Terminal.bindPaste(this);
var term = this;

Terminal.bindKeys(this);
Terminal.bindCopy(this);
Terminal.bindFocus(this);
Terminal.bindBlur(this);
};

/**
* Bind to paste event and allow both keyboard and right-click pasting, without having the
* contentEditable value set to true.
*/
Terminal.bindPaste = function(term) {
on([term.textarea, term.element], 'paste', function(ev) {
ev.stopPropagation();
if (ev.clipboardData) {
var text = ev.clipboardData.getData('text/plain');
term.handler(text);
term.textarea.value = '';
return term.cancel(ev);
}
// Bind clipboard functionality
on(this.element, 'copy', copyHandler);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should copy be exposed via the API so it can be done programmatically?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All methods are already exposed in Clipboard.js. This also would not work in browsers, so I am skeptical about exposing such a public API.

on(this.textarea, 'paste', function (ev) {
pasteHandler.call(this, ev, term);
});
on(this.element, 'contextmenu', function (ev) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might conflict with vscode which overrides the context menu to show options to create new terminal, paste and copy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? I am not cancelling the event.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parisk wouldn't it cause 2 context menus to appear or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. All I am doing here is move the textarea when the context menu appears. Is there any easy way to try this out?

rightClickHandler.call(this, ev, term);
});
};

/**
* Prepares text copied from terminal selection, to be saved in the clipboard by:
* 1. stripping all trailing white spaces
* 2. converting all non-breaking spaces to regular spaces
* @param {string} text The copied text that needs processing for storing in clipboard
* @returns {string}
* @static
*/
Terminal.prepareCopiedTextForClipboard = function (text) {
var space = String.fromCharCode(32),
nonBreakingSpace = String.fromCharCode(160),
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
processedText = text.split('\n').map(function (line) {
/**
* Strip all trailing white spaces and convert all non-breaking spaces to regular
* spaces.
*/
var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);

return processedLine;
}).join('\n');

return processedText;
};

/**
Expand Down Expand Up @@ -515,16 +484,6 @@ Terminal.bindKeys = function(term) {
term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));
};

/**
* Binds copy functionality to the given terminal.
* @static
*/
Terminal.bindCopy = function(term) {
on(term.element, 'copy', function(ev) {
return; // temporary
});
};


/**
* Insert the given row to the terminal or produce a new one
Expand Down
18 changes: 18 additions & 0 deletions test/clipboard-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var assert = require('chai').assert;
var Terminal = require('../src/xterm');
var Clipboard = require('../src/handlers/Clipboard');


describe('evaluateCopiedTextProcessing', function () {
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
var nonBreakingSpace = String.fromCharCode(160),
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
processedText = Clipboard.prepareTextForClipboard(copiedText);

// No trailing spaces
assert.equal(processedText.match(/\s+$/), null);

// No non-breaking space
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
});
});
14 changes: 0 additions & 14 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,20 +245,6 @@ describe('xterm.js', function() {
});
});

describe('evaluateCopiedTextProcessing', function () {
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
var nonBreakingSpace = String.fromCharCode(160),
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
processedText = Terminal.prepareCopiedTextForClipboard(copiedText);

// No trailing spaces
assert.equal(processedText.match(/\s+$/), null);

// No non-breaking space
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
});
});

describe('Third level shift', function() {
var evKeyDown = {
preventDefault: function() {},
Expand Down