Last active
August 26, 2015 12:47
-
-
Save blink1073/a0a9198de7bff53d941d to your computer and use it in GitHub Desktop.
Keyboard Manager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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