From fc7b22dc485a52e736fd85bae0aaba9bbdd63c80 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 16:03:31 -0700 Subject: [PATCH 01/38] Manually revert textarea changes --- src/xterm.css | 17 ++++ src/xterm.js | 234 ++++++++++++++++++-------------------------------- 2 files changed, 101 insertions(+), 150 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index b34f41a22..348fc097f 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -47,6 +47,23 @@ outline: none; } +.terminal .xterm-helpers { + position: absolute; + top: 0; +} + +.terminal .xterm-helper-textarea { + /* + * HACK: to fix IE's blinking cursor + * Move textarea out of the screen to the far left, so that the cursor is not visible. + */ + text-indent: -9999em; + opacity: 0; + width: 0; + height: 0; + z-index: -10; +} + .terminal .terminal-cursor { background-color: #fff; color: #000; diff --git a/src/xterm.js b/src/xterm.js index 8633ff96e..30e6aee5f 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -108,8 +108,8 @@ var self = this; function on() { var args = Array.prototype.slice.call(arguments); - self.removeListener(type, on); - return listener.apply(self, args); + this.removeListener(type, on); + return listener.apply(this, args); } on.listener = listener; return this.on(type, on); @@ -434,7 +434,17 @@ * @public */ Terminal.prototype.focus = function() { - return this.element.focus(); + if (document.activeElement === this.textarea) { + return; + } + + if (this.sendFocus) { + this.send('\x1b[I'); + } + + this.showCursor(); + this.textarea.focus(); + Terminal.focus = this; }; /** @@ -460,7 +470,17 @@ * @public */ Terminal.prototype.blur = function() { - return this.element.blur(); + if (Terminal.focus !== this) { + return; + } + + this.cursorState = 0; + this.refresh(this.y, this.y); + this.textarea.blur(); + if (this.sendFocus) { + this.send('\x1b[0]'); + } + Terminal.focus = null; }; /** @@ -482,48 +502,9 @@ * Initialize default behavior */ Terminal.prototype.initGlobal = function() { - Terminal.bindKeys(this); Terminal.bindPaste(this); + Terminal.bindKeys(this); Terminal.bindCopy(this); - Terminal.bindCut(this); - Terminal.bindDrop(this); - Terminal.bindFocus(this); - Terminal.bindBlur(this); - }; - - /** - * Clears all selected text, inside the terminal. - */ - Terminal.prototype.clearSelection = function() { - var selectionBaseNode = window.getSelection().baseNode; - - if (selectionBaseNode && (this.element.contains(selectionBaseNode.parentElement))) { - window.getSelection().removeAllRanges(); - } - }; - - /** - * This function temporarily enables (leases) the contentEditable value of the terminal, which - * should be set back to false within 5 seconds at most. - */ - Terminal.prototype.leaseContentEditable = function (ms, callback) { - var term = this; - - term.element.contentEditable = true; - - /** - * Blur and re-focus instantly. This is due to a weird focus state on Chrome, when setting - * contentEditable to true on a focused element. - */ - term.blur(); - term.focus(); - - setTimeout(function () { - term.element.contentEditable = false; - if (typeof callback == 'function') { - callback.call(term); - } - }, ms || 5000); }; /** @@ -531,70 +512,15 @@ * contentEditable value set to true. */ Terminal.bindPaste = function(term) { - on(term.element, 'paste', function(ev) { + on([term.textarea, term.element], 'paste', function(ev) { + ev.stopPropagation(); if (ev.clipboardData) { var text = ev.clipboardData.getData('text/plain'); - term.emit('paste', text, ev); term.handler(text); - /** - * Cancel the paste event, or else things will be pasted twice: - * 1. by the terminal handler - * 2. by the browser, because of the contentEditable value being true - */ - term.cancel(ev, true); - - /** - * After the paste event is completed, always set the contentEditable value to false. - */ - term.element.contentEditable = false; + term.textarea.value = ''; + return term.cancel(ev); } }); - - /** - * Hack pasting with keyboard, in order to make it work without contentEditable. - * When a user types Ctrl + Shift + V or Shift + Insert on a non Mac or Cmd + V on a Mac, - * lease the contentEditable value as true. - */ - on(term.element, 'keydown', function (ev) { - var isEditable = term.element.contentEditable === "true"; - - /** - * If on a Mac, lease the contentEditable value temporarily, when the user presses - * the Cmd button, in a keydown event order to paste frictionlessly. - */ - if (term.isMac && ev.metaKey && !isEditable) { - term.leaseContentEditable(5000); - } - - if (!term.isMac && !isEditable) { - if ((ev.keyCode == 45 && ev.shiftKey && !ev.ctrlKey) || // Shift + Insert - (ev.keyCode == 86 && ev.shiftKey && ev.ctrlKey)) { // Ctrl + Shict + V - term.leaseContentEditable(); - } - } - }); - - /** - * Hack pasting with right-click in order to allow right-click paste, by leasing the - * contentEditable value as true. - */ - on(term.element, 'contextmenu', function (ev) { - term.leaseContentEditable(); - }); - }; - - - /* - * Apply key handling to the terminal - */ - Terminal.bindKeys = function(term) { - on(term.element, 'keydown', function(ev) { - term.keyDown(ev); - }, true); - - on(term.element, 'keypress', function(ev) { - term.keyPress(ev); - }, true); }; /** @@ -622,48 +548,44 @@ return processedText; }; + /** + * Apply key handling to the terminal + */ + Terminal.bindKeys = function(term) { + on(term.element, 'keydown', function(ev) { + if (document.activeElement != this) { + return; + } + term.keyDown(ev); + }, true); + + on(term.element, 'keypress', function(ev) { + if (document.activeElement != this) { + return; + } + term.keyPress(ev); + }, true); + + on(term.element, 'keyup', term.focus.bind(term)); + + on(term.textarea, 'keydown', function(ev) { + term.keyDown(ev); + }, true); + + on(term.textarea, 'keypress', function(ev) { + term.keyPress(ev); + // Truncate the textarea's value, since it is not needed + this.value = ''; + }, true); + }; + /** * Binds copy functionality to the given terminal. * @static */ Terminal.bindCopy = function(term) { on(term.element, 'copy', function(ev) { - var copiedText = window.getSelection().toString(), - text = Terminal.prepareCopiedTextForClipboard(copiedText); - - ev.clipboardData.setData('text/plain', text); - ev.preventDefault(); - }); - }; - - /** - * Cancel the cut event completely - */ - Terminal.bindCut = function(term) { - on(term.element, 'cut', function (ev) { - ev.preventDefault(); - }); - }; - - - Terminal.bindDrop = function (term) { - /* - * Do not perform the "drop" event. Altering the contents of the - * terminal with drag n drop is unwanted behavior. - */ - on(term.element, 'drop', function (ev) { - term.cancel(ev, true); - }); - }; - - - Terminal.click = function (term) { - /* - * Do not perform the "drop" event. Altering the contents of the - * terminal with drag n drop is unwanted behavior. - */ - on(term.element, 'click', function (ev) { - term.cancel(ev, true); + return; // temporary }); }; @@ -740,7 +662,6 @@ this.element.classList.add('xterm'); this.element.classList.add('xterm-theme-' + this.theme); this.element.setAttribute('tabindex', 0); - this.element.spellcheck = false; /* * Create the container that will hold the lines of the terminal and then @@ -751,6 +672,27 @@ this.element.appendChild(this.rowContainer); this.children = []; + /* + * Create the container that will hold helpers like the textarea for + * capturing DOM Events. Then produce the helpers. + */ + this.helperContainer = document.createElement('div'); + this.helperContainer.classList.add('xterm-helpers'); + this.element.appendChild(this.helperContainer); + this.textarea = document.createElement('textarea'); + this.textarea.classList.add('xterm-helper-textarea'); + this.textarea.setAttribute('autocorrect', 'off'); + this.textarea.setAttribute('autocapitalize', 'off'); + this.textarea.setAttribute('spellcheck', 'false'); + this.textarea.tabIndex = 0; + this.textarea.onfocus = function() { + self.emit('focus', {terminal: self}); + } + this.textarea.onblur = function() { + self.emit('blur', {terminal: self}); + } + this.helperContainer.appendChild(this.textarea); + for (; i < this.rows; i++) { this.insertRow(); } @@ -2757,6 +2699,8 @@ Terminal.prototype.keyPress = function(ev) { var key; + this.cancel(ev); + if (ev.charCode) { key = ev.charCode; } else if (ev.which == null) { @@ -2775,21 +2719,11 @@ key = String.fromCharCode(key); - /** - * When a key is pressed and a character is sent to the terminal, then clear any text - * selected in the terminal. - */ - if (key) { - this.clearSelection(); - } - this.emit('keypress', key, ev); this.emit('key', key, ev); this.showCursor(); this.handler(key); - this.cancel(ev, true); - return false; }; From aaedcfc7002883a2bdc0bedf1b1047ca854d1755 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 16:18:05 -0700 Subject: [PATCH 02/38] Support focus class on the .xterm element --- src/xterm.css | 10 +++++----- src/xterm.js | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 348fc097f..4f1f4796e 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -43,7 +43,7 @@ position: relative; } -.terminal:focus { +.terminal.focus { outline: none; } @@ -57,11 +57,11 @@ * HACK: to fix IE's blinking cursor * Move textarea out of the screen to the far left, so that the cursor is not visible. */ - text-indent: -9999em; + /*text-indent: -9999em; opacity: 0; width: 0; height: 0; - z-index: -10; + z-index: -10;*/ } .terminal .terminal-cursor { @@ -69,13 +69,13 @@ color: #000; } -.terminal:not(:focus) .terminal-cursor { +.terminal:not(.focus) .terminal-cursor { outline: 1px solid #fff; outline-offset: -1px; background-color: transparent; } -.terminal:focus .terminal-cursor.blinking { +.terminal.focus .terminal-cursor.blinking { animation: blink-cursor 1.2s infinite step-end; } diff --git a/src/xterm.js b/src/xterm.js index 30e6aee5f..a33261c02 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -442,6 +442,7 @@ this.send('\x1b[I'); } + this.element.classList.add('focus'); this.showCursor(); this.textarea.focus(); Terminal.focus = this; @@ -474,6 +475,7 @@ return; } + this.element.classList.remove('focus'); this.cursorState = 0; this.refresh(this.y, this.y); this.textarea.blur(); From a52b7e7a4eaf9b6a37876e6d980fd72d007b58e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 16:50:07 -0700 Subject: [PATCH 03/38] Add starting point for IME support --- src/xterm.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index a33261c02..6682f83a1 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -579,6 +579,20 @@ // Truncate the textarea's value, since it is not needed this.value = ''; }, true); + + on(term.textarea, 'compositionstart', function(ev) { + console.log('compositionstart', ev); + // TODO: Set a composing flag + }); + on(term.textarea, 'compositionupdate', function(ev) { + console.log('compositionupdate', ev); + // TODO: Display text being composed in the UI (needs to be handled in keydown) + }); + on(term.textarea, 'compositionend', function(ev) { + console.log('compositionend', ev); + // TODO: Send composed text to the pty + // TODO: Remove composing flag + }); }; /** @@ -2464,6 +2478,8 @@ // Key Resources: // https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent Terminal.prototype.keyDown = function(ev) { + // TODO: Ignore event if currently composing text + var self = this; var result = this.evaluateKeyEscapeSequence(ev); From 26af6ffd4df7449673071654689d95a5f613ccbb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 18:48:08 -0700 Subject: [PATCH 04/38] Get IMEs working --- src/xterm.css | 11 +++++++++ src/xterm.js | 62 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 4f1f4796e..55fe27bfb 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -90,6 +90,17 @@ } } +.terminal .composition-view { + background: #000; + color: #FFF; + display: none; + position: absolute; +} + +.terminal .composition-view.active { + display: block; +} + /* * Determine default colors for xterm.js */ diff --git a/src/xterm.js b/src/xterm.js index a6cdd8d40..f2a5b9000 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -238,6 +238,15 @@ */ this.isRefreshing = false; + // Whether input composition is currently happening, eg. via a mobile keyboard, speech input + // or IME. This variable determines whether the compositionText should be displayed on the UI. + this.isComposing = false; + + // The input currently being composed, eg. via a mobile keyboard, speech input or IME. + this.compositionText = null; + + this.compositionPosition = { start: null, end: null }; + /** * Whether there is a full terminal refresh queued */ @@ -581,18 +590,42 @@ this.value = ''; }, true); + // TODO: Refactor into a CompositionHelper object on(term.textarea, 'compositionstart', function(ev) { - console.log('compositionstart', ev); - // TODO: Set a composing flag + this.isComposing = true; + term.compositionPosition.start = this.value.length; + term.compositionView.textContent = ''; + term.compositionView.classList.add('active'); }); on(term.textarea, 'compositionupdate', function(ev) { - console.log('compositionupdate', ev); - // TODO: Display text being composed in the UI (needs to be handled in keydown) + term.compositionPosition.end = this.value.length - 1; + + // Update composition view contents and position + term.compositionView.textContent = ev.data; + var cursor = document.querySelector('.terminal-cursor'); + term.compositionView.style.left = cursor.offsetLeft + 'px'; + term.compositionView.style.top = cursor.offsetTop + 'px'; }); on(term.textarea, 'compositionend', function(ev) { - console.log('compositionend', ev); - // TODO: Send composed text to the pty - // TODO: Remove composing flag + term.compositionView.classList.remove('active'); + this.isComposing = false; + var textarea = this; + // Record composition position here as a new compositionstart event may fire before the + // setTimeout executes + var compositionPosition = term.compositionPosition; + + // Since composition* events happen before the changes take place in the textarea on most + // browsers, use a setTimeout with 0ms time to allow the native compositionend event to + // complete. This ensures the correct character is retrieved, this solution was used + // because: + // - The compositionend event's data property is unreliable, at least on Chromium + // - The last compositionupdate event's data property does not always accurately describe + // the character, a counter example being Korean where an ending consonsant can move to + // the following character if the following input is a vowel. + setTimeout(function () { + var input = textarea.value.substring(compositionPosition.start, compositionPosition.end); + term.write(input); + }, 0); }); }; @@ -694,6 +727,7 @@ */ this.helperContainer = document.createElement('div'); this.helperContainer.classList.add('xterm-helpers'); + // TODO: This should probably be inserted once it's filled to prevent an additional layout this.element.appendChild(this.helperContainer); this.textarea = document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); @@ -708,12 +742,16 @@ self.emit('blur', {terminal: self}); } this.helperContainer.appendChild(this.textarea); + this.compositionView = document.createElement('div'); + this.compositionView.classList.add('composition-view'); + this.helperContainer.appendChild(this.compositionView); for (; i < this.rows; i++) { this.insertRow(); } this.parent.appendChild(this.element); + // Draw the screen. this.refresh(0, this.rows - 1); @@ -1349,6 +1387,12 @@ this.element.appendChild(this.rowContainer); } + // TODO: Put in a listener? + // Update composition view contents and position + var cursor = document.querySelector('.terminal-cursor'); + term.compositionView.style.left = cursor.offsetLeft + 'px'; + term.compositionView.style.top = cursor.offsetTop + 'px'; + this.emit('refresh', {element: this.element, start: start, end: end}); }; @@ -2487,7 +2531,9 @@ * @param {KeyboardEvent} ev The keydown event to be handled. */ Terminal.prototype.keyDown = function(ev) { - // TODO: Ignore event if currently composing text + if (this.isComposing) { + return; + } var self = this; var result = this.evaluateKeyEscapeSequence(ev); From 03fa017d4a6ff6b0a0610f8a3200b0699bb4e7be Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 18:49:37 -0700 Subject: [PATCH 05/38] Re-hide the textarea --- src/xterm.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 55fe27bfb..403389ac1 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -57,11 +57,11 @@ * HACK: to fix IE's blinking cursor * Move textarea out of the screen to the far left, so that the cursor is not visible. */ - /*text-indent: -9999em; + text-indent: -9999em; opacity: 0; width: 0; height: 0; - z-index: -10;*/ + z-index: -10; } .terminal .terminal-cursor { From 29000fb79e3e29549f81cf1911693607be28109e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 19:14:56 -0700 Subject: [PATCH 06/38] Refactor into CompositionHelper object --- src/xterm.js | 93 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index f2a5b9000..3b6e2d606 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -133,6 +133,64 @@ }; + function CompositionHelper(textarea, compositionView, terminal) { + this.textarea = textarea; + this.compositionView = compositionView; + this.terminal = terminal; + + // Whether input composition is currently happening, eg. via a mobile keyboard, speech input + // or IME. This variable determines whether the compositionText should be displayed on the UI. + this.isComposing = false; + + // The input currently being composed, eg. via a mobile keyboard, speech input or IME. + this.compositionText = null; + + // The position within the input textarea's value of the current composition. + this.compositionPosition = { start: null, end: null }; + } + + CompositionHelper.prototype.compositionstart = function(ev) { + this.isComposing = true; + this.compositionPosition.start = this.textarea.value.length; + this.compositionView.textContent = ''; + this.compositionView.classList.add('active'); + }; + + CompositionHelper.prototype.compositionupdate = function(ev) { + this.compositionPosition.end = this.textarea.value.length - 1; + this.compositionView.textContent = ev.data; + this.updateCursorPosition(); + }; + + CompositionHelper.prototype.compositionend = function(ev) { + this.compositionView.classList.remove('active'); + this.isComposing = false; + // Record composition position here as a new compositionstart event may fire before the + // setTimeout executes + var currentCompositionPosition = this.compositionPosition; + + // Since composition* events happen before the changes take place in the textarea on most + // browsers, use a setTimeout with 0ms time to allow the native compositionend event to + // complete. This ensures the correct character is retrieved, this solution was used + // because: + // - The compositionend event's data property is unreliable, at least on Chromium + // - The last compositionupdate event's data property does not always accurately describe + // the character, a counter example being Korean where an ending consonsant can move to + // the following character if the following input is a vowel. + var self = this; + setTimeout(function () { + var input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + self.terminal.write(input); + }, 0); + }; + + CompositionHelper.prototype.updateCursorPosition = function() { + var cursor = document.querySelector('.terminal-cursor'); + term.compositionView.style.left = cursor.offsetLeft + 'px'; + term.compositionView.style.top = cursor.offsetTop + 'px'; + }; + + /** * States */ @@ -238,15 +296,6 @@ */ this.isRefreshing = false; - // Whether input composition is currently happening, eg. via a mobile keyboard, speech input - // or IME. This variable determines whether the compositionText should be displayed on the UI. - this.isComposing = false; - - // The input currently being composed, eg. via a mobile keyboard, speech input or IME. - this.compositionText = null; - - this.compositionPosition = { start: null, end: null }; - /** * Whether there is a full terminal refresh queued */ @@ -592,22 +641,25 @@ // TODO: Refactor into a CompositionHelper object on(term.textarea, 'compositionstart', function(ev) { - this.isComposing = true; + term.compositionHelper.compositionstart.bind(term.compositionHelper, ev)(); + /*this.isComposing = true; term.compositionPosition.start = this.value.length; term.compositionView.textContent = ''; - term.compositionView.classList.add('active'); + term.compositionView.classList.add('active');*/ }); on(term.textarea, 'compositionupdate', function(ev) { - term.compositionPosition.end = this.value.length - 1; + term.compositionHelper.compositionupdate.bind(term.compositionHelper, ev)(); + /*term.compositionPosition.end = this.value.length - 1; // Update composition view contents and position term.compositionView.textContent = ev.data; var cursor = document.querySelector('.terminal-cursor'); term.compositionView.style.left = cursor.offsetLeft + 'px'; - term.compositionView.style.top = cursor.offsetTop + 'px'; + term.compositionView.style.top = cursor.offsetTop + 'px';*/ }); on(term.textarea, 'compositionend', function(ev) { - term.compositionView.classList.remove('active'); + term.compositionHelper.compositionend.bind(term.compositionHelper, ev)(); + /*term.compositionView.classList.remove('active'); this.isComposing = false; var textarea = this; // Record composition position here as a new compositionstart event may fire before the @@ -625,7 +677,7 @@ setTimeout(function () { var input = textarea.value.substring(compositionPosition.start, compositionPosition.end); term.write(input); - }, 0); + }, 0);*/ }); }; @@ -742,8 +794,10 @@ self.emit('blur', {terminal: self}); } this.helperContainer.appendChild(this.textarea); + this.compositionView = document.createElement('div'); this.compositionView.classList.add('composition-view'); + this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this); this.helperContainer.appendChild(this.compositionView); for (; i < this.rows; i++) { @@ -1387,11 +1441,8 @@ this.element.appendChild(this.rowContainer); } - // TODO: Put in a listener? - // Update composition view contents and position - var cursor = document.querySelector('.terminal-cursor'); - term.compositionView.style.left = cursor.offsetLeft + 'px'; - term.compositionView.style.top = cursor.offsetTop + 'px'; + // TODO: Attach to refresh event instead? + term.compositionHelper.updateCursorPosition(); this.emit('refresh', {element: this.element, start: start, end: end}); }; @@ -2531,7 +2582,7 @@ * @param {KeyboardEvent} ev The keydown event to be handled. */ Terminal.prototype.keyDown = function(ev) { - if (this.isComposing) { + if (this.compositionHelper.isComposing) { return; } From c656ed04b0050d40d85e24e1e0753ecf3736ab1c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 20:19:26 -0700 Subject: [PATCH 07/38] Use handler not write, fix other edge cases --- src/xterm.js | 123 ++++++++++++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 55 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 3b6e2d606..c0f49c89f 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -147,43 +147,81 @@ // The position within the input textarea's value of the current composition. this.compositionPosition = { start: null, end: null }; + + this.isSendingComposition = false; } - CompositionHelper.prototype.compositionstart = function(ev) { + CompositionHelper.prototype.compositionstart = function() { this.isComposing = true; this.compositionPosition.start = this.textarea.value.length; + console.log('In compositionstart: compositionPosition=', this.compositionPosition); this.compositionView.textContent = ''; this.compositionView.classList.add('active'); }; CompositionHelper.prototype.compositionupdate = function(ev) { - this.compositionPosition.end = this.textarea.value.length - 1; + this.compositionView.textContent = ev.data; this.updateCursorPosition(); - }; - - CompositionHelper.prototype.compositionend = function(ev) { - this.compositionView.classList.remove('active'); - this.isComposing = false; - // Record composition position here as a new compositionstart event may fire before the - // setTimeout executes - var currentCompositionPosition = this.compositionPosition; - - // Since composition* events happen before the changes take place in the textarea on most - // browsers, use a setTimeout with 0ms time to allow the native compositionend event to - // complete. This ensures the correct character is retrieved, this solution was used - // because: - // - The compositionend event's data property is unreliable, at least on Chromium - // - The last compositionupdate event's data property does not always accurately describe - // the character, a counter example being Korean where an ending consonsant can move to - // the following character if the following input is a vowel. var self = this; - setTimeout(function () { - var input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); - self.terminal.write(input); + setTimeout(function() { + self.compositionPosition.end = self.textarea.value.length; + + console.log('In compositionupdate: compositionPosition=', self.compositionPosition); }, 0); }; + CompositionHelper.prototype.compositionend = function() { + this.finalizeComposition(true); + }; + + /** + * Finalizes the composition, resuming regular input actions. This is called when a composition + * is ending. + * @param {boolean} waitForPropogation Whether to wait for events to propogate before sending + * the input. This should be false if a non-composition keystroke is entered before the + * compositionend event is triggered, such as enter, so that the composition is send before + * the command is executed. + */ + CompositionHelper.prototype.finalizeComposition = function(waitForPropogation) { + this.compositionView.classList.remove('active'); + this.isComposing = false; + + if (!waitForPropogation) { + // Cancel any delayed composition send requests and send the input immediately. + this.isSendingComposition = false; + var input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end); + this.terminal.handler(input); + } else { + // Make a deep copy of the composition position here as a new compositionstart event may + // fire before the setTimeout executes. + var currentCompositionPosition = { + start: this.compositionPosition.start, + end: this.compositionPosition.end, + } + + // Since composition* events happen before the changes take place in the textarea on most + // browsers, use a setTimeout with 0ms time to allow the native compositionend event to + // complete. This ensures the correct character is retrieved, this solution was used + // because: + // - The compositionend event's data property is unreliable, at least on Chromium + // - The last compositionupdate event's data property does not always accurately describe + // the character, a counter example being Korean where an ending consonsant can move to + // the following character if the following input is a vowel. + var self = this; + this.isSendingComposition = true; + setTimeout(function () { + // Ensure that the input has not already been sent + if (self.isSendingComposition) { + console.log('send input ' + input + ' delayed'); + self.isSendingComposition = false; + var input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + self.terminal.handler(input); + } + }, 0); + } + } + CompositionHelper.prototype.updateCursorPosition = function() { var cursor = document.querySelector('.terminal-cursor'); term.compositionView.style.left = cursor.offsetLeft + 'px'; @@ -639,45 +677,14 @@ this.value = ''; }, true); - // TODO: Refactor into a CompositionHelper object on(term.textarea, 'compositionstart', function(ev) { term.compositionHelper.compositionstart.bind(term.compositionHelper, ev)(); - /*this.isComposing = true; - term.compositionPosition.start = this.value.length; - term.compositionView.textContent = ''; - term.compositionView.classList.add('active');*/ }); on(term.textarea, 'compositionupdate', function(ev) { term.compositionHelper.compositionupdate.bind(term.compositionHelper, ev)(); - /*term.compositionPosition.end = this.value.length - 1; - - // Update composition view contents and position - term.compositionView.textContent = ev.data; - var cursor = document.querySelector('.terminal-cursor'); - term.compositionView.style.left = cursor.offsetLeft + 'px'; - term.compositionView.style.top = cursor.offsetTop + 'px';*/ }); on(term.textarea, 'compositionend', function(ev) { term.compositionHelper.compositionend.bind(term.compositionHelper, ev)(); - /*term.compositionView.classList.remove('active'); - this.isComposing = false; - var textarea = this; - // Record composition position here as a new compositionstart event may fire before the - // setTimeout executes - var compositionPosition = term.compositionPosition; - - // Since composition* events happen before the changes take place in the textarea on most - // browsers, use a setTimeout with 0ms time to allow the native compositionend event to - // complete. This ensures the correct character is retrieved, this solution was used - // because: - // - The compositionend event's data property is unreliable, at least on Chromium - // - The last compositionupdate event's data property does not always accurately describe - // the character, a counter example being Korean where an ending consonsant can move to - // the following character if the following input is a vowel. - setTimeout(function () { - var input = textarea.value.substring(compositionPosition.start, compositionPosition.end); - term.write(input); - }, 0);*/ }); }; @@ -2582,8 +2589,14 @@ * @param {KeyboardEvent} ev The keydown event to be handled. */ Terminal.prototype.keyDown = function(ev) { - if (this.compositionHelper.isComposing) { - return; + if (this.compositionHelper.isComposing || this.compositionHelper.isSendingComposition) { + if (ev.keyCode === 229) { + // Continue composing + return; + } else { + // Finish composition immediately + this.compositionHelper.finalizeComposition(null); + } } var self = this; From cab79c473f2698e6051252f6bd0136abc6949622 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 20:26:18 -0700 Subject: [PATCH 08/38] jsdoc and tidy up --- src/xterm.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index c0f49c89f..07b54b316 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -133,6 +133,14 @@ }; + /** + * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend + * events, displaying the in-progress composition to the UI and forwarding the final composition + * to the handler. + * @param {HTMLTextAreaElement} textarea The textarea that xterm uses for input. + * @param {HTMLElement} compositionView The element to display the in-progress composition in. + * @param {Terminal} terminal The Terminal to forward the finished composition to. + */ function CompositionHelper(textarea, compositionView, terminal) { this.textarea = textarea; this.compositionView = compositionView; @@ -148,29 +156,38 @@ // The position within the input textarea's value of the current composition. this.compositionPosition = { start: null, end: null }; + // Whether a composition is in the process of being sent, setting this to false will cancel + // any in-progress composition. this.isSendingComposition = false; } + /** + * Handles the compositionstart event, activating the composition view. + */ CompositionHelper.prototype.compositionstart = function() { this.isComposing = true; this.compositionPosition.start = this.textarea.value.length; - console.log('In compositionstart: compositionPosition=', this.compositionPosition); this.compositionView.textContent = ''; this.compositionView.classList.add('active'); }; + /** + * Handles the compositionupdate event, updating the composition view. + * @param {CompositionEvent} ev The event. + */ CompositionHelper.prototype.compositionupdate = function(ev) { - this.compositionView.textContent = ev.data; - this.updateCursorPosition(); + this.updateCompositionViewPosition(); var self = this; setTimeout(function() { self.compositionPosition.end = self.textarea.value.length; - - console.log('In compositionupdate: compositionPosition=', self.compositionPosition); }, 0); }; + /** + * Handles the compositionend event, hiding the composition view and sending the composition to + * the handler. + */ CompositionHelper.prototype.compositionend = function() { this.finalizeComposition(true); }; @@ -222,7 +239,10 @@ } } - CompositionHelper.prototype.updateCursorPosition = function() { + /** + * Updates the composition view's position. + */ + CompositionHelper.prototype.updateCompositionViewPosition = function() { var cursor = document.querySelector('.terminal-cursor'); term.compositionView.style.left = cursor.offsetLeft + 'px'; term.compositionView.style.top = cursor.offsetTop + 'px'; @@ -1449,7 +1469,7 @@ } // TODO: Attach to refresh event instead? - term.compositionHelper.updateCursorPosition(); + term.compositionHelper.updateCompositionViewPosition(); this.emit('refresh', {element: this.element, start: start, end: end}); }; From 8faea59e316e1d47e0377b894cce63adc7cb847a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 20:51:55 -0700 Subject: [PATCH 09/38] Handle non-composition input when IME is active --- src/xterm.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index 07b54b316..f9b9786f5 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -239,6 +239,22 @@ } } + CompositionHelper.prototype.handleAnyTextareaChanges = function() { + var oldValue = this.textarea.value; + + var self = this; + setTimeout(function() { + // Ensure no composition has started since the timeout + if (!self.isComposing) { + var newValue = self.textarea.value; + var diff = newValue.replace(oldValue, ''); + if (diff.length > 0) { + self.terminal.handler(diff); + } + } + }, 0); + } + /** * Updates the composition view's position. */ @@ -2619,6 +2635,11 @@ } } + if (ev.keyCode === 229) { + this.compositionHelper.handleAnyTextareaChanges(); + return; + } + var self = this; var result = this.evaluateKeyEscapeSequence(ev); From 237e6819bfd66fb266d475bfe358ef1b944b9ca0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 20:55:24 -0700 Subject: [PATCH 10/38] Fix tests --- test/test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test.js b/test/test.js index 70b5db8ea..63a33c93f 100644 --- a/test/test.js +++ b/test/test.js @@ -81,6 +81,9 @@ describe('xterm.js', function() { xterm.handler = function() {}; xterm.showCursor = function() {}; xterm.clearSelection = function() {}; + xterm.compositionHelper = { + isComposing: false + }; }); describe('On Mac OS', function() { From 8b46e842637a2dfdb3fcaf1f2b775ff4b6b5bb1c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Jul 2016 21:42:10 -0700 Subject: [PATCH 11/38] Use correct .terminal-cursor for composition view --- src/xterm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index f9b9786f5..e9e6ec9b9 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -259,7 +259,7 @@ * Updates the composition view's position. */ CompositionHelper.prototype.updateCompositionViewPosition = function() { - var cursor = document.querySelector('.terminal-cursor'); + var cursor = this.terminal.element.querySelector('.terminal-cursor'); term.compositionView.style.left = cursor.offsetLeft + 'px'; term.compositionView.style.top = cursor.offsetTop + 'px'; }; From e1c1b07a0b1887e9b7bc8f9abb9be8560c8dfb08 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 08:37:24 -0700 Subject: [PATCH 12/38] Add test cases for Korean --- src/xterm.js | 6 +- test/composition-helper-test.js | 170 ++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 test/composition-helper-test.js diff --git a/src/xterm.js b/src/xterm.js index 0bff3e6d4..b34c75033 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -230,7 +230,6 @@ setTimeout(function () { // Ensure that the input has not already been sent if (self.isSendingComposition) { - console.log('send input ' + input + ' delayed'); self.isSendingComposition = false; var input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); self.terminal.handler(input); @@ -260,8 +259,8 @@ */ CompositionHelper.prototype.updateCompositionViewPosition = function() { var cursor = this.terminal.element.querySelector('.terminal-cursor'); - term.compositionView.style.left = cursor.offsetLeft + 'px'; - term.compositionView.style.top = cursor.offsetTop + 'px'; + this.compositionView.style.left = cursor.offsetLeft + 'px'; + this.compositionView.style.top = cursor.offsetTop + 'px'; }; @@ -5226,6 +5225,7 @@ */ Terminal.EventEmitter = EventEmitter; + Terminal.CompositionHelper = CompositionHelper; Terminal.inherits = inherits; /** diff --git a/test/composition-helper-test.js b/test/composition-helper-test.js new file mode 100644 index 000000000..502d72cf2 --- /dev/null +++ b/test/composition-helper-test.js @@ -0,0 +1,170 @@ +var assert = require('chai').assert; +var Terminal = require('../src/xterm'); + +describe('CompositionHelper', function () { + var terminal; + var compositionHelper; + var compositionView; + var textarea; + var handledText; + + beforeEach(function () { + compositionView = { + classList: { + add: function () {}, + remove: function () {}, + }, + style: { + left: 0, + top: 0 + }, + textContent: '' + } + textarea = { + value: '' + } + terminal = { + element: { + querySelector: function () { + return { offsetLeft: 0, offsetTop: 0 }; + } + }, + handler: function (text) { + handledText += text; + console.log('handler - ' + text); + } + } + handledText = ''; + compositionHelper = new Terminal.CompositionHelper(textarea, compositionView, terminal); + }); + + describe('Public API', function () { + it('should define CompositionHelper.prototype.compositionstart', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.compositionstart); + }); + it('should define CompositionHelper.prototype.compositionupdate', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.compositionupdate); + }); + it('should define CompositionHelper.prototype.compositionend', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.compositionend); + }); + it('should define CompositionHelper.prototype.finalizeComposition', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.finalizeComposition); + }); + it('should define CompositionHelper.prototype.handleAnyTextareaChanges', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.handleAnyTextareaChanges); + }); + it('should define CompositionHelper.prototype.updateCompositionViewPosition', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.updateCompositionViewPosition); + }); + it('should define CompositionHelper.isComposing', function () { + assert.isDefined(compositionHelper.isComposing); + }); + it('should define CompositionHelper.isSendingComposition', function () { + assert.isDefined(compositionHelper.isSendingComposition); + }); + }); + + describe('Input', function () { + it('Should insert simple characters', function (done) { + // First character + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'ㅇ' }); + textarea.value = 'ㅇ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, 'ㅇ'); + // Second character + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'ㅇ' }); + textarea.value = 'ㅇㅇ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, 'ㅇㅇ'); + done(); + }, 0); + }, 0); + }, 0); + }, 0); + }); + + it('Should insert complex characters', function (done) { + // First character + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'ㅇ' }); + textarea.value = 'ㅇ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: '아' }); + textarea.value = '아'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: '앙' }); + textarea.value = '앙'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, '앙'); + // Second character + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'ㅇ' }); + textarea.value = '앙ㅇ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: '아' }); + textarea.value = '앙아'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: '앙' }); + textarea.value = '앙앙'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, '앙앙'); + done(); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); + }); + + it('Should insert complex characters that change with following character', function (done) { + // First character + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'ㅇ' }); + textarea.value = 'ㅇ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: '아' }); + textarea.value = '아'; + setTimeout(function() { // wait for any textarea updates + // Start second character in first character + compositionHelper.compositionupdate({ data: '앙' }); + textarea.value = '앙'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: '아' }); + textarea.value = '아아' + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, '아아'); + done(); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); + }); + + it('Should insert multi-line charcters', function () { + // TODO: Implement a hiragana example + }); + + it('Should insert multi-line charcters that are converted to other characters', function () { + // TODO: Implement a hiragana -> kanji example + }); + }); +}); From f3db84decbb7f495f3326a1fe4d0b550ac1eae38 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 15:28:14 -0700 Subject: [PATCH 13/38] Add statement on browser support Fixes #183 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 9720507d4..e8fc5fcd6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad ![xterm.js screenshot](xtermjs.png) +## Browser Support + +- Chrome 48+ +- Firefox 44+ +- Safari 8+ +- Internet Explorer 11+ +- Opera 35+ + ## Demo To launch the demo simply run: From 8074d754d3cc645c531f2a8baf28b605b8e89e08 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 20:12:49 -0700 Subject: [PATCH 14/38] Only position composition view if a cursor is visible --- src/xterm.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index b34c75033..8166c545e 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -259,8 +259,10 @@ */ CompositionHelper.prototype.updateCompositionViewPosition = function() { var cursor = this.terminal.element.querySelector('.terminal-cursor'); - this.compositionView.style.left = cursor.offsetLeft + 'px'; - this.compositionView.style.top = cursor.offsetTop + 'px'; + if (cursor) { + this.compositionView.style.left = cursor.offsetLeft + 'px'; + this.compositionView.style.top = cursor.offsetTop + 'px'; + } }; From 38d08873bc920f89f2fcdc062c39f63ef0669411 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 20:34:43 -0700 Subject: [PATCH 15/38] Don't finalize composition on modifier key presses --- src/xterm.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index 8166c545e..30bf987fb 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2627,7 +2627,10 @@ Terminal.prototype.keyDown = function(ev) { if (this.compositionHelper.isComposing || this.compositionHelper.isSendingComposition) { if (ev.keyCode === 229) { - // Continue composing + // Continue composing if the keyCode is the "composition character" + return; + } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { + // Continue composing if the keyCode is a modifier key return; } else { // Finish composition immediately From 4efb8d24acd8754c5f65847ab4cb879a7d45dd09 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 20:34:55 -0700 Subject: [PATCH 16/38] Don't wrap multi-character compositions --- src/xterm.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xterm.css b/src/xterm.css index 403389ac1..7f00a8b7b 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -95,6 +95,7 @@ color: #FFF; display: none; position: absolute; + white-space: nowrap; } .terminal .composition-view.active { From 0a7c1bfd98d7e1849775fc30d3842884d0c782ad Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 20:46:43 -0700 Subject: [PATCH 17/38] Position the textarea below the cursor during a composition This will position any IME helper in the correct position --- src/xterm.css | 3 ++- src/xterm.js | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 7f00a8b7b..07b82dd5b 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -53,11 +53,12 @@ } .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. */ - text-indent: -9999em; + left: -9999em; opacity: 0; width: 0; height: 0; diff --git a/src/xterm.js b/src/xterm.js index 30bf987fb..27ce0450d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -177,7 +177,7 @@ */ CompositionHelper.prototype.compositionupdate = function(ev) { this.compositionView.textContent = ev.data; - this.updateCompositionViewPosition(); + this.updateCompositionElements(); var self = this; setTimeout(function() { self.compositionPosition.end = self.textarea.value.length; @@ -203,6 +203,7 @@ CompositionHelper.prototype.finalizeComposition = function(waitForPropogation) { this.compositionView.classList.remove('active'); this.isComposing = false; + this.clearTextareaPosition(); if (!waitForPropogation) { // Cancel any delayed composition send requests and send the input immediately. @@ -255,16 +256,28 @@ } /** - * Updates the composition view's position. + * Positions the composition view on top of the cursor and the textarea just below it (so the + * IME helper dialog is positioned correctly). */ - CompositionHelper.prototype.updateCompositionViewPosition = function() { + CompositionHelper.prototype.updateCompositionElements = function() { var cursor = this.terminal.element.querySelector('.terminal-cursor'); if (cursor) { this.compositionView.style.left = cursor.offsetLeft + 'px'; this.compositionView.style.top = cursor.offsetTop + 'px'; } + this.textarea.style.left = cursor.offsetLeft + 'px'; + this.textarea.style.top = (cursor.offsetTop + cursor.offsetHeight) + 'px'; }; + /** + * Clears the textarea's position so that the cursor does not blink on IE. + * @private + */ + CompositionHelper.prototype.clearTextareaPosition = function() { + this.textarea.style.left = undefined; + this.textarea.style.top = undefined; + } + /** * States @@ -1485,7 +1498,7 @@ } // TODO: Attach to refresh event instead? - term.compositionHelper.updateCompositionViewPosition(); + term.compositionHelper.updateCompositionElements(); this.emit('refresh', {element: this.element, start: start, end: end}); }; From ac8db6e8ec0bcf4115c05e48d401e8b2c9ed77a2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 20:52:55 -0700 Subject: [PATCH 18/38] jsdoc handleAnyTextareaChanges --- src/xterm.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 27ce0450d..d3b500ae9 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -239,12 +239,17 @@ } } + /** + * Apply any changes made to the textarea after the current event chain is allowed to complete. + * This should be called when not currently composing but a keydown event with the "composition + * character" (229) is triggered, in order to allow non-composition text to be entered when an + * IME is active. + */ CompositionHelper.prototype.handleAnyTextareaChanges = function() { var oldValue = this.textarea.value; - var self = this; setTimeout(function() { - // Ensure no composition has started since the timeout + // Ignore if a composition has started since the timeout if (!self.isComposing) { var newValue = self.textarea.value; var diff = newValue.replace(oldValue, ''); From 89d29bbc10ef624a493d96ae06d6318867c6aa13 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 21:02:43 -0700 Subject: [PATCH 19/38] Fix non-composition char input after composition char + test --- src/xterm.js | 11 ++++++++++- test/composition-helper-test.js | 26 +++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index d3b500ae9..416422c51 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -232,7 +232,16 @@ // Ensure that the input has not already been sent if (self.isSendingComposition) { self.isSendingComposition = false; - var input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + var input; + if (self.isComposing) { + // Use the end position to get the string if a new composition has started. + input = self.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + } else { + // Don't use the end position here in order to pick up any characters after the + // composition has finished, for example when typing a non-composition character + // (eg. 2) after a composition character. + input = self.textarea.value.substring(currentCompositionPosition.start); + } self.terminal.handler(input); } }, 0); diff --git a/test/composition-helper-test.js b/test/composition-helper-test.js index 502d72cf2..558bcdffb 100644 --- a/test/composition-helper-test.js +++ b/test/composition-helper-test.js @@ -21,7 +21,11 @@ describe('CompositionHelper', function () { textContent: '' } textarea = { - value: '' + value: '', + style: { + left: 0, + top: 0 + } } terminal = { element: { @@ -54,8 +58,8 @@ describe('CompositionHelper', function () { it('should define CompositionHelper.prototype.handleAnyTextareaChanges', function () { assert.isDefined(Terminal.CompositionHelper.prototype.handleAnyTextareaChanges); }); - it('should define CompositionHelper.prototype.updateCompositionViewPosition', function () { - assert.isDefined(Terminal.CompositionHelper.prototype.updateCompositionViewPosition); + it('should define CompositionHelper.prototype.updateCompositionElements', function () { + assert.isDefined(Terminal.CompositionHelper.prototype.updateCompositionElements); }); it('should define CompositionHelper.isComposing', function () { assert.isDefined(compositionHelper.isComposing); @@ -166,5 +170,21 @@ describe('CompositionHelper', function () { it('Should insert multi-line charcters that are converted to other characters', function () { // TODO: Implement a hiragana -> kanji example }); + + it('Should insert non-composition charcters input immediately after composition characters', function () { + // First character + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'ㅇ' }); + textarea.value = 'ㅇ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + // Second character is non-composition character (1) + textarea.value = 'ㅇ1'; + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, 'ㅇ1'); + done(); + }, 0); + }, 0); + }); }); }); From 14a62c0f8b064dd09f6c54202670c3e212f01248 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 21:22:27 -0700 Subject: [PATCH 20/38] Add test cases for Japanese --- test/composition-helper-test.js | 98 ++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/test/composition-helper-test.js b/test/composition-helper-test.js index 558bcdffb..7bc4ee486 100644 --- a/test/composition-helper-test.js +++ b/test/composition-helper-test.js @@ -35,7 +35,6 @@ describe('CompositionHelper', function () { }, handler: function (text) { handledText += text; - console.log('handler - ' + text); } } handledText = ''; @@ -71,7 +70,7 @@ describe('CompositionHelper', function () { describe('Input', function () { it('Should insert simple characters', function (done) { - // First character + // First character 'ㅇ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; @@ -79,7 +78,7 @@ describe('CompositionHelper', function () { compositionHelper.compositionend(); setTimeout(function() { // wait for any textarea updates assert.equal(handledText, 'ㅇ'); - // Second character + // Second character 'ㅇ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇㅇ'; @@ -95,7 +94,7 @@ describe('CompositionHelper', function () { }); it('Should insert complex characters', function (done) { - // First character + // First character '앙' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; @@ -109,7 +108,7 @@ describe('CompositionHelper', function () { compositionHelper.compositionend(); setTimeout(function() { // wait for any textarea updates assert.equal(handledText, '앙'); - // Second character + // Second character '앙' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = '앙ㅇ'; @@ -135,7 +134,7 @@ describe('CompositionHelper', function () { }); it('Should insert complex characters that change with following character', function (done) { - // First character + // First character '아' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; @@ -143,7 +142,7 @@ describe('CompositionHelper', function () { compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아'; setTimeout(function() { // wait for any textarea updates - // Start second character in first character + // Start second character '아' in first character compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙'; setTimeout(function() { // wait for any textarea updates @@ -163,22 +162,93 @@ describe('CompositionHelper', function () { }, 0); }); - it('Should insert multi-line charcters', function () { - // TODO: Implement a hiragana example + it('Should insert multi-characters compositions', function (done) { + // First character 'だ' + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'd' }); + textarea.value = 'd'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: 'だ' }); + textarea.value = 'だ'; + setTimeout(function() { // wait for any textarea updates + // Second character 'あ' + compositionHelper.compositionupdate({ data: 'だあ' }); + textarea.value = 'だあ'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, 'だあ'); + done(); + }, 0); + }, 0); + }, 0); + }, 0); }); - it('Should insert multi-line charcters that are converted to other characters', function () { - // TODO: Implement a hiragana -> kanji example + it('Should insert multi-character compositions that are converted to other characters with the same length', function (done) { + // First character 'だ' + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'd' }); + textarea.value = 'd'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: 'だ' }); + textarea.value = 'だ'; + setTimeout(function() { // wait for any textarea updates + // Second character 'ー' + compositionHelper.compositionupdate({ data: 'だー' }); + textarea.value = 'だー'; + setTimeout(function() { // wait for any textarea updates + // Convert to katakana 'ダー' + compositionHelper.compositionupdate({ data: 'ダー' }); + textarea.value = 'ダー'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, 'ダー'); + done(); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); + }) + + it('Should insert multi-character compositions that are converted to other characters with different lengths', function (done) { + // First character 'い' + compositionHelper.compositionstart(); + compositionHelper.compositionupdate({ data: 'い' }); + textarea.value = 'い'; + setTimeout(function() { // wait for any textarea updates + // Second character 'ま' + compositionHelper.compositionupdate({ data: 'いm' }); + textarea.value = 'いm'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionupdate({ data: 'いま' }); + textarea.value = 'いま'; + setTimeout(function() { // wait for any textarea updates + // Convert to kanji '今' + compositionHelper.compositionupdate({ data: '今' }); + textarea.value = '今'; + setTimeout(function() { // wait for any textarea updates + compositionHelper.compositionend(); + setTimeout(function() { // wait for any textarea updates + assert.equal(handledText, '今'); + done(); + }, 0); + }, 0); + }, 0); + }, 0); + }, 0); }); - it('Should insert non-composition charcters input immediately after composition characters', function () { - // First character + it('Should insert non-composition characters input immediately after composition characters', function (done) { + // First character 'ㅇ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(function() { // wait for any textarea updates compositionHelper.compositionend(); - // Second character is non-composition character (1) + // Second character '1' (a non-composition character) textarea.value = 'ㅇ1'; setTimeout(function() { // wait for any textarea updates assert.equal(handledText, 'ㅇ1'); From 534a9e7f5a8157f89b67a4a884de2bd2b73d9a74 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Jul 2016 21:50:00 -0700 Subject: [PATCH 21/38] Big clean up Move keydown event to CompositionHelper Move call in refresh to be handled with an event --- src/xterm.js | 61 +++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 416422c51..d8cce1959 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -192,6 +192,35 @@ this.finalizeComposition(true); }; + /** + * Handles the keydown event, routing any necessary events to the CompositionHelper functions. + * @return Whether the Terminal should continue processing the keydown event. + */ + CompositionHelper.prototype.keydown = function(ev) { + if (this.isComposing || this.isSendingComposition) { + if (ev.keyCode === 229) { + // Continue composing if the keyCode is the "composition character" + return false; + } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { + // Continue composing if the keyCode is a modifier key + return false; + } else { + // Finish composition immediately. This is mainly here for the case where enter is + // pressed and the handler needs to be triggered before the command is executed. + this.finalizeComposition(false); + } + } + + if (ev.keyCode === 229) { + // If the "composition character" is used but gets to this point it means a non-composition + // character (eg. numbers and punctuation) was pressed when the IME was active. + this.handleAnyTextareaChanges(); + return false; + } + + return true; + } + /** * Finalizes the composition, resuming regular input actions. This is called when a composition * is ending. @@ -740,15 +769,10 @@ this.value = ''; }, true); - on(term.textarea, 'compositionstart', function(ev) { - term.compositionHelper.compositionstart.bind(term.compositionHelper, ev)(); - }); - on(term.textarea, 'compositionupdate', function(ev) { - term.compositionHelper.compositionupdate.bind(term.compositionHelper, ev)(); - }); - on(term.textarea, 'compositionend', function(ev) { - term.compositionHelper.compositionend.bind(term.compositionHelper, ev)(); - }); + on(term.textarea, 'compositionstart', term.compositionHelper.compositionstart.bind(term.compositionHelper)); + on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper)); + on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper)); + term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper)); }; /** @@ -1511,9 +1535,6 @@ this.element.appendChild(this.rowContainer); } - // TODO: Attach to refresh event instead? - term.compositionHelper.updateCompositionElements(); - this.emit('refresh', {element: this.element, start: start, end: end}); }; @@ -2652,21 +2673,7 @@ * @param {KeyboardEvent} ev The keydown event to be handled. */ Terminal.prototype.keyDown = function(ev) { - if (this.compositionHelper.isComposing || this.compositionHelper.isSendingComposition) { - if (ev.keyCode === 229) { - // Continue composing if the keyCode is the "composition character" - return; - } else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { - // Continue composing if the keyCode is a modifier key - return; - } else { - // Finish composition immediately - this.compositionHelper.finalizeComposition(null); - } - } - - if (ev.keyCode === 229) { - this.compositionHelper.handleAnyTextareaChanges(); + if (this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { return; } From b61e81d5b4563655383b0f6f0e7c9cf16b548cb1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 15 Jul 2016 10:10:10 -0700 Subject: [PATCH 22/38] Add latest Edge version as supported, alphabetize --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e8fc5fcd6..1eddf62a3 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,12 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad ## Browser Support -- Chrome 48+ -- Firefox 44+ -- Safari 8+ -- Internet Explorer 11+ -- Opera 35+ +- Chrome 48+ +- Edge 13+ +- Firefox 44+ +- Internet Explorer 11+ +- Opera 35+ +- Safari 8+ ## Demo From 2c810ce763033e219ea9423f4260e17d4f197563 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 15 Jul 2016 10:12:55 -0700 Subject: [PATCH 23/38] Add note about support intentions --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1eddf62a3..5f1fb2486 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad ## Browser Support +Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: + - Chrome 48+ - Edge 13+ - Firefox 44+ @@ -19,6 +21,8 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad - Opera 35+ - Safari 8+ +xterm.js may work on earlier versions of the browsers but these are the browsers we strive to keep working. + ## Demo To launch the demo simply run: From fedc1fd325a372b8e56a9dc98a0cc1720953464b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 15 Jul 2016 10:17:29 -0700 Subject: [PATCH 24/38] Fix keydown conditional --- src/xterm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index d6883196b..b834702da 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2673,7 +2673,7 @@ * @param {KeyboardEvent} ev The keydown event to be handled. */ Terminal.prototype.keyDown = function(ev) { - if (this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { + if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { return; } From a3a7017f6b0f4478d84a9656fd68c92240606f99 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 15 Jul 2016 10:22:31 -0700 Subject: [PATCH 25/38] Fix tests --- test/test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index e98ce25e2..e398eb11f 100644 --- a/test/test.js +++ b/test/test.js @@ -88,7 +88,12 @@ describe('xterm.js', function() { xterm.showCursor = function() {}; xterm.clearSelection = function() {}; xterm.compositionHelper = { - isComposing: false + isComposing: false, + keydown: { + bind: function() { + return function() { return true; }; + } + } }; }); From 0010a5a109cdc36975109aa5399f2d252e0e8244 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 15 Jul 2016 16:33:30 -0700 Subject: [PATCH 26/38] Explicitly return from keydown --- src/xterm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index b834702da..71541c3f3 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2674,7 +2674,7 @@ */ Terminal.prototype.keyDown = function(ev) { if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { - return; + return false; } var self = this; From 9dd64dfb4947e45bd1b69e84ede1b937d0d34f7c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 15 Jul 2016 16:41:40 -0700 Subject: [PATCH 27/38] Switch linkify for fit in readme Linkify doesn't work currently so it shouldn't be used as an example. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9720507d4..5e179eedb 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,13 @@ To use an addon, just include the JavaScript file after xterm.js and before the ```html - + ``` ```js var xterm = new Terminal(); // init code... -xterm.linkify(); +xterm.fit(); ``` ## Contribution and License Agreement From d91fa3b6fbd9680ac714cde17f6631b743f515e9 Mon Sep 17 00:00:00 2001 From: Paris Date: Sun, 17 Jul 2016 21:55:16 +0300 Subject: [PATCH 28/38] Add CONTRIBUTING.md --- CONTRIBUTING.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8b33d183c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# How to contribute to xterm.js + +- [Opening issues for bug reports or feature requests](#opening-issues) +- [Contributing code](#contributing-code) + +## Opening issues + +The preferred way to report bugs or request features is to use +[GitHub issues](http://github.com/sourcelair/xterm.js/issues). Before +opening an issue, read these pointers. + +### Opening issues effectively + +- Include information about **the browser in which the problem occurred**. Even + if you tested several browsers, and the problem occurred in all of them, + mention this fact in the bug report. Also include browser version numbers and + the operating system that you're on. + +- Mention which release of xterm.js you're using. Preferably, try also with + the current HEAD of the master branch, to ensure the problem has not already been + fixed. + +- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the + exact steps a maintainer has to take to make the problem occur. + +- If the problem can not be reproduced in the [demo of xterm.js](README.md#demo), please provide an HTML document that demonstrates the problem. + +- Be polite. Issues with an indignant or belligerent tone tend to be moved to the + bottom of the pile. + +## Contributing code + +- Make sure you have a [GitHub account](https://github.com/join) +- Fork [xterm.js](https://github.com/sourcelair/xterm.js/) + ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) +- Make your changes +- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory. +- Follow the general code style of the rest of the project (see below). +- Submit a pull request +([how to create a pull request](https://help.github.com/articles/fork-a-repo)). + Don't put more than one feature/fix in a single pull request. + +By contributing code to xterm.js you + + - agree to license the contributed code under xterm.js' [MIT + license](LICENSE). + + - confirm that you have the right to contribute and license the code + in question. (Either you hold all rights on the code, or the rights + holder has explicitly granted the right to use it like this, + through a compatible open source license or through a direct + agreement with you.) From 7cd99d1559310b7b2ff6b1843575edfa833cf0f8 Mon Sep 17 00:00:00 2001 From: Paris Date: Sun, 17 Jul 2016 22:04:48 +0300 Subject: [PATCH 29/38] Add Development and Contribution section in README.md --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1cd275613..7c5ff60af 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,26 @@ var xterm = new Terminal(); xterm.fit(); ``` -## Contribution and License Agreement +## Development and Contribution + +To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) before. + +The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features). + +It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are: + +#### [SourceLair](https://www.sourcelair.com) + +Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser. + +#### [Visual Studio Code](http://code.visualstudio.com/) + +[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set. + +## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. -## License - Copyright (c) 2014-2016, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License) Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) From 94d0009994d192f2a35b4d7cc7bde07b1889925c Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 18 Jul 2016 09:36:01 +0300 Subject: [PATCH 30/38] Add issue template --- .github/ISSUE_TEMPLATE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..cbdf05602 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ + + +## Details +- Browser and browser version: +- OS version: +- xterm.js version: + +Steps to Reproduce: + +1. +2. From 6e6b527d6ed9b5ede2be41c062d3c5d660a67eff Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 18 Jul 2016 09:36:39 +0300 Subject: [PATCH 31/38] Improve issue template --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cbdf05602..359353554 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,7 +5,7 @@ - OS version: - xterm.js version: -Steps to Reproduce: +### Steps to reproduce 1. 2. From 9fa0f00c233b0a7bd8478cfe2fe1a05eceb6a22e Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 18 Jul 2016 10:04:04 +0300 Subject: [PATCH 32/38] Add AUTHORS and AUTHORS generator script --- .mailmap | 7 +++++++ AUTHORS | 19 +++++++++++++++++++ bin/generate-authors | 7 +++++++ 3 files changed, 33 insertions(+) create mode 100644 .mailmap create mode 100644 AUTHORS create mode 100755 bin/generate-authors diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..a8de62a9a --- /dev/null +++ b/.mailmap @@ -0,0 +1,7 @@ +Antonis Kalipetis +Antonis Kalipetis +Daniel Imms +Paris Kasidiaris +Paris Kasidiaris +Thanasis Daglis +Thanasis Daglis diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..9d8ffb79c --- /dev/null +++ b/AUTHORS @@ -0,0 +1,19 @@ +List of xterm.js contributors. Updated before every release. + +Alexander Olsson +Antonis Kalipetis +Austin Robertson +Benjamin Fischer +Carson Anderson +Christopher Jeffrey +Daniel Imms +Daniel Risacher +Dan Kaplun +Darin Morrison +Jean Bruenn +Jörg Breitbart +Paris Kasidiaris +Paris Kasidiaris +runarberg +Steven Silvester +Thanasis Daglis diff --git a/bin/generate-authors b/bin/generate-authors new file mode 100755 index 000000000..21446ad49 --- /dev/null +++ b/bin/generate-authors @@ -0,0 +1,7 @@ +#! /usr/bin/env sh + +tail --lines=+3 AUTHORS > AUTHORS.tmp +git log --format='%aN <%aE>' >> AUTHORS.tmp +echo "List of xterm.js contributors. Updated before every release.\n" > AUTHORS +sort -u AUTHORS.tmp >> AUTHORS +rm -f AUTHORS.tmp From 7fdfc622374677f3989d71df082beabec35b3c34 Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 18 Jul 2016 10:43:35 +0300 Subject: [PATCH 33/38] Introduce `prepare-release` script --- bin/prepare-release | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 bin/prepare-release diff --git a/bin/prepare-release b/bin/prepare-release new file mode 100755 index 000000000..49feacaf5 --- /dev/null +++ b/bin/prepare-release @@ -0,0 +1,27 @@ +#! /usr/bin/env sh + +# Usage: ./bin/prepare-release x.y.z +# x.y.z should be semver (e.g. 1.0.0) + +NEW_VERSION=$1 +CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \ + | grep version \ + | head -1 \ + | awk -F: '{ print $2 }' \ + | sed 's/[",]//g' \ + | tr -d '[[:space:]]') +CURRENT_BOWER_JSON_VERSION=$(cat bower.json \ + | grep version \ + | head -1 \ + | awk -F: '{ print $2 }' \ + | sed 's/[",]//g' \ + | tr -d '[[:space:]]') + +# Update version in package.json and bower.json +sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json +sed -i "s/\"version\": \"$CURRENT_BOWER_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" bower.json + +# Update AUTHORS file +sh bin/generate-authors + +git commit -a -m "Bump version to $NEW_VERSION" From 14cd4315c7c83a0de32e0a71c641680cc60b5f50 Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 18 Jul 2016 07:46:19 +0000 Subject: [PATCH 34/38] Bump version to 1.0.0 --- bower.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index 33fbf7d37..409fe2137 100644 --- a/bower.json +++ b/bower.json @@ -1,5 +1,5 @@ { "name": "xterm.js", - "version": "0.33", + "version": "1.0.0", "ignore": ["demo", "test", ".gitignore"] } diff --git a/package.json b/package.json index 488820066..0141ca2b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xterm", - "version": "0.33.0", + "version": "1.0.0", "ignore": [ "demo", "test", From ec7130b46f4cea07cb0f398dc7384df1555189b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 21 Jul 2016 12:39:36 +0200 Subject: [PATCH 35/38] fixing arrow key sequences --- src/xterm.js | 79 ++++++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 49 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index d88ddf49e..de32690eb 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2581,6 +2581,7 @@ // The number of characters to scroll, if this is defined it will cancel the event scrollDisp: undefined }; + var modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; switch (ev.keyCode) { // backspace case 8: @@ -2611,61 +2612,39 @@ break; // left-arrow case 37: - if (ev.altKey) { - result.key = '\x1bb' // Jump a word back - result.cancel = true; - break; - } - if (ev.ctrlKey) { - result.key = '\x1b[5D'; // Jump a word back - break; - } - if (this.applicationCursor) { - result.key = '\x1bOD'; // SS3 as ^[O for 7-bit - break; - } - result.key = '\x1b[D'; + if (modifiers) + result.key = '\x1b[1;' + (modifiers + 1) + 'D'; + else if (this.applicationCursor) + result.key = '\x1bOD'; + else + result.key = '\x1b[D'; break; // right-arrow case 39: - if (ev.altKey) { - result.key = '\x1bf' // Jump a word forward - result.cancel = true; - break; - } - if (ev.ctrlKey) { - result.key = '\x1b[5C'; // Jump a word forward - break; - } - if (this.applicationCursor) { + if (modifiers) + result.key = '\x1b[1;' + (modifiers + 1) + 'C'; + else if (this.applicationCursor) result.key = '\x1bOC'; - break; - } - result.key = '\x1b[C'; + else + result.key = '\x1b[C'; break; // up-arrow case 38: - if (this.applicationCursor) { + if (modifiers) + result.key = '\x1b[1;' + (modifiers + 1) + 'A'; + else if (this.applicationCursor) result.key = '\x1bOA'; - break; - } - if (ev.ctrlKey) { - result.scrollDisp = -1; - } else { + else result.key = '\x1b[A'; - } break; // down-arrow case 40: - if (this.applicationCursor) { + if (modifiers) + result.key = '\x1b[1;' + (modifiers + 1) + 'B'; + else if (this.applicationCursor) result.key = '\x1bOB'; - break; - } - if (ev.ctrlKey) { - result.scrollDisp = 1; - } else { + else result.key = '\x1b[B'; - } break; // insert case 45: @@ -2679,19 +2658,21 @@ case 46: result.key = '\x1b[3~'; break; // home case 36: - if (this.applicationKeypad) { + if (modifiers) + result.key = '\x1b[1;' + (modifiers + 1) + 'H'; + else if (this.applicationCursor) result.key = '\x1bOH'; - break; - } - result.key = '\x1bOH'; + else + result.key = '\x1b[H'; break; // end case 35: - if (this.applicationKeypad) { + if (modifiers) + result.key = '\x1b[1;' + (modifiers + 1) + 'F'; + else if (this.applicationCursor) result.key = '\x1bOF'; - break; - } - result.key = '\x1bOF'; + else + result.key = '\x1b[F'; break; // page up case 33: From 8e7eeff45c866355912e4032d8a98f795e2e029c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 21 Jul 2016 12:47:19 +0200 Subject: [PATCH 36/38] fixing test cases --- test/test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test.js b/test/test.js index 5059a2428..b04b091ff 100644 --- a/test/test.js +++ b/test/test.js @@ -24,8 +24,8 @@ describe('xterm.js', function() { assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~ assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~ // End, Home - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1bOF'); // SS3 F - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1bOH'); // SS3 H + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H // Left, up, right, down arrows assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A @@ -50,10 +50,10 @@ describe('xterm.js', function() { assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~ }); it('should return \\x1b[5D for ctrl+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[5D'); // CSI 5 D + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D }); it('should return \\x1b[5C for ctrl+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[5C'); // CSI 5 C + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C }); }); From 4595a18197a9f7378077b078c06719ecb16adb38 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 21 Jul 2016 10:51:43 -0700 Subject: [PATCH 37/38] Tidy up blur/focus handlers --- src/xterm.js | 52 ++++++++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 71541c3f3..01afe3f02 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -307,9 +307,9 @@ if (cursor) { this.compositionView.style.left = cursor.offsetLeft + 'px'; this.compositionView.style.top = cursor.offsetTop + 'px'; + this.textarea.style.left = cursor.offsetLeft + 'px'; + this.textarea.style.top = (cursor.offsetTop + cursor.offsetHeight) + 'px'; } - this.textarea.style.left = cursor.offsetLeft + 'px'; - this.textarea.style.top = (cursor.offsetTop + cursor.offsetHeight) + 'px'; }; /** @@ -625,18 +625,7 @@ * Focus the terminal. Delegates focus handling to the terminal's DOM element. */ Terminal.prototype.focus = function() { - if (document.activeElement === this.textarea) { - return; - } - - if (this.sendFocus) { - this.send('\x1b[I'); - } - - this.element.classList.add('focus'); - this.showCursor(); - this.textarea.focus(); - Terminal.focus = this; + return this.element.focus(); }; /** @@ -646,11 +635,17 @@ */ Terminal.bindFocus = function (term) { on(term.element, 'focus', function (ev) { + if (Terminal.focus === term) { + return; + } + if (term.sendFocus) { term.send('\x1b[I'); } + term.element.classList.add('focus'); term.showCursor(); + term.textarea.focus(); Terminal.focus = term; term.emit('focus', {terminal: term}); }); @@ -660,18 +655,7 @@ * Blur the terminal. Delegates blur handling to the terminal's DOM element. */ Terminal.prototype.blur = function() { - if (Terminal.focus !== this) { - return; - } - - this.element.classList.remove('focus'); - this.cursorState = 0; - this.refresh(this.y, this.y); - this.textarea.blur(); - if (this.sendFocus) { - this.send('\x1b[0]'); - } - Terminal.focus = null; + return this.element.blur(); }; /** @@ -681,6 +665,12 @@ */ Terminal.bindBlur = function (term) { on(term.element, 'blur', function (ev) { + if (Terminal.focus !== term) { + return; + } + term.element.classList.remove('focus'); + term.refresh(term.y, term.y); + term.textarea.blur(); if (term.sendFocus) { term.send('\x1b[O'); } @@ -696,6 +686,8 @@ Terminal.bindPaste(this); Terminal.bindKeys(this); Terminal.bindCopy(this); + Terminal.bindFocus(this); + Terminal.bindBlur(this); }; /** @@ -881,12 +873,12 @@ this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('spellcheck', 'false'); this.textarea.tabIndex = 0; - this.textarea.onfocus = function() { + this.textarea.addEventListener('focus', function() { self.emit('focus', {terminal: self}); - } - this.textarea.onblur = function() { + }); + this.textarea.addEventListener('blur', function() { self.emit('blur', {terminal: self}); - } + }); this.helperContainer.appendChild(this.textarea); this.compositionView = document.createElement('div'); From 892db98c96d9779ef2934aaf3103161a24b1e531 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 22 Jul 2016 04:13:59 -0700 Subject: [PATCH 38/38] Correct blur/focus delegation Focus and blur events on the Terminal object are delegated to the textarea which handles everything. --- src/xterm.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 13d3494fb..39b4eb163 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -625,7 +625,7 @@ * Focus the terminal. Delegates focus handling to the terminal's DOM element. */ Terminal.prototype.focus = function() { - return this.element.focus(); + return this.textarea.focus(); }; /** @@ -634,18 +634,12 @@ * @static */ Terminal.bindFocus = function (term) { - on(term.element, 'focus', function (ev) { - if (Terminal.focus === term) { - return; - } - + on(term.textarea, 'focus', function (ev) { if (term.sendFocus) { term.send('\x1b[I'); } - term.element.classList.add('focus'); term.showCursor(); - term.textarea.focus(); Terminal.focus = term; term.emit('focus', {terminal: term}); }); @@ -655,7 +649,7 @@ * Blur the terminal. Delegates blur handling to the terminal's DOM element. */ Terminal.prototype.blur = function() { - return this.element.blur(); + return this.textarea.blur(); }; /** @@ -664,16 +658,12 @@ * @static */ Terminal.bindBlur = function (term) { - on(term.element, 'blur', function (ev) { - if (Terminal.focus !== term) { - return; - } - term.element.classList.remove('focus'); + on(term.textarea, 'blur', function (ev) { term.refresh(term.y, term.y); - term.textarea.blur(); if (term.sendFocus) { term.send('\x1b[O'); } + term.element.classList.remove('focus'); Terminal.focus = null; term.emit('blur', {terminal: term}); });