Skip to content

Commit

Permalink
Merge pull request #293 from sourcelair/issue-#202-fix-paste
Browse files Browse the repository at this point in the history
Allow right-click paste (again)
  • Loading branch information
parisk authored Oct 1, 2016
2 parents ab7d63a + a6f0473 commit 9115846
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 67 deletions.
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);
on(this.textarea, 'paste', function (ev) {
pasteHandler.call(this, ev, term);
});
on(this.element, 'contextmenu', function (ev) {
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

0 comments on commit 9115846

Please sign in to comment.