Skip to content

Instantly share code, notes, and snippets.

@blink1073
Last active August 26, 2015 12:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blink1073/a0a9198de7bff53d941d to your computer and use it in GitHub Desktop.
Save blink1073/a0a9198de7bff53d941d to your computer and use it in GitHub Desktop.
Keyboard Manager
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* A bunch of predefined `Simple Actions` used by Jupyter.
* `Simple Actions` have the following keys:
* help (optional): a short string the describe the action.
* will be used in various context, like as menu name, tool tips on buttons,
* and short description in help menu.
* help_index (optional): a string used to sort action in help menu.
* icon (optional): a short string that represent the icon that have to be used with this
* action. this should mainly correspond to a Font_awesome class.
* handler : a function which is called when the action is activated. It will receive at first parameter
* a dictionary containing various handle to element of the notebook.
*
* action need to be registered with a **name** that can be use to refer to this action.
*
*
* if `help` is not provided it will be derived by replacing any dash by space
* in the **name** of the action. It is advised to provide a prefix to action name to
* avoid conflict the prefix should be all lowercase and end with a dot `.`
* in the absence of a prefix the behavior of the action is undefined.
*
* All action provided by Jupyter are prefixed with `ipython.`.
*
* One can register extra actions or replace an existing action with another one is possible
* but is considered undefined behavior.
*
**/
export
interface IAction {
handler?: (env: any, evt?: Event) => any;
help?: string;
icon?: string;
help_index?: string;
shortcut?: string;
shortstring?: string;
}
var _actions: { [key: string]: IAction; } = {
'run-select-next': {
icon: 'fa-play',
help : 'run cell, select below',
help_index : 'ba',
handler : function (env) {
env.notebook.execute_cell_and_select_below();
}
},
'execute-in-place':{
help : 'run cell',
help_index : 'bb',
handler : function (env) {
env.notebook.execute_cell();
}
},
'execute-and-insert-after':{
help : 'run cell, insert below',
help_index : 'bc',
handler : function (env) {
env.notebook.execute_cell_and_insert_below();
}
},
'go-to-command-mode': {
help : 'command mode',
help_index : 'aa',
handler : function (env) {
env.notebook.command_mode();
}
},
'split-cell-at-cursor': {
help : 'split cell',
help_index : 'ea',
handler : function (env) {
env.notebook.split_cell();
}
},
'enter-edit-mode' : {
help_index : 'aa',
handler : function (env) {
env.notebook.edit_mode();
}
},
'select-previous-cell' : {
help: 'select cell above',
help_index : 'da',
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== 0 && index !== null) {
env.notebook.select_prev();
env.notebook.focus_cell();
}
}
},
'select-next-cell' : {
help: 'select cell below',
help_index : 'db',
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== (env.notebook.ncells()-1) && index !== null) {
env.notebook.select_next();
env.notebook.focus_cell();
}
}
},
'cut-selected-cell' : {
icon: 'fa-cut',
help_index : 'ee',
handler : function (env) {
var index = env.notebook.get_selected_index();
env.notebook.cut_cell();
env.notebook.select(index);
}
},
'copy-selected-cell' : {
icon: 'fa-copy',
help_index : 'ef',
handler : function (env) {
env.notebook.copy_cell();
}
},
'paste-cell-before' : {
help: 'paste cell above',
help_index : 'eg',
handler : function (env) {
env.notebook.paste_cell_above();
}
},
'paste-cell-after' : {
help: 'paste cell below',
icon: 'fa-paste',
help_index : 'eh',
handler : function (env) {
env.notebook.paste_cell_below();
}
},
'insert-cell-before' : {
help: 'insert cell above',
help_index : 'ec',
handler : function (env) {
env.notebook.insert_cell_above();
env.notebook.select_prev();
env.notebook.focus_cell();
}
},
'insert-cell-after' : {
help: 'insert cell below',
icon : 'fa-plus',
help_index : 'ed',
handler : function (env) {
env.notebook.insert_cell_below();
env.notebook.select_next();
env.notebook.focus_cell();
}
},
'change-selected-cell-to-code-cell' : {
help : 'to code',
help_index : 'ca',
handler : function (env) {
env.notebook.to_code();
}
},
'change-selected-cell-to-markdown-cell' : {
help : 'to markdown',
help_index : 'cb',
handler : function (env) {
env.notebook.to_markdown();
}
},
'change-selected-cell-to-raw-cell' : {
help : 'to raw',
help_index : 'cc',
handler : function (env) {
env.notebook.to_raw();
}
},
'change-selected-cell-to-heading-1' : {
help : 'to heading 1',
help_index : 'cd',
handler : function (env) {
env.notebook.to_heading(undefined, 1);
}
},
'change-selected-cell-to-heading-2' : {
help : 'to heading 2',
help_index : 'ce',
handler : function (env) {
env.notebook.to_heading(undefined, 2);
}
},
'change-selected-cell-to-heading-3' : {
help : 'to heading 3',
help_index : 'cf',
handler : function (env) {
env.notebook.to_heading(undefined, 3);
}
},
'change-selected-cell-to-heading-4' : {
help : 'to heading 4',
help_index : 'cg',
handler : function (env) {
env.notebook.to_heading(undefined, 4);
}
},
'change-selected-cell-to-heading-5' : {
help : 'to heading 5',
help_index : 'ch',
handler : function (env) {
env.notebook.to_heading(undefined, 5);
}
},
'change-selected-cell-to-heading-6' : {
help : 'to heading 6',
help_index : 'ci',
handler : function (env) {
env.notebook.to_heading(undefined, 6);
}
},
'toggle-output-visibility-selected-cell' : {
help : 'toggle output',
help_index : 'gb',
handler : function (env) {
env.notebook.toggle_output();
}
},
'toggle-output-scrolling-selected-cell' : {
help : 'toggle output scrolling',
help_index : 'gc',
handler : function (env) {
env.notebook.toggle_output_scroll();
}
},
'move-selected-cell-down' : {
icon: 'fa-arrow-down',
help_index : 'eb',
handler : function (env) {
env.notebook.move_cell_down();
}
},
'move-selected-cell-up' : {
icon: 'fa-arrow-up',
help_index : 'ea',
handler : function (env) {
env.notebook.move_cell_up();
}
},
'toggle-line-number-selected-cell' : {
help : 'toggle line numbers',
help_index : 'ga',
handler : function (env) {
env.notebook.cell_toggle_line_numbers();
}
},
'show-keyboard-shortcut-help-dialog' : {
help_index : 'ge',
handler : function (env) {
env.quick_help.show_keyboard_shortcuts();
}
},
'delete-cell': {
help: 'delete selected cell',
help_index : 'ej',
handler : function (env) {
env.notebook.delete_cell();
}
},
'interrupt-kernel':{
icon: 'fa-stop',
help_index : 'ha',
handler : function (env) {
env.notebook.kernel.interrupt();
}
},
'restart-kernel':{
icon: 'fa-repeat',
help_index : 'hb',
handler : function (env) {
env.notebook.restart_kernel();
}
},
'undo-last-cell-deletion' : {
help_index : 'ei',
handler : function (env) {
env.notebook.undelete_cell();
}
},
'merge-selected-cell-with-cell-after' : {
help : 'merge cell below',
help_index : 'ek',
handler : function (env) {
env.notebook.merge_cell_below();
}
},
'close-pager' : {
help_index : 'gd',
handler : function (env) {
env.pager.collapse();
}
}
}
/**
* A bunch of `Advance actions` for Jupyter.
* Cf `Simple Action` plus the following properties.
*
* handler: first argument of the handler is the event that triggerd the action
* (typically keypress). The handler is responsible for any modification of the
* event and event propagation.
* Is also responsible for returning false if the event have to be further ignored,
* true, to tell keyboard manager that it ignored the event.
*
* the second parameter of the handler is the environemnt passed to Simple Actions
*
**/
var custom_ignore: { [key: string]: IAction; } = {
'ignore':{
handler : function () {
return true;
}
},
'move-cursor-up-or-previous-cell':{
handler : function (env, event) {
var index = env.notebook.get_selected_index();
var cell = env.notebook.get_cell(index);
var cm = env.notebook.get_selected_cell().code_mirror;
var cur = cm.getCursor();
if (cell && cell.at_top() && index !== 0 && cur.ch === 0) {
if (event) {
event.preventDefault();
}
env.notebook.command_mode();
env.notebook.select_prev();
env.notebook.edit_mode();
cm = env.notebook.get_selected_cell().code_mirror;
cm.setCursor(cm.lastLine(), 0);
}
return false;
}
},
'move-cursor-down-or-next-cell':{
handler : function (env, event) {
var index = env.notebook.get_selected_index();
var cell = env.notebook.get_cell(index);
if (cell.at_bottom() && index !== (env.notebook.ncells()-1)) {
if (event) {
event.preventDefault();
}
env.notebook.command_mode();
env.notebook.select_next();
env.notebook.edit_mode();
var cm = env.notebook.get_selected_cell().code_mirror;
cm.setCursor(0, 0);
}
return false;
}
},
'scroll-down': {
handler: function(env, event) {
if (event) {
event.preventDefault();
}
return env.notebook.scroll_manager.scroll(1);
},
},
'scroll-up': {
handler: function(env, event) {
if (event) {
event.preventDefault();
}
return env.notebook.scroll_manager.scroll(-1);
},
},
'scroll-cell-center': {
help: "Scroll the current cell to the center",
handler: function (env, event) {
if (event) {
event.preventDefault();
}
var cell = env.notebook.get_selected_index();
return env.notebook.scroll_cell_percent(cell, 50, 0);
}
},
'scroll-cell-top': {
help: "Scroll the current cell to the top",
handler: function (env, event) {
if (event) {
event.preventDefault();
}
var cell = env.notebook.get_selected_index();
return env.notebook.scroll_cell_percent(cell, 0, 0);
}
},
'save-notebook':{
help: "Save and Checkpoint",
help_index : 'fb',
icon: 'fa-save',
handler : function (env, event) {
env.notebook.save_checkpoint();
if (event) {
event.preventDefault();
}
return false;
}
}
}
export
class ActionHandler {
constructor(env: any) {
this._env = env || {};
this._setActions();
Object.seal(this);
}
/**
* Register an `action` with an optional name and prefix.
*
* If name and prefix are not given they will be determined automatically.
* If action if just a `function` it will be wrapped in an anonymous action.
*
* Return the full name to access this action .
**/
register(action: IAction, name?: string, prefix?: string): string {
action = this.normalise(action);
if (!name) {
name = 'autogenerated-' + String(action.handler);
}
prefix = prefix || 'auto';
var full_name = prefix + '.' + name;
this._actions.set(full_name, action);
return full_name;
}
/**
* Given an `action` or `function`, return a normalised `action`
* by setting all known attributes and removing unknown attributes.
**/
normalise(data: any): any {
if (typeof(data) === 'function') {
data = {handler:data};
}
if (typeof(data.handler) !== 'function') {
throw('unknown datatype, cannot register');
}
var _data = data;
data = {};
data.handler = _data.handler;
data.help = _data.help || '';
data.icon = _data.icon || '';
data.help_index = _data.help_index || '';
return data;
}
get_name(name_or_data: any): string {
/**
* given an `action` or `name` of a action, return the name attached to this action.
* if given the name of and corresponding actions does not exist in registry, return `null`.
**/
if (typeof(name_or_data) === 'string') {
if (this.exists(name_or_data)) {
return name_or_data;
} else {
return null;
}
} else {
return this.register(name_or_data);
}
}
get(name: string): IAction {
return this._actions.get(name);
}
call(name: string, event: Event, env?: any): IAction {
return this._actions.get(name).handler(env || this._env, event);
}
exists(name: string): boolean {
return (typeof(this._actions.get(name)) !== 'undefined');
}
// Will actually generate/register all the Jupyter actions
private _setActions(): any {
var final_actions = new Map<string, IAction>();
var k: string;
for (k in this._actions) {
if (_actions.hasOwnProperty(k)) {
// Js closure are function level not block level need to wrap in a IIFE
// and append ipython to event name these things do intercept event so are wrapped
// in a function that return false.
var handler = _prepare_handler(final_actions, k, this._actions.get(k));
(function(key: string, hdlr: (env: any, evt?: Event) => any) {
var action = {handler: (env: any, evt: Event) => {
hdlr(env);
if (evt) {
evt.preventDefault();
}
return false;
}};
final_actions.set('ipython.' + key, action);
})(k, handler);
}
}
for (k in custom_ignore) {
// Js closure are function level not block level need to wrap in a IIFE
// same as above, but decide for themselves wether or not they intercept events.
if (custom_ignore.hasOwnProperty(k)) {
var handler = _prepare_handler(final_actions, k, custom_ignore[k]);
(function(key: string, hdlr: (env: any, evt?: Event) => any) {
var action = {handler: (env: any, evt: Event) => {
return hdlr(env, evt);
}};
final_actions.set('ipython.' + key, action)
})(k, handler);
}
}
this._actions = final_actions;
}
private _env: any = null;
private _actions: Map<string, IAction> = null;
}
// private stuff that prepend `.ipython` to actions names
// and uniformize/fill in missing pieces in of an action.
function _prepare_handler(registry: Map<string, IAction>, subkey: string, sourceAction: IAction) {
var action = {
help: sourceAction.help || subkey.replace(/-/g,' '),
help_index: sourceAction.help_index,
icon: sourceAction.icon,
handler: sourceAction.handler
};
registry.set('ipython.' + subkey, action);
return action.handler;
}
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import utils = require('./utils');
import actions = require('./actions');
import IAction = actions.IAction;
import Signal = phosphor.core.Signal;
import emit = phosphor.core.emit;
/**
* Setup global keycodes and inverse keycodes.
*
* See http://unixpapa.com/js/key.html for a complete description. The short of
* it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
* and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
* but have minor differences.
**/
// These apply to Firefox, (Webkit and IE)
// This does work **only** on US keyboard.
var _keycodes: { [key: string]: number; } = {
'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
'1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
'7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
'[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
'\\ |': 220, '\' "': 222,
'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
'insert': 45, 'delete': 46, 'numlock': 144,
};
// These apply to Firefox and Opera
var _mozilla_keycodes: { [key: string]: number; } = {
'; :': 59, '= +': 61, '- _': 173, 'meta': 224
};
// This apply to Webkit and IE
var _ie_keycodes: { [key: string]: number; } = {
'; :': 186, '= +': 187, '- _': 189
};
var browser = utils.browser[0];
var platform = utils.platform;
if (browser === 'Firefox' || browser === 'Opera' || browser === 'Netscape') {
utils.extend(_keycodes, _mozilla_keycodes);
} else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
utils.extend(_keycodes, _ie_keycodes);
}
var keycodes: { [key: string]: number; } = {};
var invKeycodes: { [key: number]: string; } = {};
for (var name in _keycodes) {
var names: string[] = name.split(' ');
if (names.length === 1) {
var n: string = names[0];
keycodes[n] = _keycodes[n];
invKeycodes[_keycodes[n]] = n;
} else {
var primary = names[0];
var secondary = names[1];
keycodes[primary] = _keycodes[name];
keycodes[secondary] = _keycodes[name];
invKeycodes[_keycodes[name]] = primary;
}
}
function normalizeKey(key: string): string {
return invKeycodes[keycodes[key]];
};
/**
* Return a dict containing the normalized shortcut and the number of time
* it should be pressed:
*
* Put a shortcut into normalized form:
* 1. Make lowercase
* 2. Replace cmd by meta
* 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift
* 4. Normalize keys
**/
function normalizeShortcut(shortcut: string): string {
if (platform === 'MacOS') {
shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'cmd-');
} else {
shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'ctrl-');
}
shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
shortcut = shortcut.replace(/,$/, 'comma'); // catch shortcuts using '-' key
if (shortcut.indexOf(',') !== -1) {
var sht: string[] = shortcut.split(',');
sht = sht.map(normalizeShortcut);
return shortcut;
}
shortcut = shortcut.replace(/comma/g, ','); // catch shortcuts using '-' key
var values = shortcut.split("-");
if (values.length === 1) {
return normalizeKey(values[0]);
} else {
var modifiers = values.slice(0,-1);
var key = normalizeKey(values[values.length-1]);
modifiers.sort();
return modifiers.join('-') + '-' + key;
}
};
/**
* Convert a shortcut (shift-r) to a KeyboardEvent object
**/
function shortcutToEvent(shortcut: string, type?: string): KeyboardEvent {
type = type || 'keydown';
shortcut = normalizeShortcut(shortcut);
shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
var values = shortcut.split("-");
var modifiers = values.slice(0,-1);
var key = values[values.length-1];
var opts: any = {which: keycodes[key]};
if (modifiers.indexOf('alt') !== -1) {opts.altKey = true;}
if (modifiers.indexOf('ctrl') !== -1) {opts.ctrlKey = true;}
if (modifiers.indexOf('meta') !== -1) {opts.metaKey = true;}
if (modifiers.indexOf('shift') !== -1) {opts.shiftKey = true;}
var evt = new KeyboardEvent();
evt.key = key;
utils.extend(evt, opts);
return evt;
};
/**
* Return `true` if the event only contains modifiers keys and
* false otherwise.
**/
function onlyModifierEvent(event: KeyboardEvent) : boolean {
var key = invKeycodes[event.which];
return ((event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) &&
(key === 'alt'|| key === 'ctrl'|| key === 'meta'|| key === 'shift'));
};
/**
* Convert an Event object to a normalized shortcut string (shift-r).
**/
function eventToShortcut(event: KeyboardEvent) : string {
var shortcut = '';
var key = invKeycodes[event.which];
if (event.altKey && key !== 'alt') {shortcut += 'alt-';}
if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl-';}
if (event.metaKey && key !== 'meta') {shortcut += 'meta-';}
if (event.shiftKey && key !== 'shift') {shortcut += 'shift-';}
shortcut += key;
return shortcut;
};
/**
* Flatten a tree of shortcut sequences.
* Iterate over all the key/values of available shortcuts.
**/
function flattenShortTree(tree: INode) : { [key: string]: IAction; } {
var dct: { [key: string]: IAction; } = {};
for (var key in tree) {
var value = tree[key];
if (typeof(value) === 'string') {
dct[key] = value;
} else {
var ftree = flattenShortTree((<INode>value));
for (var subkey in ftree) {
dct[key + ',' + subkey] = ftree[subkey];
}
}
}
return dct;
}
/**
* A class to deal with keyboard events and shortcuts.
*/
export
class ShortcutManager {
static shortcutsChanged = new Signal<ShortcutManager, string>();
/**
* Construct a ShortcutManager.
*/
constructor(delay: number, actions: actions.ActionHandler) {
this._delay = delay; // delay in milliseconds
this._actions = actions;
Object.seal(this);
}
/**
* Clear the pending shortcut soon, and cancel previous clearing
* that might be registered.
**/
clearSoon() {
clearTimeout(this._clearTimeout);
this._clearTimeout = setTimeout(() => {this.clearQueue();}, this._delay);
}
/**
* Clear the pending shortcut sequence now.
**/
clearQueue() {
this._queue = [];
clearTimeout(this._clearTimeout);
}
help(): IAction[] {
var help: IAction[] = [];
var ftree = flattenShortTree(this._shortcuts);
for (var shortcut in ftree) {
var action = this._actions.get(shortcut);
var helpString: string = action.help || '== no help ==';
var helpIndex: string = action.help_index;
if (helpString) {
var shortstring: string = (action.shortstring || shortcut);
help.push({
shortcut: shortstring,
help: helpString,
help_index: helpIndex}
);
}
}
help.sort((a: IAction, b: IAction): number => {
if (a.help_index === b.help_index) {
return 0;
}
if (a.help_index === undefined || a.help_index > b.help_index) {
return 1;
}
return -1;
});
return help;
}
clearShortcuts() {
this._shortcuts = null;
}
/**
* Return a node of the shortcut tree which an action name (string) if leaf,
* and an object with `object.subtree===true`
**/
getShortcut(shortcut: string | string[]): any {
var shortcuts: string[];
if (typeof(shortcut) === 'string') {
shortcuts = (<string>shortcut).split(',');
} else {
shortcuts = (<string[]>shortcut);
}
return this._getLeaf(shortcuts, this._shortcuts);
}
setShortcut(shortcut: string | string[], actionName: string): boolean {
var shortcuts: string[];
if (typeof(shortcut) === 'string') {
shortcuts = (<string>shortcut).split(',');
} else {
shortcuts = (<string[]>shortcut);
}
return this._setLeaf(shortcuts, actionName, this._shortcuts);
}
/**
* Add a action to be handled by shortcut manager.
*
* - `shortcut` should be a `Shortcut Sequence` of the for `Ctrl-Alt-C,Meta-X`...
* - `data` could be an `action name`, an `action` or a `function`.
* if a `function` is passed it will be converted to an anonymous `action`.
*
**/
addShortcut(shortcut: string, data: any, suppressHelpUpdate: boolean): void {
var action_name = this._actions.get_name(data);
if (!action_name) {
throw('does not know how to deal with ', data);
}
shortcut = normalizeShortcut(shortcut);
this.setShortcut(shortcut, action_name);
if (!suppressHelpUpdate) {
// update the keyboard shortcuts notebook help
emit(this, ShortcutManager.shortcutsChanged, void 0);
}
}
/**
* Convenience method to call `add_shortcut(key, value)` on several items
*
* data : Dict of the form {key:value, ...}
**/
addShortcuts(shortcuts: Map<string, any>) : void {
for (var shortcut in shortcuts) {
this.addShortcut(shortcut, shortcuts.get(shortcut), true);
}
// update the keyboard shortcuts notebook help
emit(this, ShortcutManager.shortcutsChanged, void 0);
}
/**
* Remove the binding of shortcut `sortcut` with its action.
* throw an error if trying to remove a non-exiting shortcut
**/
removeShortcut(shortcut: string, suppressHelpUpdate: boolean): void {
shortcut = normalizeShortcut(shortcut);
var shortcuts = this._getShortcutList(shortcut)
/*
* The shortcut error should be explicit here, because it will be
* seen by users.
*/
try {
this._removeLeaf(shortcuts, this._shortcuts);
if (!suppressHelpUpdate) {
// update the keyboard shortcuts notebook help
emit(this, ShortcutManager.shortcutsChanged, void 0);
}
} catch (ex) {
throw ('try to remove non-existing shortcut');
}
}
/**
* Call the corresponding shortcut handler for a keyboard event
* @method call_handler
* @return {Boolean} `true|false`, `false` if no handler was found, otherwise the value return by the handler.
* @param event {event}
*
* given an event, call the corresponding shortcut.
* return false is event wan handled, true otherwise
* in any case returning false stop event propagation
**/
callHandler(event: KeyboardEvent): boolean {
this.clearSoon();
if (onlyModifierEvent(event)) {
return true;
}
var shortcut = eventToShortcut(event);
this._queue.push(shortcut);
var actionName = this.getShortcut(this._queue);
if (typeof(actionName) === 'undefined'|| actionName === null) {
this.clearQueue();
return true;
}
if (this._actions.exists(actionName)) {
event.preventDefault();
this.clearQueue();
this._actions.call(actionName, event);
return true;
}
return false;
}
handles(event: KeyboardEvent) : boolean {
var shortcut = eventToShortcut(event);
var actionName = this.getShortcut(this._queue.concat(shortcut));
return (typeof(actionName) !== 'undefined');
}
private _isLeaf(shortcutArray: string[], tree: INode): boolean {
if (shortcutArray.length === 1) {
return(typeof(tree[shortcutArray[0]]) === 'string');
} else {
var subtree = tree[shortcutArray[0]];
return this._isLeaf(shortcutArray.slice(1), (<INode>subtree));
}
}
private _removeLeaf(shortcutArray: string[], tree: INode): void {
if (shortcutArray.length === 1) {
var currentNode = tree[shortcutArray[0]];
if (typeof(currentNode) === 'string') {
delete tree[shortcutArray[0]];
} else {
throw('try to delete non-leaf');
}
} else {
this._removeLeaf(shortcutArray.slice(1), (<INode>tree[shortcutArray[0]]));
if (Object.keys(tree[shortcutArray[0]]).length === 0) {
delete tree[shortcutArray[0]];
}
}
}
private _setLeaf(shortcutArray: string[], actionName: string, tree: INode): boolean {
var currentNode = tree[shortcutArray[0]];
if (shortcutArray.length === 1) {
if (currentNode !== undefined && typeof(currentNode) !== 'string') {
console.warn('[warning], you are overriting a long shortcut with a shorter one');
}
tree[shortcutArray[0]] = actionName;
return true;
} else {
if (typeof(currentNode) === 'string') {
console.warn('you are trying to set a shortcut that will be shadowed' +
'by a more specific one. Aborting for :', actionName, 'the follwing '+
'will take precedence', currentNode);
return false;
} else {
tree[shortcutArray[0]] = tree[shortcutArray[0]] || {};
}
this._setLeaf(shortcutArray.slice(1), actionName,
(<INode>tree[shortcutArray[0]]));
return true;
}
}
/**
* Get a list of shortcuts from a comma separate shortcut name or list.
*/
private _getShortcutList(shortcut: string | string[]): string[] {
var shortcuts: string[];
if (typeof(shortcut) === 'string') {
shortcuts = (<string>shortcut).split(',');
} else {
shortcuts = (<string[]>shortcut);
}
return shortcuts;
}
/**
* Find a leaf/node in a subtree of the keyboard shortcut.
*
**/
private _getLeaf(shortcutArray: string[], tree: INode) : string {
if (shortcutArray.length === 1) {
return (<string>tree[shortcutArray[0]]);
} else if (typeof(tree[shortcutArray[0]]) !== 'string') {
return this._getLeaf(shortcutArray.slice(1),
(<INode>tree[shortcutArray[0]]));
}
return null;
}
private _shortcuts: INode = null;
private _delay = 800;
private _actions: actions.ActionHandler = null;
private _queue : string[] = null;
private _clearTimeout = -1;
}
interface INode {
[index: string]: string | INode;
}
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* Copy the contents of one object to another, recursively.
*
* http://stackoverflow.com/questions/12317003/something-like-jquery-extend-but-standalone
*/
export
function extend(target: any, source: any): any {
target = target || {};
for (var prop in source) {
if (typeof source[prop] === 'object') {
target[prop] = extend(target[prop], source[prop]);
} else {
target[prop] = source[prop];
}
}
return target;
}
/**
* Get a uuid as a string.
*
* http://www.ietf.org/rfc/rfc4122.txt
*/
export
function uuid(): string {
var s: string[] = [];
var hexDigits = "0123456789ABCDEF";
for (var i = 0; i < 32; i++) {
s[i] = hexDigits.charAt(Math.floor(Math.random() * 0x10));
}
s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[16] = hexDigits.charAt((Number(s[16]) & 0x3) | 0x8); // bits 6-7 of the clock_seq_hi_and_reserved to 01
return s.join("");
}
/**
* Join a sequence of url components with '/'.
*/
export
function urlPathJoin(...paths: string[]): string {
var url = '';
for (var i = 0; i < paths.length; i++) {
if (paths[i] === '') {
continue;
}
if (url.length > 0 && url.charAt(url.length - 1) != '/') {
url = url + '/' + paths[i];
} else {
url = url + paths[i];
}
}
return url.replace(/\/\/+/, '/');
}
/**
* Encode just the components of a multi-segment uri,
* leaving '/' separators.
*/
export
function encodeURIComponents(uri: string): string {
return uri.split('/').map(encodeURIComponent).join('/');
}
/**
* Join a sequence of url components with '/',
* encoding each component with encodeURIComponent.
*/
export
function urlJoinEncode(...args: string[]): string {
return encodeURIComponents(urlPathJoin.apply(null, args));
}
/**
* Properly detect the current browser.
* http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
*/
export
var browser: string[] = (() => {
if (typeof navigator === 'undefined') {
// navigator undefined in node
return ['None'];
}
var N: string = navigator.appName;
var ua: string = navigator.userAgent
var tem: RegExpMatchArray;
var M: RegExpMatchArray = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
if (M && (tem = ua.match(/version\/([\.\d]+)/i)) !== null) M[2] = tem[1];
M = M ? [M[1], M[2]] : [N, navigator.appVersion, '-?'];
return M;
})();
/**
* Properly detect the current platform.
* http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
*/
export
var platform: string = (() => {
if (typeof navigator === 'undefined') {
// navigator undefined in node
return 'None';
}
var OSName="None";
if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
return OSName;
})();
/**
* Return a serialized object string suitable for a query.
*
* http://stackoverflow.com/a/30707423
*/
export
function jsonToQueryString(json: any): string {
return '?' +
Object.keys(json).map((key: string): any => {
return encodeURIComponent(key) + '=' +
encodeURIComponent(json[key]);
}).join('&');
}
/**
* Input settings for an AJAX request.
*/
export
interface IAjaxSetttings {
method: string;
dataType: string;
contentType?: string;
data?: any;
}
/**
* Success handler for AJAX request.
*/
export
interface IAjaxSuccess {
data: any;
statusText: string;
xhr: XMLHttpRequest;
}
/**
* Error handler for AJAX request.
*/
export
interface IAjaxError {
xhr: XMLHttpRequest;
statusText: string;
error: ErrorEvent;
}
/**
* Asynchronous XMLHTTPRequest handler.
*
* http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promisifying-xmlhttprequest
*/
export
function ajaxRequest(url: string, settings: IAjaxSetttings): Promise<any> {
return new Promise((resolve, reject) => {
var req = new XMLHttpRequest();
req.open(settings.method, url);
if (settings.contentType) {
req.overrideMimeType(settings.contentType);
}
req.onload = () => {
var response = req.response;
if (settings.dataType === 'json') {
response = JSON.parse(req.response);
}
resolve({data: response, statusText: req.statusText, xhr: req});
}
req.onerror = (err: ErrorEvent) => {
reject({xhr:req, statusText: req.statusText, erorr:err});
}
if (settings.data) {
req.send(settings.data);
} else {
req.send();
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment