phosphor-keymap
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -1,128 +0,2 @@ | ||
import { IDisposable } from 'phosphor-disposable'; | ||
/** | ||
* An object which represents a key binding. | ||
*/ | ||
export interface IKeyBinding { | ||
/** | ||
* The key sequence for the key binding. | ||
* | ||
* Each keystroke must adhere to the following format: | ||
* | ||
* `[<modifier-1>+[<modifier-2>+[<modifier-n>+]]]<key>` | ||
* | ||
* - Supported modifiers are `'ctrl'`, `'alt'`, `'shift'`, `'cmd'`. | ||
* - The `'cmd'` modifier only works on OSX (browser limitation). | ||
* - The modifiers may appear in any order. | ||
* - The modifiers cannot appear in duplicate. | ||
* - The primary key must be a valid key character. | ||
* - The keystroke is case insensitive. | ||
* - Mutliple keystrokes are separated by whitespace. | ||
* | ||
* #### Example | ||
* **Valid Key Sequences** | ||
* ``` typescript | ||
* 'a' | ||
* 'b' | ||
* 'd d' | ||
* 'ctrl+-' | ||
* 'ctrl+=' | ||
* 'ctrl+alt+5' | ||
* 'shift+f11' | ||
* 'ctrl+k ctrl+t' | ||
* 'alt+cmd+y ctrl+4 alt+]' | ||
* ``` | ||
* | ||
* **Invalid Key Sequences** | ||
* ```typescript | ||
* '%' | ||
* '$' | ||
* 'ctrl-a' | ||
* '+ctrl+a' | ||
* 'shift++o' | ||
* 'ctrl+a shift' | ||
* ``` | ||
*/ | ||
sequence: string; | ||
/** | ||
* The handler function to invoke when the key sequence is matched. | ||
* | ||
* The handler should return `true` to prevent the default and stop | ||
* propagation of the key event. It should return `false` to allow | ||
* the continued processing of the event. | ||
*/ | ||
handler: () => boolean; | ||
} | ||
/** | ||
* A class which manages a collection of key bindings. | ||
*/ | ||
export declare class KeymapManager { | ||
/** | ||
* Construct a new key map. | ||
*/ | ||
contstructor(): void; | ||
/** | ||
* Add key bindings to the key map. | ||
* | ||
* @param selector - The CSS selector for the key bindings. | ||
* | ||
* @param bindings - The key bindings to add to the key map. | ||
* | ||
* @returns A disposable which will remove the key bindings. | ||
* | ||
* #### Notes | ||
* If the selector is an invalid CSS selector, a warning will | ||
* be logged to the console and `undefined` will be returned. | ||
* | ||
* If the key sequence for a binding is invalid, or if a binding | ||
* has a null handler, a warning will be logged to the console | ||
* and that binding will be ignored. | ||
*/ | ||
add(selector: string, bindings: IKeyBinding[]): IDisposable; | ||
/** | ||
* Process a `'keydown'` event and invoke the matching key bindings. | ||
* | ||
* @param event - The event object for a `'keydown'` event. | ||
* | ||
* #### Notes | ||
* This should be called by user code in response to a `'keydown'` | ||
* event. The keymap **does not** install its own event listeners, | ||
* which allows user code full control over the nodes for which | ||
* the keymap processes events. | ||
*/ | ||
processKeydownEvent(event: KeyboardEvent): void; | ||
/** | ||
* Remove an array of ex key bindings from the key map. | ||
*/ | ||
private _removeBindings(arr); | ||
/** | ||
* Start or restart the pending timer for the key map. | ||
*/ | ||
private _startTimer(); | ||
/** | ||
* Clear the pending timer for the key map. | ||
*/ | ||
private _clearTimer(); | ||
/** | ||
* Clear the pending state for the keymap. | ||
*/ | ||
private _clearPendingState(); | ||
/** | ||
* Set the pending exact match data. | ||
*/ | ||
private _setExactData(exact, event); | ||
/** | ||
* Handle the partial timer timeout. | ||
* | ||
* This will reset the pending state and dispatch the exact matches. | ||
*/ | ||
private _onPendingTimeout(); | ||
private _timer; | ||
private _partialTimeout; | ||
private _keystrokes; | ||
private _bindings; | ||
private _exactData; | ||
} | ||
/** | ||
* Test whether an element matches a CSS selector. | ||
*/ | ||
export declare function matchesSelector(elem: Element, selector: string): boolean; | ||
export * from './keycodes'; | ||
export * from './manager'; |
336
lib/index.js
@@ -9,335 +9,7 @@ /*----------------------------------------------------------------------------- | ||
'use strict'; | ||
var clear_cut_1 = require('clear-cut'); | ||
var phosphor_disposable_1 = require('phosphor-disposable'); | ||
var keycodes_1 = require('./keycodes'); | ||
/** | ||
* A class which manages a collection of key bindings. | ||
*/ | ||
var KeymapManager = (function () { | ||
function KeymapManager() { | ||
this._timer = 0; | ||
this._partialTimeout = 1000; | ||
this._keystrokes = []; | ||
this._bindings = []; | ||
this._exactData = null; | ||
} | ||
/** | ||
* Construct a new key map. | ||
*/ | ||
KeymapManager.prototype.contstructor = function () { }; | ||
/** | ||
* Add key bindings to the key map. | ||
* | ||
* @param selector - The CSS selector for the key bindings. | ||
* | ||
* @param bindings - The key bindings to add to the key map. | ||
* | ||
* @returns A disposable which will remove the key bindings. | ||
* | ||
* #### Notes | ||
* If the selector is an invalid CSS selector, a warning will | ||
* be logged to the console and `undefined` will be returned. | ||
* | ||
* If the key sequence for a binding is invalid, or if a binding | ||
* has a null handler, a warning will be logged to the console | ||
* and that binding will be ignored. | ||
*/ | ||
KeymapManager.prototype.add = function (selector, bindings) { | ||
var _this = this; | ||
// Log a warning and bail if the selector is invalid. | ||
if (!clear_cut_1.isSelectorValid(selector)) { | ||
console.warn("Invalid key binding selector: " + selector); | ||
return void 0; | ||
} | ||
// The newly created ex bindings for the valid key bindings. | ||
var newBindings = []; | ||
// Iterate over the bindings and covert them into ex bindings. | ||
for (var i = 0, n = bindings.length; i < n; ++i) { | ||
var binding = bindings[i]; | ||
// If the binding does not have a handler, warn and continue. | ||
if (!binding.handler) { | ||
console.warn("null handler for key binding: " + binding.sequence); | ||
continue; | ||
} | ||
// Trim the key sequence and split into individual keystrokes. | ||
var keystrokes = binding.sequence.trim().split(/\s+/); | ||
// Normalize each keystroke and re-join into a canoncial form. | ||
// If any of the keystrokes are invalid, warn and continue. | ||
try { | ||
var sequence = keystrokes.map(keycodes_1.normalizeKeystroke).join(' '); | ||
} | ||
catch (e) { | ||
console.warn("invalid key binding sequence: " + binding.sequence); | ||
continue; | ||
} | ||
// Create a new extended binding and add it to the arrays. | ||
var exb = new ExBinding(selector, sequence, binding.handler); | ||
this._bindings.push(exb); | ||
newBindings.push(exb); | ||
} | ||
// Return a disposable which will remove the new bindings. | ||
return new phosphor_disposable_1.DisposableDelegate(function () { return _this._removeBindings(newBindings); }); | ||
}; | ||
/** | ||
* Process a `'keydown'` event and invoke the matching key bindings. | ||
* | ||
* @param event - The event object for a `'keydown'` event. | ||
* | ||
* #### Notes | ||
* This should be called by user code in response to a `'keydown'` | ||
* event. The keymap **does not** install its own event listeners, | ||
* which allows user code full control over the nodes for which | ||
* the keymap processes events. | ||
*/ | ||
KeymapManager.prototype.processKeydownEvent = function (event) { | ||
// If the actual pressed key is a modifier key, prevent the default | ||
// and return. No bindings can be matched for *just* modifier keys. | ||
if (keycodes_1.isModifierKeyCode(event.keyCode)) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
// Get the normalized keystroke and store it as a pending. | ||
this._keystrokes.push(keycodes_1.keystrokeForKeydownEvent(event)); | ||
// Convert the pending keystrokes to a sequence. | ||
var sequence = this._keystrokes.join(' '); | ||
// Find the exact and partial matches for the key sequence. | ||
var matches = findSequenceMatches(this._bindings, sequence); | ||
// If there are no exact match and not partial matches, clear | ||
// all pending state so the next key press starts from default. | ||
if (matches.exact.length === 0 && matches.partial.length === 0) { | ||
this._clearPendingState(); | ||
return; | ||
} | ||
// If there are exact matches but no partial matches, the exact | ||
// matches can be dispatched immediately. The pending state is | ||
// reset so the next key press starts from default. | ||
if (matches.partial.length === 0) { | ||
this._clearPendingState(); | ||
dispatchBindings(matches.exact, event); | ||
return; | ||
} | ||
// At this point, there are partial matches. | ||
// If there are exact matches and partial matches, the exact | ||
// matches are stored so they can be dispatched if the timer | ||
// expires before a more specific match is found. | ||
if (matches.exact.length > 0) { | ||
this._setExactData(matches.exact, event); | ||
} | ||
// Restart the timer for equal intervals between keystrokes. | ||
// | ||
// TODO - we may want to replay prevented defaults if match fails. | ||
// - we may also want to stop propagation until match fails. | ||
event.preventDefault(); | ||
this._startTimer(); | ||
}; | ||
/** | ||
* Remove an array of ex key bindings from the key map. | ||
*/ | ||
KeymapManager.prototype._removeBindings = function (arr) { | ||
this._bindings = this._bindings.filter(function (b) { return arr.indexOf(b) === -1; }); | ||
}; | ||
/** | ||
* Start or restart the pending timer for the key map. | ||
*/ | ||
KeymapManager.prototype._startTimer = function () { | ||
var _this = this; | ||
this._clearTimer(); | ||
this._timer = setTimeout(function () { | ||
_this._onPendingTimeout(); | ||
}, this._partialTimeout); | ||
}; | ||
/** | ||
* Clear the pending timer for the key map. | ||
*/ | ||
KeymapManager.prototype._clearTimer = function () { | ||
if (this._timer !== 0) { | ||
clearTimeout(this._timer); | ||
this._timer = 0; | ||
} | ||
}; | ||
/** | ||
* Clear the pending state for the keymap. | ||
*/ | ||
KeymapManager.prototype._clearPendingState = function () { | ||
this._clearTimer(); | ||
this._exactData = null; | ||
this._keystrokes.length = 0; | ||
}; | ||
/** | ||
* Set the pending exact match data. | ||
*/ | ||
KeymapManager.prototype._setExactData = function (exact, event) { | ||
if (!this._exactData) { | ||
this._exactData = { exact: exact, event: event }; | ||
} | ||
else { | ||
this._exactData.exact = exact; | ||
this._exactData.event = event; | ||
} | ||
}; | ||
/** | ||
* Handle the partial timer timeout. | ||
* | ||
* This will reset the pending state and dispatch the exact matches. | ||
*/ | ||
KeymapManager.prototype._onPendingTimeout = function () { | ||
this._timer = 0; | ||
var d = this._exactData; | ||
this._clearPendingState(); | ||
if (d) | ||
dispatchBindings(d.exact, d.event); | ||
}; | ||
return KeymapManager; | ||
})(); | ||
exports.KeymapManager = KeymapManager; | ||
/** | ||
* An extended key bind object used by a keymap manager. | ||
*/ | ||
var ExBinding = (function () { | ||
/** | ||
* Construct a new extended key binding. | ||
* | ||
* @param selector - The valid CSS selector for the binding. | ||
* | ||
* @param sequence - The normalized key binding sequence. | ||
* | ||
* @param handler - The handler function for the binding. | ||
*/ | ||
function ExBinding(selector, sequence, handler) { | ||
this._id = ExBinding.idTick++; | ||
this._selector = selector; | ||
this._sequence = sequence; | ||
this._handler = handler; | ||
this._specificity = clear_cut_1.calculateSpecificity(selector); | ||
} | ||
/** | ||
* A comparison function for extended bindings. | ||
* | ||
* This can be used to sort an array of bindings according to | ||
* highest CSS specificity. Ties are broken according to the | ||
* binding id, with newer bindings appearing first. | ||
*/ | ||
ExBinding.compare = function (exA, exB) { | ||
if (exA._specificity === exB._specificity) { | ||
return exB._id - exA._id; | ||
} | ||
return exB._specificity - exA._specificity; | ||
}; | ||
/** | ||
* Create a public key binding object for this extended binding. | ||
*/ | ||
ExBinding.prototype.toBinding = function () { | ||
return { sequence: this._sequence, handler: this._handler }; | ||
}; | ||
/** | ||
* Test whether the binding is an exact match for a key sequence. | ||
*/ | ||
ExBinding.prototype.isExactMatch = function (sequence) { | ||
return this._sequence === sequence; | ||
}; | ||
/** | ||
* Test whether the binding is a partial match for a key sequence. | ||
*/ | ||
ExBinding.prototype.isPartialMatch = function (sequence) { | ||
return this._sequence.indexOf(sequence) === 0; | ||
}; | ||
/** | ||
* Test whether the binding selector matches an element. | ||
*/ | ||
ExBinding.prototype.isSelectorMatch = function (target) { | ||
return matchesSelector(target, this._selector); | ||
}; | ||
/** | ||
* Invoke the handler for the binding and return its result. | ||
*/ | ||
ExBinding.prototype.invoke = function () { | ||
return this._handler.call(void 0); | ||
}; | ||
/** | ||
* A monotonically increasing binding identifier. | ||
* | ||
* The binding id is used to break sorting ties. | ||
*/ | ||
ExBinding.idTick = 0; | ||
return ExBinding; | ||
})(); | ||
/** | ||
* Filter the bindings for those which match the given sequence. | ||
* | ||
* The result contains both exact matches and partial matches. | ||
*/ | ||
function findSequenceMatches(bindings, sequence) { | ||
var exact = []; | ||
var partial = []; | ||
var partialSequence = sequence + ' '; | ||
for (var i = 0, n = bindings.length; i < n; ++i) { | ||
var exb = bindings[i]; | ||
if (exb.isExactMatch(sequence)) { | ||
exact.push(exb); | ||
} | ||
else if (exb.isPartialMatch(partialSequence)) { | ||
partial.push(exb); | ||
} | ||
} | ||
return { exact: exact, partial: partial }; | ||
function __export(m) { | ||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; | ||
} | ||
/** | ||
* Filter the bindings for those with a matching selector. | ||
*/ | ||
function findSelectorMatches(bindings, target) { | ||
var matches = bindings.filter(function (exb) { return exb.isSelectorMatch(target); }); | ||
return matches.sort(ExBinding.compare); | ||
} | ||
/** | ||
* Dispatch the key bindings for the given keyboard event. | ||
* | ||
* As the dispatcher walks up the DOM, the bindings will be filtered | ||
* for matching selectors, and invoked in specificity order. If the | ||
* handler for a binding returns `true`, dispatch will terminate and | ||
* the event propagation will be stopped. | ||
*/ | ||
function dispatchBindings(bindings, event) { | ||
var target = event.target; | ||
var current = event.currentTarget; | ||
while (target) { | ||
var matches = findSelectorMatches(bindings, target); | ||
for (var i = 0, n = matches.length; i < n; ++i) { | ||
if (matches[i].invoke()) { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
return; | ||
} | ||
} | ||
if (target === current) { | ||
return; | ||
} | ||
target = target.parentElement; | ||
} | ||
} | ||
/** | ||
* Test whether an element matches a CSS selector. | ||
*/ | ||
function matchesSelector(elem, selector) { | ||
return protoMatchFunc.call(elem, selector); | ||
} | ||
exports.matchesSelector = matchesSelector; | ||
/** | ||
* A cross-browser CSS selector matching prototype function. | ||
* | ||
* The function must be called with the element as `this`. | ||
*/ | ||
var protoMatchFunc = (function () { | ||
var proto = Element.prototype; | ||
return (proto.matches || | ||
proto.matchesSelector || | ||
proto.mozMatchesSelector || | ||
proto.msMatchesSelector || | ||
proto.oMatchesSelector || | ||
proto.webkitMatchesSelector || | ||
(function (selector) { | ||
var elem = this; | ||
var matches = elem.ownerDocument.querySelectorAll(selector); | ||
return Array.prototype.indexOf.call(matches, elem) !== -1; | ||
})); | ||
})(); | ||
__export(require('./keycodes')); | ||
__export(require('./manager')); | ||
//# sourceMappingURL=index.js.map |
@@ -29,2 +29,4 @@ /** | ||
* | ||
* @throws An error if the keystroke has an invalid format. | ||
* | ||
* #### Notes | ||
@@ -31,0 +33,0 @@ * The keystroke must adhere to the following format: |
@@ -59,2 +59,4 @@ /*----------------------------------------------------------------------------- | ||
* | ||
* @throws An error if the keystroke has an invalid format. | ||
* | ||
* #### Notes | ||
@@ -85,14 +87,6 @@ * The keystroke must adhere to the following format: | ||
if (token === '+') { | ||
if (key) { | ||
if (sep || key || !(alt || cmd || ctrl || shift)) { | ||
throwKeystrokeError(keystroke); | ||
} | ||
else if ((i === n - 1) && (n === 1 || sep)) { | ||
key = token; | ||
} | ||
else if (!sep && (alt || cmd || ctrl || shift)) { | ||
sep = true; | ||
} | ||
else { | ||
throwKeystrokeError(keystroke); | ||
} | ||
sep = true; | ||
} | ||
@@ -99,0 +93,0 @@ else if (token === 'alt') { |
{ | ||
"name": "phosphor-keymap", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "A module for keyboard shortcut mapping.", | ||
@@ -9,3 +9,3 @@ "main": "lib/index.js", | ||
"clear-cut": "^2.0.1", | ||
"phosphor-disposable": "^1.0.3" | ||
"phosphor-disposable": "^1.0.4" | ||
}, | ||
@@ -28,4 +28,4 @@ "devDependencies": { | ||
"rimraf": "^2.4.2", | ||
"typedoc": "git://github.com/phosphorjs/typedoc.git", | ||
"typescript": "1.6.0-beta" | ||
"typedoc": "^0.3.11", | ||
"typescript": "^1.6.2" | ||
}, | ||
@@ -55,3 +55,5 @@ "scripts": { | ||
"lib/keycodes.js", | ||
"lib/keycodes.d.ts" | ||
"lib/keycodes.d.ts", | ||
"lib/manager.js", | ||
"lib/manager.d.ts" | ||
], | ||
@@ -58,0 +60,0 @@ "keywords": [ |
@@ -0,0 +0,0 @@ phosphor-keymap |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
31036
9
802
1
Updatedphosphor-disposable@^1.0.4