phosphor-keymap
Advanced tools
Comparing version 0.7.1 to 0.8.0
@@ -48,6 +48,4 @@ import { IDisposable } from 'phosphor-disposable'; | ||
* The handler to execute when the key binding is matched. | ||
* | ||
* Returns `true` if the action is handled, `false` otherwise. | ||
*/ | ||
handler: (args: any) => boolean; | ||
handler: (args: any) => void; | ||
/** | ||
@@ -110,3 +108,3 @@ * The arguments for the handler, if necessary. | ||
*/ | ||
private _removeBindings(array); | ||
private _removeBindings(exbArray); | ||
/** | ||
@@ -121,2 +119,6 @@ * Start or restart the pending timer for the key map. | ||
/** | ||
* Replay the events which were suppressed. | ||
*/ | ||
private _replayEvents(); | ||
/** | ||
* Clear the pending state for the keymap. | ||
@@ -130,6 +132,8 @@ */ | ||
private _timer; | ||
private _replaying; | ||
private _layout; | ||
private _sequence; | ||
private _exact; | ||
private _bindings; | ||
private _exactData; | ||
private _events; | ||
} |
@@ -25,5 +25,7 @@ /*----------------------------------------------------------------------------- | ||
this._timer = 0; | ||
this._replaying = false; | ||
this._sequence = []; | ||
this._exact = null; | ||
this._bindings = []; | ||
this._exactData = null; | ||
this._events = []; | ||
this._layout = layout; | ||
@@ -66,6 +68,6 @@ } | ||
var exb = createExBinding(kb, this._layout); | ||
if (exb) | ||
if (exb !== null) | ||
exbArray.push(exb); | ||
} | ||
this._bindings = this._bindings.concat(exbArray); | ||
Array.prototype.push.apply(this._bindings, exbArray); | ||
return new phosphor_disposable_1.DisposableDelegate(function () { return _this._removeBindings(exbArray); }); | ||
@@ -87,2 +89,6 @@ }; | ||
KeymapManager.prototype.processKeydownEvent = function (event) { | ||
// Bail immediately if playing back keystrokes. | ||
if (this._replaying) { | ||
return; | ||
} | ||
// Get the canonical keystroke for the event. An empty string | ||
@@ -97,30 +103,32 @@ // indicates a keystroke which cannot be a valid key shortcut. | ||
// Find the exact and partial matches for the key sequence. | ||
var matches = findSequenceMatches(this._bindings, this._sequence); | ||
// If there are no exact matches and no partial matches, clear | ||
// all pending state so the next key press starts from default. | ||
if (matches.exact.length === 0 && matches.partial.length === 0) { | ||
var _a = findMatch(this._bindings, this._sequence, event), exact = _a.exact, partial = _a.partial; | ||
// If there is no exact or partial match, replay any suppressed | ||
// events and clear the pending state so that the next key press | ||
// starts from a fresh default state. | ||
if (!exact && !partial) { | ||
this._replayEvents(); | ||
this._clearPendingState(); | ||
return; | ||
} | ||
// If there are exact matches but no partial matches, the exact | ||
// matches can be dispatched immediately. The pending state is | ||
// cleared so the next key press starts from default. | ||
if (matches.partial.length === 0) { | ||
// Stop propagation of the event. If there is only a partial match, | ||
// the event will be replayed if a final match is never triggered. | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
// If there is an exact match but no partial, the exact match can | ||
// be dispatched immediately. The pending state is cleared so the | ||
// next key press starts from a fresh default state. | ||
if (!partial) { | ||
safeInvoke(exact); | ||
this._clearPendingState(); | ||
dispatchBindings(matches.exact, event); | ||
return; | ||
} | ||
// If there are both exact matches and partial matches, the exact | ||
// matches are stored so that they can be dispatched if the timer | ||
// expires before a more specific match is found. | ||
if (matches.exact.length > 0) { | ||
this._exactData = { exact: matches.exact, event: event }; | ||
} | ||
// (Re)start the timer to trigger the most recent exact match in | ||
// the event the pending partial match fails to result in a final | ||
// unambiguous exact match. | ||
// | ||
// TODO - we may want to replay events if an exact match fails. | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
// If there is both an exact match and a partial, the exact match | ||
// is stored for future dispatch in case the timer expires before | ||
// a more specific match is triggered. | ||
if (exact) | ||
this._exact = exact; | ||
// Store the event for possible playback in the future. | ||
this._events.push(event); | ||
// (Re)start the timer to dispatch the most recent exact match | ||
// in case the partial match fails to result in an exact match. | ||
this._startTimer(); | ||
@@ -131,4 +139,14 @@ }; | ||
*/ | ||
KeymapManager.prototype._removeBindings = function (array) { | ||
this._bindings = this._bindings.filter(function (exb) { return array.indexOf(exb) === -1; }); | ||
KeymapManager.prototype._removeBindings = function (exbArray) { | ||
var count = 0; | ||
for (var i = 0, n = this._bindings.length; i < n; ++i) { | ||
var exb = this._bindings[i]; | ||
if (exbArray.indexOf(exb) !== -1) { | ||
count++; | ||
} | ||
else { | ||
this._bindings[i - count] = exb; | ||
} | ||
} | ||
this._bindings.length -= count; | ||
}; | ||
@@ -155,2 +173,17 @@ /** | ||
/** | ||
* Replay the events which were suppressed. | ||
*/ | ||
KeymapManager.prototype._replayEvents = function () { | ||
if (this._events.length === 0) { | ||
return; | ||
} | ||
this._replaying = true; | ||
for (var _i = 0, _a = this._events; _i < _a.length; _i++) { | ||
var evt = _a[_i]; | ||
var clone = cloneKeyboardEvent(evt); | ||
evt.target.dispatchEvent(clone); | ||
} | ||
this._replaying = false; | ||
}; | ||
/** | ||
* Clear the pending state for the keymap. | ||
@@ -160,3 +193,4 @@ */ | ||
this._clearTimer(); | ||
this._exactData = null; | ||
this._exact = null; | ||
this._events.length = 0; | ||
this._sequence.length = 0; | ||
@@ -168,8 +202,10 @@ }; | ||
KeymapManager.prototype._onPendingTimeout = function () { | ||
var data = this._exactData; | ||
this._timer = 0; | ||
this._exactData = null; | ||
this._sequence.length = 0; | ||
if (data) | ||
dispatchBindings(data.exact, data.event); | ||
if (this._exact) { | ||
safeInvoke(this._exact); | ||
} | ||
else { | ||
this._replayEvents(); | ||
} | ||
this._clearPendingState(); | ||
}; | ||
@@ -210,3 +246,3 @@ return KeymapManager; | ||
/** | ||
* Test whether an ex-binding sequence matches a key sequence. | ||
* Test whether a binding sequence matches a key sequence. | ||
* | ||
@@ -230,56 +266,78 @@ * Returns a `SequenceMatch` value indicating the type of match. | ||
/** | ||
* Find the extended bindings which match a key sequence. | ||
* Find the distance from the target node to the first matching node. | ||
* | ||
* Returns a match result which contains the exact and partial matches. | ||
* This traverses the event path from `target` to `currentTarget` and | ||
* computes the distance from `target` to the first node which matches | ||
* the CSS selector. If no match is found, `-1` is returned. | ||
*/ | ||
function findSequenceMatches(bindings, sequence) { | ||
var exact = []; | ||
var partial = []; | ||
for (var _i = 0; _i < bindings.length; _i++) { | ||
var exb = bindings[_i]; | ||
var match = matchSequence(exb.sequence, sequence); | ||
if (match === 1 /* Exact */) { | ||
exact.push(exb); | ||
function targetDistance(selector, event) { | ||
var distance = 0; | ||
var target = event.target; | ||
var current = event.currentTarget; | ||
for (; target !== null; target = target.parentElement, ++distance) { | ||
if (matchesSelector(target, selector)) { | ||
return distance; | ||
} | ||
else if (match === 2 /* Partial */) { | ||
partial.push(exb); | ||
if (target === current) { | ||
return -1; | ||
} | ||
} | ||
return { exact: exact, partial: partial }; | ||
return -1; | ||
} | ||
/** | ||
* Find the bindings which match the given target element. | ||
* Find the bindings which match a key sequence. | ||
* | ||
* The matched bindings are ordered from highest to lowest specificity. | ||
* This returns a match result which contains the best exact matching | ||
* binding, and a flag which indicates if there are partial matches. | ||
*/ | ||
function findOrderedMatches(bindings, target) { | ||
return bindings.filter(function (exb) { | ||
return matchesSelector(target, exb.selector); | ||
}).sort(function (a, b) { | ||
return b.specificity - a.specificity; | ||
}); | ||
function findMatch(bindings, sequence, event) { | ||
// Whether a partial match has been found. | ||
var partial = false; | ||
// The current best exact match. | ||
var exact = null; | ||
// The match distance for the exact match. | ||
var distance = Infinity; | ||
// Iterate the bindings and search for the best match. | ||
for (var i = 0, n = bindings.length; i < n; ++i) { | ||
// Lookup the current binding. | ||
var exb = bindings[i]; | ||
// Check whether the binding sequence is a match. | ||
var match = matchSequence(exb.sequence, sequence); | ||
// If there is no match, the binding is ignored. | ||
if (match === 0 /* None */) { | ||
continue; | ||
} | ||
// If it is a partial match and no other partial match has been | ||
// found, ensure the selector matches and mark the partial flag. | ||
if (match === 2 /* Partial */) { | ||
if (!partial && targetDistance(exb.selector, event) !== -1) { | ||
partial = true; | ||
} | ||
continue; | ||
} | ||
// Otherwise, it's an exact match. Update the best match if the | ||
// binding is a stronger match than the current best exact match. | ||
var td = targetDistance(exb.selector, event); | ||
if (td !== -1 && td <= distance) { | ||
if (exact === null || exb.specificity > exact.specificity) { | ||
exact = exb; | ||
distance = td; | ||
} | ||
} | ||
} | ||
// Return the match result. | ||
return { exact: exact, partial: partial }; | ||
} | ||
/** | ||
* Dispatch the key bindings for the given keyboard event. | ||
* Safely invoke the handler for the key binding. | ||
* | ||
* As the dispatcher walks up the DOM, the bindings will be filtered | ||
* for the best matching keybinding. If a match is found, the handler | ||
* is invoked and event propagation is stopped. | ||
* Exceptions in the handler will be caught and logged. | ||
*/ | ||
function dispatchBindings(bindings, event) { | ||
var target = event.target; | ||
while (target) { | ||
for (var _i = 0, _a = findOrderedMatches(bindings, target); _i < _a.length; _i++) { | ||
var _b = _a[_i], handler = _b.handler, args = _b.args; | ||
if (handler(args)) { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
return; | ||
} | ||
} | ||
if (target === event.currentTarget) { | ||
return; | ||
} | ||
target = target.parentElement; | ||
function safeInvoke(binding) { | ||
try { | ||
binding.handler.call(void 0, binding.args); | ||
} | ||
catch (err) { | ||
console.error(err); | ||
} | ||
} | ||
@@ -311,1 +369,23 @@ /** | ||
} | ||
/** | ||
* Clone a keyboard event. | ||
* | ||
* #### Notes | ||
* A custom event is required because Chrome nulls out the `keyCode` | ||
* field in user-generated `KeyboardEvent` types. | ||
*/ | ||
function cloneKeyboardEvent(event) { | ||
var clone = document.createEvent('Event'); | ||
var bubbles = event.bubbles || true; | ||
var cancelable = event.cancelable || true; | ||
clone.initEvent(event.type || 'keydown', bubbles, cancelable); | ||
clone.key = event.key || ''; | ||
clone.keyCode = event.keyCode || 0; | ||
clone.which = event.keyCode || 0; | ||
clone.ctrlKey = event.ctrlKey || false; | ||
clone.altKey = event.altKey || false; | ||
clone.shiftKey = event.shiftKey || false; | ||
clone.metaKey = event.metaKey || false; | ||
clone.view = event.view || window; | ||
return clone; | ||
} |
{ | ||
"name": "phosphor-keymap", | ||
"version": "0.7.1", | ||
"version": "0.8.0", | ||
"description": "A module for keyboard shortcut mapping.", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
38775
1042