mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
264 lines
8.0 KiB
264 lines
8.0 KiB
import { QueryList, InjectionToken } from '@angular/core';
|
|
import { Subscription, isObservable, Subject, of } from 'rxjs';
|
|
import { take } from 'rxjs/operators';
|
|
import { Typeahead } from './_typeahead-chunk.mjs';
|
|
import { coerceObservable } from './coercion-private.mjs';
|
|
|
|
class TreeKeyManager {
|
|
_activeItemIndex = -1;
|
|
_activeItem = null;
|
|
_shouldActivationFollowFocus = false;
|
|
_horizontalOrientation = 'ltr';
|
|
_skipPredicateFn = _item => false;
|
|
_trackByFn = item => item;
|
|
_items = [];
|
|
_typeahead;
|
|
_typeaheadSubscription = Subscription.EMPTY;
|
|
_hasInitialFocused = false;
|
|
_initializeFocus() {
|
|
if (this._hasInitialFocused || this._items.length === 0) {
|
|
return;
|
|
}
|
|
let activeIndex = 0;
|
|
for (let i = 0; i < this._items.length; i++) {
|
|
if (!this._skipPredicateFn(this._items[i]) && !this._isItemDisabled(this._items[i])) {
|
|
activeIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
const activeItem = this._items[activeIndex];
|
|
if (activeItem.makeFocusable) {
|
|
this._activeItem?.unfocus();
|
|
this._activeItemIndex = activeIndex;
|
|
this._activeItem = activeItem;
|
|
this._typeahead?.setCurrentSelectedItemIndex(activeIndex);
|
|
activeItem.makeFocusable();
|
|
} else {
|
|
this.focusItem(activeIndex);
|
|
}
|
|
this._hasInitialFocused = true;
|
|
}
|
|
constructor(items, config) {
|
|
if (items instanceof QueryList) {
|
|
this._items = items.toArray();
|
|
items.changes.subscribe(newItems => {
|
|
this._items = newItems.toArray();
|
|
this._typeahead?.setItems(this._items);
|
|
this._updateActiveItemIndex(this._items);
|
|
this._initializeFocus();
|
|
});
|
|
} else if (isObservable(items)) {
|
|
items.subscribe(newItems => {
|
|
this._items = newItems;
|
|
this._typeahead?.setItems(newItems);
|
|
this._updateActiveItemIndex(newItems);
|
|
this._initializeFocus();
|
|
});
|
|
} else {
|
|
this._items = items;
|
|
this._initializeFocus();
|
|
}
|
|
if (typeof config.shouldActivationFollowFocus === 'boolean') {
|
|
this._shouldActivationFollowFocus = config.shouldActivationFollowFocus;
|
|
}
|
|
if (config.horizontalOrientation) {
|
|
this._horizontalOrientation = config.horizontalOrientation;
|
|
}
|
|
if (config.skipPredicate) {
|
|
this._skipPredicateFn = config.skipPredicate;
|
|
}
|
|
if (config.trackBy) {
|
|
this._trackByFn = config.trackBy;
|
|
}
|
|
if (typeof config.typeAheadDebounceInterval !== 'undefined') {
|
|
this._setTypeAhead(config.typeAheadDebounceInterval);
|
|
}
|
|
}
|
|
change = new Subject();
|
|
destroy() {
|
|
this._typeaheadSubscription.unsubscribe();
|
|
this._typeahead?.destroy();
|
|
this.change.complete();
|
|
}
|
|
onKeydown(event) {
|
|
const key = event.key;
|
|
switch (key) {
|
|
case 'Tab':
|
|
return;
|
|
case 'ArrowDown':
|
|
this._focusNextItem();
|
|
break;
|
|
case 'ArrowUp':
|
|
this._focusPreviousItem();
|
|
break;
|
|
case 'ArrowRight':
|
|
this._horizontalOrientation === 'rtl' ? this._collapseCurrentItem() : this._expandCurrentItem();
|
|
break;
|
|
case 'ArrowLeft':
|
|
this._horizontalOrientation === 'rtl' ? this._expandCurrentItem() : this._collapseCurrentItem();
|
|
break;
|
|
case 'Home':
|
|
this._focusFirstItem();
|
|
break;
|
|
case 'End':
|
|
this._focusLastItem();
|
|
break;
|
|
case 'Enter':
|
|
case ' ':
|
|
this._activateCurrentItem();
|
|
break;
|
|
default:
|
|
if (event.key === '*') {
|
|
this._expandAllItemsAtCurrentItemLevel();
|
|
break;
|
|
}
|
|
this._typeahead?.handleKey(event);
|
|
return;
|
|
}
|
|
this._typeahead?.reset();
|
|
event.preventDefault();
|
|
}
|
|
getActiveItemIndex() {
|
|
return this._activeItemIndex;
|
|
}
|
|
getActiveItem() {
|
|
return this._activeItem;
|
|
}
|
|
_focusFirstItem() {
|
|
this.focusItem(this._findNextAvailableItemIndex(-1));
|
|
}
|
|
_focusLastItem() {
|
|
this.focusItem(this._findPreviousAvailableItemIndex(this._items.length));
|
|
}
|
|
_focusNextItem() {
|
|
this.focusItem(this._findNextAvailableItemIndex(this._activeItemIndex));
|
|
}
|
|
_focusPreviousItem() {
|
|
this.focusItem(this._findPreviousAvailableItemIndex(this._activeItemIndex));
|
|
}
|
|
focusItem(itemOrIndex, options = {}) {
|
|
options.emitChangeEvent ??= true;
|
|
let index = typeof itemOrIndex === 'number' ? itemOrIndex : this._items.findIndex(item => this._trackByFn(item) === this._trackByFn(itemOrIndex));
|
|
if (index < 0 || index >= this._items.length) {
|
|
return;
|
|
}
|
|
const activeItem = this._items[index];
|
|
if (this._activeItem !== null && this._trackByFn(activeItem) === this._trackByFn(this._activeItem)) {
|
|
return;
|
|
}
|
|
const previousActiveItem = this._activeItem;
|
|
this._activeItem = activeItem ?? null;
|
|
this._activeItemIndex = index;
|
|
this._typeahead?.setCurrentSelectedItemIndex(index);
|
|
this._activeItem?.focus();
|
|
previousActiveItem?.unfocus();
|
|
if (options.emitChangeEvent) {
|
|
this.change.next(this._activeItem);
|
|
}
|
|
if (this._shouldActivationFollowFocus) {
|
|
this._activateCurrentItem();
|
|
}
|
|
}
|
|
_updateActiveItemIndex(newItems) {
|
|
const activeItem = this._activeItem;
|
|
if (!activeItem) {
|
|
return;
|
|
}
|
|
const newIndex = newItems.findIndex(item => this._trackByFn(item) === this._trackByFn(activeItem));
|
|
if (newIndex > -1 && newIndex !== this._activeItemIndex) {
|
|
this._activeItemIndex = newIndex;
|
|
this._typeahead?.setCurrentSelectedItemIndex(newIndex);
|
|
}
|
|
}
|
|
_setTypeAhead(debounceInterval) {
|
|
this._typeahead = new Typeahead(this._items, {
|
|
debounceInterval: typeof debounceInterval === 'number' ? debounceInterval : undefined,
|
|
skipPredicate: item => this._skipPredicateFn(item)
|
|
});
|
|
this._typeaheadSubscription = this._typeahead.selectedItem.subscribe(item => {
|
|
this.focusItem(item);
|
|
});
|
|
}
|
|
_findNextAvailableItemIndex(startingIndex) {
|
|
for (let i = startingIndex + 1; i < this._items.length; i++) {
|
|
if (!this._skipPredicateFn(this._items[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
return startingIndex;
|
|
}
|
|
_findPreviousAvailableItemIndex(startingIndex) {
|
|
for (let i = startingIndex - 1; i >= 0; i--) {
|
|
if (!this._skipPredicateFn(this._items[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
return startingIndex;
|
|
}
|
|
_collapseCurrentItem() {
|
|
if (!this._activeItem) {
|
|
return;
|
|
}
|
|
if (this._isCurrentItemExpanded()) {
|
|
this._activeItem.collapse();
|
|
} else {
|
|
const parent = this._activeItem.getParent();
|
|
if (!parent || this._skipPredicateFn(parent)) {
|
|
return;
|
|
}
|
|
this.focusItem(parent);
|
|
}
|
|
}
|
|
_expandCurrentItem() {
|
|
if (!this._activeItem) {
|
|
return;
|
|
}
|
|
if (!this._isCurrentItemExpanded()) {
|
|
this._activeItem.expand();
|
|
} else {
|
|
coerceObservable(this._activeItem.getChildren()).pipe(take(1)).subscribe(children => {
|
|
const firstChild = children.find(child => !this._skipPredicateFn(child));
|
|
if (!firstChild) {
|
|
return;
|
|
}
|
|
this.focusItem(firstChild);
|
|
});
|
|
}
|
|
}
|
|
_isCurrentItemExpanded() {
|
|
if (!this._activeItem) {
|
|
return false;
|
|
}
|
|
return typeof this._activeItem.isExpanded === 'boolean' ? this._activeItem.isExpanded : this._activeItem.isExpanded();
|
|
}
|
|
_isItemDisabled(item) {
|
|
return typeof item.isDisabled === 'boolean' ? item.isDisabled : item.isDisabled?.();
|
|
}
|
|
_expandAllItemsAtCurrentItemLevel() {
|
|
if (!this._activeItem) {
|
|
return;
|
|
}
|
|
const parent = this._activeItem.getParent();
|
|
let itemsToExpand;
|
|
if (!parent) {
|
|
itemsToExpand = of(this._items.filter(item => item.getParent() === null));
|
|
} else {
|
|
itemsToExpand = coerceObservable(parent.getChildren());
|
|
}
|
|
itemsToExpand.pipe(take(1)).subscribe(items => {
|
|
for (const item of items) {
|
|
item.expand();
|
|
}
|
|
});
|
|
}
|
|
_activateCurrentItem() {
|
|
this._activeItem?.activate();
|
|
}
|
|
}
|
|
const TREE_KEY_MANAGER = new InjectionToken('tree-key-manager', {
|
|
providedIn: 'root',
|
|
factory: () => (items, options) => new TreeKeyManager(items, options)
|
|
});
|
|
|
|
export { TREE_KEY_MANAGER, TreeKeyManager };
|
|
//# sourceMappingURL=_tree-key-manager-chunk.mjs.map
|
|
|