blob: f351a90be921da3c2a1572ecf12dd68a5b0573b5 [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
lib.rtdep('lib.colors', 'lib.f', 'lib.fs', 'lib.MessageManager');
/**
* Constructor a new ConnectDialog instance.
*
* There should only be one of these, and it assumes the connect dialog is
* the only thing in the current window.
*
* @param {MessagePort} messagePort The HTML5 message port we should use to
* communicate with the nassh instance.
*/
nassh.ConnectDialog = function(messagePort) {
// Message port back to the terminal.
this.messagePort_ = messagePort;
this.messagePort_.onmessage = this.onMessage_.bind(this);
this.messagePort_.start();
// Turn off spellcheck everywhere.
var ary = document.querySelectorAll('input[type="text"]');
for (var i = 0; i < ary.length; i++) {
ary[i].setAttribute('spellcheck', 'false');
}
// The Message Manager instance, null until the messages have loaded.
this.mm_ = null;
// The nassh global pref manager.
this.prefs_ = new nassh.PreferenceManager();
this.prefs_.readStorage(() => {
this.syncProfiles_(this.onPreferencesReady_.bind(this));
});
// The profile we're currently displaying.
this.currentProfileRecord_ = null;
// The 'new' profile is special in that it doesn't have a real id or
// prefs object until it is saved for the first time.
this.emptyProfileRecord_ = new nassh.ConnectDialog.ProfileRecord(
'new', null, '[New Connection]');
// Map of id->nassh.ConnectDialog.ProfileRecord.
this.profileMap_ = {};
// Array of nassh.ConnectDialog.ProfileRecord instances in display order.
this.profileList_ = [];
// Cached DOM nodes.
this.form_ = document.querySelector('form');
this.mountButton_ = document.querySelector('#mount');
this.unmountButton_ = document.querySelector('#unmount');
this.connectButton_ = document.querySelector('#connect');
this.deleteButton_ = document.querySelector('#delete');
this.optionsButton_ = document.querySelector('#options');
};
/**
* Global window message handler, uninstalled after proper handshake.
*/
nassh.ConnectDialog.onWindowMessage = function(e) {
if (e.data.name != 'ipc-init') {
console.warn('Unknown message from terminal:', e.data);
return;
}
window.removeEventListener('message', nassh.ConnectDialog.onWindowMessage);
lib.init(function() {
window.dialog_ = new nassh.ConnectDialog(e.data.argv[0].messagePort);
});
};
// Register the message listener.
window.addEventListener('message', nassh.ConnectDialog.onWindowMessage);
/**
* Called by the preference manager when we've retrieved the current preference
* values from storage.
*/
nassh.ConnectDialog.prototype.onPreferencesReady_ = function() {
// Create and draw the shortcut list.
this.shortcutList_ = new nassh.ColumnList(
document.querySelector('#shortcut-list'), this.profileList_);
// Install various (DOM and non-DOM) event handlers.
this.installHandlers_();
var profileIndex = 0;
if (this.profileList_.length == 1) {
// Just one profile record? It's the "New..." profile, focus the form.
this.$f('description').focus();
} else {
this.shortcutList_.focus();
chrome.storage.local.get('/nassh/connectDialog/lastProfileId', (items) => {
var lastProfileId = items['/nassh/connectDialog/lastProfileId'];
if (lastProfileId)
profileIndex = Math.max(0, this.getProfileIndex_(lastProfileId));
this.shortcutList_.setActiveIndex(profileIndex);
this.setCurrentProfileRecord(this.profileList_[profileIndex]);
});
}
this.shortcutList_.setActiveIndex(profileIndex);
// The shortcut list will eventually do this async, but we want it now...
this.setCurrentProfileRecord(this.profileList_[profileIndex]);
nassh.getFileSystem(this.onFileSystemFound_.bind(this));
};
/**
* Simple struct to collect data about a profile.
*/
nassh.ConnectDialog.ProfileRecord = function(id, prefs, opt_textContent) {
this.id = id;
this.prefs = prefs;
this.textContent = opt_textContent || prefs.get('description');
};
/**
* Get a localized message from the Message Manager.
*
* This converts all message name to UPPER_AND_UNDER format, since that's
* pretty handy in the connect dialog.
*/
nassh.ConnectDialog.prototype.msg = function(name, opt_args) {
if (!this.mm_)
return 'loading...';
return this.mm_.get(name.toUpperCase().replace(/-/g, '_'), opt_args);
};
/**
* Align the bottom fields.
*
* We want a grid-like layout for these fields. This is not easily done with
* box layout, but since we're using a fixed width font it's a simple hack.
*/
nassh.ConnectDialog.prototype.alignLabels_ = function() {
var labels = document.querySelectorAll('.aligned-dialog-labels');
let maxWidth = 0;
labels.forEach((el) => maxWidth = Math.max(maxWidth, el.clientWidth));
labels.forEach((el) => el.style.width = `${maxWidth}px`);
};
/**
* Install various event handlers.
*/
nassh.ConnectDialog.prototype.installHandlers_ = function() {
// Small utility to connect DOM events.
function addListeners(node, events, handler, var_args) {
for (var i = 2; i < arguments.length; i++) {
handler = arguments[i];
for (var j = 0; j < events.length; j++) {
node.addEventListener(events[j], handler);
}
}
}
// Observe global 'profile-ids' list so we can keep the ColumnList updated.
this.prefs_.addObservers(null, {
'profile-ids': this.onProfileListChanged_.bind(this)
});
// Same for the 'description' field of all known profiles.
for (var i = 0; i < this.profileList_.length; i++) {
var rec = this.profileList_[i];
if (rec.prefs) {
rec.prefs.addObservers(null, {
description: this.onDescriptionChanged_.bind(this)
});
}
}
// Watch for keypresses sent anywhere in this frame.
document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this));
// Watch for selection changes on the ColumnList so we can keep the
// 'billboard' updated.
this.shortcutList_.onActiveIndexChanged =
this.onProfileIndexChanged.bind(this);
// Register for keyboard shortcuts on the column list.
this.shortcutList_.addEventListener('keydown',
this.onShortcutListKeyDown_.bind(this));
this.shortcutList_.addEventListener('dblclick',
this.onShortcutListDblClick_.bind(this));
this.form_.addEventListener('keyup', this.onFormKeyUp_.bind(this));
this.connectButton_.addEventListener('keypress',
this.onButtonKeypress_.bind(this));
this.deleteButton_.addEventListener('keypress',
this.onButtonKeypress_.bind(this));
this.mountButton_.addEventListener('click',
this.onMountClick_.bind(this));
this.unmountButton_.addEventListener('click',
this.onUnmountClick_.bind(this));
this.connectButton_.addEventListener('click',
this.onConnectClick_.bind(this));
this.deleteButton_.addEventListener('click',
this.onDeleteClick_.bind(this));
this.optionsButton_.addEventListener('click',
this.onOptionsClick_.bind(this));
this.$f('identity').addEventListener('select', function(e) {
if (e.target.value == '')
e.target.selectedIndex = 0;
});
// These fields interact with each-other's placeholder text.
['description', 'username', 'hostname', 'port'
].forEach((name) => {
var field = this.$f(name);
// Alter description or detail placeholders, and commit the pref.
addListeners(field, ['change', 'keypress', 'keyup'],
this.updatePlaceholders_.bind(this, name),
this.maybeDirty_.bind(this, name));
addListeners(field, ['focus'],
this.maybeCopyPlaceholder_.bind(this, name));
});
this.$f('description').addEventListener(
'blur', this.maybeCopyPlaceholders_.bind(this));
// These fields are plain text with no fancy properties.
['argstr', 'terminal-profile', 'mount-path',
].forEach((name) => {
var field = this.$f(name)
addListeners(field,
['change', 'keypress', 'keyup'],
this.maybeDirty_.bind(this, name));
});
['description', 'username', 'hostname', 'port', 'relay-options',
'identity', 'argstr', 'terminal-profile', 'mount-path',
].forEach((name) => {
addListeners(this.$f(name), ['focus', 'blur'],
this.onFormFocusChange_.bind(this, name));
});
// Listen for DEL on the identity select box.
this.$f('identity').addEventListener('keyup', (e) => {
if (e.keyCode == 46 && e.target.selectedIndex != 0) {
this.deleteIdentity_(e.target.value);
}
});
this.importFileInput_ = document.querySelector('#import-file-input');
this.importFileInput_.addEventListener(
'change', this.onImportFiles_.bind(this));
var importLink = document.querySelector('#import-link');
importLink.addEventListener('click', (e) => {
this.importFileInput_.click();
e.preventDefault();
});
};
/**
* Quick way to ask for a '#field-' element from the dom.
*/
nassh.ConnectDialog.prototype.$f = function(
name, opt_attrName, opt_attrValue) {
var node = document.querySelector('#field-' + name);
if (!node)
throw new Error('Can\'t find: #field-' + name);
if (!opt_attrName)
return node;
if (typeof opt_attrValue == 'undefined')
return node.getAttribute(opt_attrName);
node.setAttribute(opt_attrName, opt_attrValue);
};
/**
* Change the active profile.
*/
nassh.ConnectDialog.prototype.setCurrentProfileRecord = function(
profileRecord) {
if (!profileRecord)
throw 'null profileRecord.';
this.currentProfileRecord_ = profileRecord;
this.syncForm_();
// For console debugging.
window.p_ = profileRecord;
};
/**
* Change the enabled state of one of our <div role='button'> elements.
*
* Since they're not real <button> tags the don't react properly to the
* disabled property.
*/
nassh.ConnectDialog.prototype.enableButton_ = function(button, state) {
if (state) {
button.removeAttribute('disabled');
button.setAttribute('tabindex', '0');
} else {
button.setAttribute('disabled', 'disabled');
button.setAttribute('tabindex', '-1');
}
};
/**
* Change the display state of one of our <div role='button'> elements.
*/
nassh.ConnectDialog.prototype.displayButton_ = function(
button, state, style='inline') {
if (state) {
button.style.display = style;
button.setAttribute('tabindex', '0');
} else {
button.style.display = 'none';
button.setAttribute('tabindex', '-1');
}
};
/**
* Change the mounted state of the mount button.
*/
nassh.ConnectDialog.prototype.displayMountButton_ = function(state) {
this.displayButton_(document.querySelector('#mount-path'), state, 'flex');
if (!state) {
this.displayButton_(this.mountButton_, false);
this.displayButton_(this.unmountButton_, false);
return;
}
chrome.fileSystemProvider.getAll((fileSystems) => {
for (var i in fileSystems) {
if (fileSystems[i].fileSystemId == this.currentProfileRecord_.id) {
this.displayButton_(this.mountButton_, false);
this.displayButton_(this.unmountButton_, true);
return;
}
}
this.displayButton_(this.mountButton_, true);
this.displayButton_(this.unmountButton_, false);
this.enableButton_(this.mountButton_, this.form_.checkValidity());
});
};
/**
* Persist the current form to prefs, even if it's invalid.
*/
nassh.ConnectDialog.prototype.save = function() {
if (!this.$f('description').value)
return;
var dirtyForm = false;
var changedFields = {};
var prefs = this.currentProfileRecord_.prefs;
['description', 'username', 'hostname', 'port', 'relay-options',
'identity', 'argstr', 'terminal-profile', 'mount-path',
].forEach((name) => {
if (name == 'identity' && this.$f('identity').selectedIndex === 0)
return;
var value = this.$f(name).value;
if (name == 'port')
value = value ? parseInt(value) : '';
if ((!prefs && !value) || (prefs && value == prefs.get(name)))
return;
dirtyForm = true;
changedFields[name] = value;
});
if (dirtyForm) {
if (!prefs) {
var prefs = this.prefs_.createProfile();
var rec = new nassh.ConnectDialog.ProfileRecord(
prefs.id, prefs, changedFields['description']);
this.currentProfileRecord_ = rec;
prefs.addObservers(null, {
description: this.onDescriptionChanged_.bind(this)
});
this.shortcutList_.afterNextRedraw(() => {
this.shortcutList_.setActiveIndex(this.profileList_.length - 1);
});
}
for (var name in changedFields) {
this.currentProfileRecord_.prefs.set(name, changedFields[name]);
}
}
};
/**
* Mount if the form validates.
*/
nassh.ConnectDialog.prototype.mount = function() {
this.maybeCopyPlaceholders_();
this.save();
var items = {
'/nassh/connectDialog/lastProfileId': this.currentProfileRecord_.id
};
chrome.storage.local.set(items);
if (this.form_.checkValidity())
this.postMessage('mountProfile', [this.currentProfileRecord_.id]);
};
/**
* Unmount the SFTP connection.
*/
nassh.ConnectDialog.prototype.unmount = function() {
var options = {fileSystemId: this.currentProfileRecord_.id};
chrome.extension.getBackgroundPage().
nassh.sftp.fsp.onUnmountRequested(options,
(success) => { this.displayMountButton_(true); },
(error) => { /* do nothing */ });
};
/**
* Save any changes and connect if the form validates.
*/
nassh.ConnectDialog.prototype.connect = function(name, argv) {
this.maybeCopyPlaceholders_();
this.save();
// Since the user has initiated this connection, register the protocol.
nassh.registerProtocolHandler();
var items = {
'/nassh/connectDialog/lastProfileId': this.currentProfileRecord_.id
};
chrome.storage.local.set(items);
if (this.form_.checkValidity())
this.postMessage('connectToProfile', [this.currentProfileRecord_.id]);
};
/**
* Send a message back to the terminal.
*/
nassh.ConnectDialog.prototype.postMessage = function(name, argv) {
this.messagePort_.postMessage({name: name, argv: argv || null});
};
/**
* Set the profile's dirty bit if the given field has changed from it's current
* pref value.
*/
nassh.ConnectDialog.prototype.maybeDirty_ = function(fieldName) {
if (this.currentProfileRecord_.prefs) {
if (this.$f(fieldName).value !=
this.currentProfileRecord_.prefs.get(fieldName)) {
this.currentProfileRecord_.dirty = true;
}
} else {
if (this.$f(fieldName).value)
this.currentProfileRecord_.dirty = true;
}
};
/**
* Invoke the maybeCopyPlaceholder_ method for the fields we're willing
* to bulk-default.
*/
nassh.ConnectDialog.prototype.maybeCopyPlaceholders_ = function() {
['description', 'username', 'hostname', 'port', 'relay-options',
].forEach(this.maybeCopyPlaceholder_.bind(this));
this.syncButtons_();
};
/**
* If the field is empty and the current placeholder isn't the default,
* then initialize the field to the placeholder.
*/
nassh.ConnectDialog.prototype.maybeCopyPlaceholder_ = function(fieldName) {
var field = this.$f(fieldName);
var placeholder = field.getAttribute('placeholder');
if (!field.value && placeholder != this.msg('FIELD_' + fieldName +
'_PLACEHOLDER')) {
field.value = placeholder;
}
};
/**
* Compute the placeholder text for a given field.
*/
nassh.ConnectDialog.prototype.updatePlaceholders_ = function(fieldName) {
if (fieldName == 'description') {
// If the description changed, update the username/host/etc placeholders.
this.updateDetailPlaceholders_();
} else {
// Otherwise update the description placeholder.
this.updateDescriptionPlaceholder_();
}
// In either case, the hostname might have changed, so cascade updates into
// the relay options.
this.updateRelayOptionsPlaceholder_();
};
/**
* Update the placeholders in the detail (username, hostname, etc) fields.
*/
nassh.ConnectDialog.prototype.updateDetailPlaceholders_ = function() {
// Try to split the description up into the sub-fields.
var ary = this.$f('description').value.match(
/^([^@]+)@([^:@]+)?(?::(\d+)?)?(?:@(.*))?$/);
// Set a blank array if the match failed.
ary = ary || [];
// Remove element 0, the "full match" string.
ary.shift();
// Copy the remaining match elements into the appropriate placeholder
// attribute. Set the default placeholder text from this.str.placeholders
// for any field that was not matched.
['username', 'hostname', 'port'
].forEach((name) => {
var value = ary.shift();
if (!value) {
value = this.msg('FIELD_' + name + '_PLACEHOLDER');
}
this.$f(name, 'placeholder', value);
});
};
/**
* Update the relay-options placeholder.
*/
nassh.ConnectDialog.prototype.updateRelayOptionsPlaceholder_ = function() {
// Google-specific relay hack. This feels dirty. We can revert this once
// we support managed default configs. http://b/28205376 & related docs.
let value = this.msg('FIELD_RELAY_OPTIONS_PLACEHOLDER');
if (!this.$f('relay-options').value) {
let hostname = this.$f('hostname').value;
if (!hostname)
hostname = this.$f('hostname').placeholder;
if (hostname.endsWith('.corp.google.com'))
value = '--config=google';
else if (hostname.endsWith('.c.googlers.com'))
value = '--config=google --proxy-host=sup-ssh-relay.corp.google.com';
}
this.$f('relay-options', 'placeholder', value);
};
/**
* Update the description placeholder.
*/
nassh.ConnectDialog.prototype.updateDescriptionPlaceholder_ = function() {
var username = this.$f('username').value;
var hostname = this.$f('hostname').value;
var placeholder;
if (username && hostname) {
placeholder = username + '@' + hostname;
var v = this.$f('port').value;
if (v)
placeholder += ':' + v;
} else {
placeholder = this.msg('FIELD_DESCRIPTION_PLACEHOLDER');
}
this.$f('description', 'placeholder', placeholder);
};
/**
* Sync the form with the current profile record.
*/
nassh.ConnectDialog.prototype.syncForm_ = function() {
['description', 'username', 'hostname', 'port', 'argstr', 'relay-options',
'identity', 'terminal-profile', 'mount-path',
].forEach((n) => {
var emptyValue = '';
if (n == 'identity')
emptyValue = this.$f('identity').firstChild.textContent;
if (this.currentProfileRecord_.prefs) {
this.$f(n).value =
this.currentProfileRecord_.prefs.get(n) || emptyValue;
} else {
this.$f(n).value = emptyValue;
}
});
this.updateDetailPlaceholders_();
this.updateDescriptionPlaceholder_();
};
/**
* Checks whether the current machine can use the File System Provider API, and
* thus be able to be mounted.
*/
nassh.ConnectDialog.prototype.checkMountable_ = function() {
return chrome.fileSystemProvider !== undefined;
};
/**
* Sync the states of the buttons.
*/
nassh.ConnectDialog.prototype.syncButtons_ = function() {
this.enableButton_(
this.deleteButton_,
document.activeElement.getAttribute('id') == 'shortcut-list');
this.enableButton_(this.connectButton_, this.form_.checkValidity());
this.displayMountButton_(this.checkMountable_());
};
/**
* Sync the identity dropdown box with the filesystem.
*/
nassh.ConnectDialog.prototype.syncIdentityDropdown_ = function(opt_onSuccess) {
var keyfileNames = [];
var identitySelect = this.$f('identity');
var selectedName;
if (this.currentProfileRecord_.prefs) {
selectedName = this.currentProfileRecord_.prefs.get('identity');
} else {
selectedName = identitySelect.value;
}
function clearSelect() {
while (identitySelect.firstChild) {
identitySelect.removeChild(identitySelect.firstChild);
}
}
var onReadError = () => {
clearSelect();
var option = document.createElement('option');
option.textContent = 'Error!';
identitySelect.appendChild(option);
};
var onReadSuccess = (entries) => {
for (var key in entries) {
var ary = key.match(/^(.*)\.pub/);
if (ary && ary[1] in entries)
keyfileNames.push(ary[1]);
}
clearSelect();
var option = document.createElement('option');
option.textContent = '[default]';
identitySelect.appendChild(option);
for (var i = 0; i < keyfileNames.length; i++) {
var option = document.createElement('option');
option.textContent = keyfileNames[i];
identitySelect.appendChild(option);
if (keyfileNames[i] == selectedName)
identitySelect.selectedIndex = i;
}
this.syncForm_();
if (opt_onSuccess)
opt_onSuccess();
};
lib.fs.readDirectory(this.fileSystem_.root, '/.ssh/', onReadSuccess,
lib.fs.err('Error enumerating /.ssh/', onReadError));
};
/**
* Delete one a pair of identity files from the html5 filesystem.
*/
nassh.ConnectDialog.prototype.deleteIdentity_ = function(identityName) {
var count = 0;
var onRemove = () => {
if (++count == 2)
this.syncIdentityDropdown_();
};
lib.fs.removeFile(this.fileSystem_.root, '/.ssh/' + identityName,
onRemove);
lib.fs.removeFile(this.fileSystem_.root, '/.ssh/' + identityName + '.pub',
onRemove);
};
nassh.ConnectDialog.prototype.deleteProfile_ = function(deadID) {
if (this.currentProfileRecord_.id == deadID) {
// The actual profile removal and list-updating will happen async.
// Rather than come up with a fancy hack to update the selection when
// it's done, we just move it before the delete.
var currentIndex = this.shortcutList_.activeIndex;
if (currentIndex == this.profileList_.length - 1) {
// User is deleting the last (non-new) profile, select the one before
// it.
this.shortcutList_.setActiveIndex(this.profileList_.length - 2);
} else {
this.shortcutList_.setActiveIndex(currentIndex + 1);
}
}
this.prefs_.removeProfile(deadID);
};
/**
* Return the index into this.profileList_ for a given profile id.
*
* Returns -1 if the id is not found.
*/
nassh.ConnectDialog.prototype.getProfileIndex_ = function(id) {
for (var i = 0; i < this.profileList_.length; i++) {
if (this.profileList_[i].id == id)
return i;
}
return -1;
};
/**
* Sync the ColumnList with the known profiles.
*/
nassh.ConnectDialog.prototype.syncProfiles_ = function(opt_callback) {
var ids = this.prefs_.get('profile-ids');
this.profileList_.length = 0;
var currentProfileExists = false;
var emptyProfileExists = false;
var deadProfiles = Object.keys(this.profileMap_);
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var p;
if (this.currentProfileRecord_ && id == this.currentProfileRecord_.id)
currentProfileExists = true;
if (id == this.emptyProfileRecord_.id) {
emptyProfileExists = true;
p = this.emptyProfileRecord_;
} else {
p = this.profileMap_[id];
}
deadProfiles.splice(deadProfiles.indexOf(id), 1);
if (!p) {
p = this.profileMap_[id] = new nassh.ConnectDialog.ProfileRecord(
id, this.prefs_.getProfile(id));
} else if (p.prefs) {
p.textContent = p.prefs.get('description');
}
this.profileList_.push(p);
}
for (var i = 0; i < deadProfiles.length; i++) {
delete this.profileMap_[deadProfiles[i]];
}
if (!currentProfileExists) {
this.setCurrentProfileRecord(this.emptyProfileRecord_);
}
if (!emptyProfileExists) {
this.profileList_.unshift(this.emptyProfileRecord_);
this.profileMap_[this.emptyProfileRecord_.id] = this.emptyProfileRecord_;
}
if (this.profileList_.length == 1) {
if (opt_callback)
opt_callback();
}
// Start at 1 for the "[New Connection]" profile.
var initialized = 1;
var onRead = function(profile) {
profile.textContent = profile.prefs.get('description');
if ((++initialized == this.profileList_.length) && opt_callback)
opt_callback();
};
this.profileList_.forEach((profile) => {
if (profile.prefs)
profile.prefs.readStorage(onRead.bind(this, profile));
});
};
/**
* Success callback for lib.fs.getFileSystem().
*
* Kick off the "Identity" dropdown now that we have access to the filesystem.
*/
nassh.ConnectDialog.prototype.onFileSystemFound_ = function(
fileSystem, sshDirectoryEntry) {
this.fileSystem_ = fileSystem;
this.sshDirectoryEntry_ = sshDirectoryEntry;
this.syncIdentityDropdown_();
// Tell the parent we're ready to roll.
this.postMessage('ipc-init-ok');
};
/**
* User initiated file import.
*
* This is the onChange handler for the `input type="file"`
* (aka this.importFileInput_) control.
*/
nassh.ConnectDialog.prototype.onImportFiles_ = function(e) {
var input = this.importFileInput_;
var select = this.$f('identity');
var onImportSuccess = () => {
this.syncIdentityDropdown_(function() {
select.selectedIndex = select.childNodes.length - 1;
});
};
if (!input.files.length)
return;
nassh.importFiles(this.fileSystem_, '/.ssh/', input.files, onImportSuccess);
return false;
};
/**
* Keydown event anywhere in this frame.
*
* @param {KeyboardEvent} e The user keydown event to process.
* @return {bool} Whether to keep processing the event.
*/
nassh.ConnectDialog.prototype.onDocumentKeyDown_ = function(e) {
const key = String.fromCharCode(e.keyCode);
const lowerKey = key.toLowerCase();
let cancel = false;
// Swallow common shortcuts that don't make sense in this app.
switch (lowerKey) {
// Shortcuts where we kill both the non-shift and shift variants.
case 'n': // New window (!shift) and new incognito window (shift).
case 'p': // Chrome print (!shift) and OS print (shift).
case 'o': // Open (!shift) and bookmark manager (shift).
case 't': // New tab (!shift) and new incognito tab (shift).
if (e.ctrlKey && !e.altKey && !e.metaKey)
cancel = true;
break;
// Shortcuts where we only kill non-shift variants (and allow shift).
case 'j': // Downloads.
case 'h': // History.
case 's': // Save.
case 'u': // View source.
if (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
cancel = true;
break;
}
if (cancel) {
e.preventDefault();
e.stopPropagation();
return false;
}
return true;
};
/**
* Keydown event on the shortcut list.
*/
nassh.ConnectDialog.prototype.onShortcutListKeyDown_ = function(e) {
var isNewConnection = this.currentProfileRecord_ == this.emptyProfileRecord_;
if (e.keyCode == 46) {
// DEL delete the profile.
if (!isNewConnection) {
this.deleteProfile_(this.currentProfileRecord_.id);
} else {
// Otherwise the user is deleting the placeholder profile. All we
// do here is reset the form.
this.syncForm_();
this.$f('description').focus();
}
} else if (e.keyCode == 13) {
if (isNewConnection) {
this.$f('description').focus();
} else {
this.onConnectClick_();
}
}
};
nassh.ConnectDialog.prototype.onShortcutListDblClick_ = function(e) {
this.onConnectClick_();
};
/**
* Called when the ColumnList says the active profile changed.
*/
nassh.ConnectDialog.prototype.onProfileIndexChanged = function(e) {
this.setCurrentProfileRecord(this.profileList_[e.now]);
this.syncButtons_();
};
/**
* Key press while a button has focus.
*/
nassh.ConnectDialog.prototype.onButtonKeypress_ = function(e) {
if (e.charCode == 13 || e.charCode == 32)
e.srcElement.click();
};
/**
* Someone clicked on the mount button.
*/
nassh.ConnectDialog.prototype.onMountClick_ = function(e) {
this.mount();
};
/**
* Someone clicked on the unmount button.
*/
nassh.ConnectDialog.prototype.onUnmountClick_ = function(e) {
this.unmount();
};
/**
* Someone clicked on the connect button.
*/
nassh.ConnectDialog.prototype.onConnectClick_ = function(e) {
if (this.connectButton_.getAttribute('disabled'))
return;
this.connect();
};
/**
* Someone clicked on the delete button.
*/
nassh.ConnectDialog.prototype.onDeleteClick_ = function(e) {
if (this.deleteButton_.getAttribute('disabled'))
return;
if (document.activeElement.getAttribute('id') == 'field-identity') {
this.deleteIdentity_(e.target.value);
} else {
this.deleteProfile_(this.currentProfileRecord_.id);
this.shortcutList_.focus();
}
};
/**
* Someone clicked on the options button.
*/
nassh.ConnectDialog.prototype.onOptionsClick_ = nassh.openOptionsPage;
/**
* KeyUp on the form element.
*/
nassh.ConnectDialog.prototype.onFormKeyUp_ = function(e) {
if (e.keyCode == 13) { // ENTER
this.connect();
} else if (e.keyCode == 27) { // ESC
this.syncForm_();
this.shortcutList_.focus();
}
};
/**
* Focus change on the form element.
*
* This handler is registered to every form element's focus and blur events.
* Keep in mind that for change in focus from one input to another will invoke
* this twice.
*/
nassh.ConnectDialog.prototype.onFormFocusChange_ = function(e) {
this.syncButtons_();
this.save();
};
/**
* Pref callback invoked when the global 'profile-ids' changed.
*/
nassh.ConnectDialog.prototype.onProfileListChanged_ = function() {
this.syncProfiles_(() => { this.shortcutList_.redraw() });
};
/**
* Pref callback invoked when a profile's description has changed.
*/
nassh.ConnectDialog.prototype.onDescriptionChanged_ = function(
value, name, prefs) {
if (this.profileMap_[prefs.id]) {
this.profileMap_[prefs.id].textContent = value;
this.shortcutList_.scheduleRedraw();
}
};
/**
* Handle a message from the terminal.
*/
nassh.ConnectDialog.prototype.onMessage_ = function(e) {
if (e.data.name in this.onMessageName_) {
this.onMessageName_[e.data.name].apply(this, e.data.argv);
} else {
console.warn('Unhandled message: ' + e.data.name, e.data);
}
};
/**
* Terminal message handlers.
*/
nassh.ConnectDialog.prototype.onMessageName_ = {};
/**
* terminal-info: The terminal introduces itself.
*/
nassh.ConnectDialog.prototype.onMessageName_['terminal-info'] = function(info) {
this.mm_ = new lib.MessageManager(info.acceptLanguages);
this.mm_.processI18nAttributes(document.body);
this.updateDetailPlaceholders_();
this.updateDescriptionPlaceholder_();
document.body.style.fontFamily = info.fontFamily;
document.body.style.fontSize = info.fontSize + 'px';
// Now that we've set the font sizes, align the labels.
this.alignLabels_();
var fg = lib.colors.normalizeCSS(info.foregroundColor);
var bg = lib.colors.normalizeCSS(info.backgroundColor);
var cursor = lib.colors.normalizeCSS(info.cursorColor);
var vars = {
'--nassh-bg-color': bg,
'--nassh-fg-color': fg,
'--nassh-cursor-color': cursor,
};
for (var i = 10; i < 100; i += 5) {
vars['--nassh-bg-color-' + i] = lib.colors.setAlpha(bg, i / 100);
vars['--nassh-fg-color-' + i] = lib.colors.setAlpha(fg, i / 100);
vars['--nassh-cursor-color-' + i] = lib.colors.setAlpha(cursor, i / 100);
}
for (var key in vars)
if (key.startsWith('--nassh-'))
document.documentElement.style.setProperty(key, vars[key]);
// Tell the parent we've finished loading all the terminal details.
this.postMessage('terminal-info-ok');
};