UNPKG

@difizen/mana-core

Version:

411 lines (369 loc) 14.8 kB
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); } 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); } import { Iterable } from '@difizen/mana-common'; import { Emitter } from '@difizen/mana-common'; import { singleton } from '@difizen/mana-syringe'; /** * 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. */ export var FocusTracker = (_dec = 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 Emitter(); this.currentChangedEmitter = new 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 = 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. */