diff --git a/jsdoc.json b/jsdoc.json index 66b174e5..6879aa2b 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -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", diff --git a/src/handlers/Clipboard.js b/src/handlers/Clipboard.js new file mode 100644 index 00000000..bfd33409 --- /dev/null +++ b/src/handlers/Clipboard.js @@ -0,0 +1,126 @@ +/** + * xterm.js: xterm, in the browser + * Copyright (c) 2016, SourceLair Private Company (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 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   + // 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 +}; diff --git a/src/xterm.css b/src/xterm.css index bbf54582..435cf0ee 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -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; diff --git a/src/xterm.js b/src/xterm.js index c49e9a70..83592bb7 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -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: @@ -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; }; /** @@ -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 diff --git a/test/clipboard-test.js b/test/clipboard-test.js new file mode 100644 index 00000000..f1688df1 --- /dev/null +++ b/test/clipboard-test.js @@ -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); + }); +}); diff --git a/test/test.js b/test/test.js index a015b455..b5dc20ec 100644 --- a/test/test.js +++ b/test/test.js @@ -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() {},