@difizen/mana-core
Version:
415 lines (373 loc) • 14.9 kB
JavaScript
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.FocusTracker = void 0;
var _manaCommon = require("@difizen/mana-common");
var _manaSyringe = require("@difizen/mana-syringe");
var _dec, _class;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/**
* A class which tracks focus among a set of views.
*
* This class is useful when code needs to keep track of the most
* recently focused view(s) among a set of related views.
*/
var FocusTracker = exports.FocusTracker = (_dec = (0, _manaSyringe.singleton)(), _dec(_class = /*#__PURE__*/function () {
function FocusTracker() {
_classCallCheck(this, FocusTracker);
this.counter = 0;
this._views = [];
this._activeView = null;
this._currentView = null;
this.numbers = new Map();
this.nodes = new Map();
this.activeChangedEmitter = new _manaCommon.Emitter();
this.currentChangedEmitter = new _manaCommon.Emitter();
}
_createClass(FocusTracker, [{
key: "currentChanged",
get:
/**
* A signal emitted when the current view has changed.
*/
function get() {
return this.currentChangedEmitter.event;
}
/**
* A signal emitted when the active view has changed.
*/
}, {
key: "activeChanged",
get: function get() {
return this.activeChangedEmitter.event;
}
/**
* A flag indicating whether the tracker is disposed.
*/
}, {
key: "isDisposed",
get: function get() {
return this.counter < 0;
}
/**
* The current view in the tracker.
*
* #### Notes
* The current view is the view among the tracked views which
* has the *descendant node* which has most recently been focused.
*
* The current view will not be updated if the node loses focus. It
* will only be updated when a different tracked view gains focus.
*
* If the current view is removed from the tracker, the previous
* current view will be restored.
*
* This behavior is intended to follow a user's conceptual model of
* a semantically "current" view, where the "last thing of type X"
* to be interacted with is the "current instance of X", regardless
* of whether that instance still has focus.
*/
}, {
key: "currentView",
get: function get() {
return this._currentView;
}
/**
* The active view in the tracker.
*
* #### Notes
* The active view is the view among the tracked views which
* has the *descendant node* which is currently focused.
*/
}, {
key: "activeView",
get: function get() {
return this._activeView;
}
/**
* A read only array of the views being tracked.
*/
}, {
key: "views",
get: function get() {
return this._views;
}
/**
* Dispose of the resources held by the tracker.
*/
}, {
key: "dispose",
value: function dispose() {
var _this = this;
// Do nothing if the tracker is already disposed.
if (this.counter < 0) {
return;
}
// Mark the tracker as disposed.
this.counter = -1;
// Clear the listeners for the tracker.
this.activeChangedEmitter.dispose();
this.currentChangedEmitter.dispose();
// Remove all event listeners.
this._views.forEach(function (view) {
var _view$container, _view$container2;
(_view$container = view.container) === null || _view$container === void 0 || (_view$container = _view$container.current) === null || _view$container === void 0 || _view$container.removeEventListener('focus', _this, true);
(_view$container2 = view.container) === null || _view$container2 === void 0 || (_view$container2 = _view$container2.current) === null || _view$container2 === void 0 || _view$container2.removeEventListener('blur', _this, true);
});
// Clear the internal data structures.
this._activeView = null;
this._currentView = null;
this.nodes.clear();
this.numbers.clear();
this._views.length = 0;
}
/**
* Get the focus number for a particular view in the tracker.
*
* @param view - The view of interest.
*
* @returns The focus number for the given view, or `-1` if the
* view has not had focus since being added to the tracker, or
* is not contained by the tracker.
*
* #### Notes
* The focus number indicates the relative order in which the views
* have gained focus. A view with a larger number has gained focus
* more recently than a view with a smaller number.
*
* The `currentView` will always have the largest focus number.
*
* All views start with a focus number of `-1`, which indicates that
* the view has not been focused since being added to the tracker.
*/
}, {
key: "focusNumber",
value: function focusNumber(view) {
var n = this.numbers.get(view);
return n === undefined ? -1 : n;
}
/**
* Test whether the focus tracker contains a given view.
*
* @param view - The view of interest.
*
* @returns `true` if the view is tracked, `false` otherwise.
*/
}, {
key: "has",
value: function has(view) {
return this.numbers.has(view);
}
/**
* Add a view to the focus tracker.
*
* @param view - The view of interest.
*
* #### Notes
* A view will be automatically removed from the tracker if it
* is disposed after being added.
*
* If the view is already tracked, this is a no-op.
*/
}, {
key: "add",
value: function add(view) {
var _view$container3,
_view$container4,
_view$container6,
_view$container7,
_this2 = this;
// Do nothing if the view is already tracked.
if (this.numbers.has(view)) {
return;
}
// Test whether the view has focus.
var focused = (_view$container3 = view.container) === null || _view$container3 === void 0 || (_view$container3 = _view$container3.current) === null || _view$container3 === void 0 ? void 0 : _view$container3.contains(document.activeElement);
// Set up the initial focus number.
var n = focused ? this.counter++ : -1;
// Add the view to the internal data structures.
this._views.push(view);
this.numbers.set(view, n);
if ((_view$container4 = view.container) !== null && _view$container4 !== void 0 && _view$container4.current) {
var _view$container5;
this.nodes.set((_view$container5 = view.container) === null || _view$container5 === void 0 ? void 0 : _view$container5.current, view);
}
// Set up the event listeners. The capturing phase must be used
// since the 'focus' and 'blur' events don't bubble and Firefox
// doesn't support the 'focusin' or 'focusout' events.
(_view$container6 = view.container) === null || _view$container6 === void 0 || (_view$container6 = _view$container6.current) === null || _view$container6 === void 0 || _view$container6.addEventListener('focus', this, true);
(_view$container7 = view.container) === null || _view$container7 === void 0 || (_view$container7 = _view$container7.current) === null || _view$container7 === void 0 || _view$container7.addEventListener('blur', this, true);
// Connect the disposed signal handler.
view.onDisposed(function () {
return _this2.onViewDisposed;
});
// Set the current and active views if needed.
if (focused) {
this.setViews(view, view);
}
}
/**
* Remove a view from the focus tracker.
*
* #### Notes
* If the view is the `currentView`, the previous current view
* will become the new `currentView`.
*
* A view will be automatically removed from the tracker if it
* is disposed after being added.
*
* If the view is not tracked, this is a no-op.
*/
}, {
key: "remove",
value: function remove(view) {
var _view$container8,
_view$container9,
_view$container10,
_this3 = this;
// Bail early if the view is not tracked.
if (!this.numbers.has(view)) {
return;
}
// Disconnect the disposed signal handler.
// view.disposed.disconnect(this.onViewDisposed, this);
// Remove the event listeners.
(_view$container8 = view.container) === null || _view$container8 === void 0 || (_view$container8 = _view$container8.current) === null || _view$container8 === void 0 || _view$container8.removeEventListener('focus', this, true);
(_view$container9 = view.container) === null || _view$container9 === void 0 || (_view$container9 = _view$container9.current) === null || _view$container9 === void 0 || _view$container9.removeEventListener('blur', this, true);
// Remove the view from the internal data structures.
this._views.splice(this._views.indexOf(view), 1);
if ((_view$container10 = view.container) !== null && _view$container10 !== void 0 && _view$container10.current) {
var _view$container11;
this.nodes.delete((_view$container11 = view.container) === null || _view$container11 === void 0 ? void 0 : _view$container11.current);
}
this.numbers.delete(view);
// Bail early if the view is not the current view.
if (this._currentView !== view) {
return;
}
// Filter the views for those which have had focus.
var valid = this._views.filter(function (w) {
return _this3.numbers.get(w) !== -1;
});
// Get the valid view with the max focus number.
var previous = _manaCommon.Iterable.max(valid, function (first, second) {
var a = _this3.numbers.get(first);
var b = _this3.numbers.get(second);
return a - b;
}) || null;
// Set the current and active views.
this.setViews(previous, null);
}
/**
* Handle the DOM events for the focus tracker.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the tracked nodes. It should
* not be called directly by user code.
*/
}, {
key: "handleEvent",
value: function handleEvent(event) {
switch (event.type) {
case 'focus':
this.handleFocusEvent(event);
break;
case 'blur':
this.handleBlurEvent(event);
break;
}
}
/**
* Set the current and active views for the tracker.
*/
}, {
key: "setViews",
value: function setViews(current, active) {
// Swap the current view.
var oldCurrent = this._currentView;
this._currentView = current;
// Swap the active view.
var oldActive = this._activeView;
this._activeView = active;
// Emit the `currentChanged` signal if needed.
if (oldCurrent !== current) {
this.currentChangedEmitter.fire({
oldValue: oldCurrent,
newValue: current
});
}
// Emit the `activeChanged` signal if needed.
if (oldActive !== active) {
this.activeChangedEmitter.fire({
oldValue: oldActive,
newValue: active
});
}
}
/**
* Handle the `'focus'` event for a tracked view.
*/
}, {
key: "handleFocusEvent",
value: function handleFocusEvent(event) {
// Find the view which gained focus, which is known to exist.
var view = this.nodes.get(event.currentTarget);
// Update the focus number if necessary.
if (view !== this._currentView) {
this.numbers.set(view, this.counter++);
}
// Set the current and active views.
this.setViews(view, view);
}
/**
* Handle the `'blur'` event for a tracked view.
*/
}, {
key: "handleBlurEvent",
value: function handleBlurEvent(event) {
var _view$container12;
// Find the view which lost focus, which is known to exist.
var view = this.nodes.get(event.currentTarget);
// Get the node which being focused after this blur.
var focusTarget = event.relatedTarget;
// If no other node is being focused, clear the active view.
if (!focusTarget) {
this.setViews(this._currentView, null);
return;
}
// Bail if the focus view is not changing.
if ((_view$container12 = view.container) !== null && _view$container12 !== void 0 && (_view$container12 = _view$container12.current) !== null && _view$container12 !== void 0 && _view$container12.contains(focusTarget)) {
return;
}
// If no tracked view is being focused, clear the active view.
if (!this._views.find(function (v) {
var _v$container;
return (_v$container = v.container) === null || _v$container === void 0 || (_v$container = _v$container.current) === null || _v$container === void 0 ? void 0 : _v$container.contains(focusTarget);
})) {
this.setViews(this._currentView, null);
return;
}
}
/**
* Handle the `disposed` signal for a tracked view.
*/
}, {
key: "onViewDisposed",
value: function onViewDisposed(sender) {
this.remove(sender);
}
}]);
return FocusTracker;
}()) || _class);
/**
* The namespace for the `FocusTracker` class statics.
*/