src/helper/tabs-store.ts (170 lines of code) (raw):
/*!
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable @typescript-eslint/no-dynamic-delete */
import clone from 'just-clone';
import { MynahUIDataModel, MynahUITabStoreModel, MynahUITabStoreTab } from '../static';
import { Config } from './config';
import { generateUID } from './guid';
import { MynahUIDataStore } from './store';
interface TabStoreSubscription {
'add': Record<string, (tabId: string, tabData?: MynahUITabStoreTab) => void>;
'remove': Record<string, (tabId: string, newSelectedTab?: MynahUITabStoreTab) => void>;
'update': Record<string, (tabId: string, tabData?: MynahUITabStoreTab) => void>;
'beforeTabChange': Record<string, (tabId: string, previousSelectedTab?: MynahUITabStoreTab) => void>;
'selectedTabChange': Record<string, (tabId: string, previousSelectedTab?: MynahUITabStoreTab) => void>;
}
export class EmptyMynahUITabsStoreModel {
data: Required<MynahUITabStoreModel>;
constructor () {
const guid = generateUID();
this.data = {
[guid]: {
isSelected: true,
store: {},
}
};
}
}
export class MynahUITabsStore {
private static instance: MynahUITabsStore | undefined;
private readonly subscriptions: TabStoreSubscription = {
add: {},
remove: {},
update: {},
beforeTabChange: {},
selectedTabChange: {}
};
private readonly tabsStore: Required<MynahUITabStoreModel> = {};
private readonly tabsDataStore: Record<string, MynahUIDataStore> = {};
private tabDefaults: MynahUITabStoreTab = {};
private constructor (initialData?: MynahUITabStoreModel, defaults?: MynahUITabStoreTab) {
this.tabsStore = Object.assign(this.tabsStore, initialData);
if (defaults != null) {
this.tabDefaults = defaults;
}
if ((initialData != null) && Object.keys(initialData).length > 0) {
Object.keys(initialData).forEach((tabId: string) => {
this.tabsDataStore[tabId] = new MynahUIDataStore(tabId, initialData[tabId].store ?? {});
});
}
}
private readonly deselectAllTabs = (): void => {
Object.keys(this.tabsStore).forEach(tabId => {
this.tabsStore[tabId].isSelected = false;
});
};
public readonly addTab = (tabData?: MynahUITabStoreTab): string | undefined => {
if (Object.keys(this.tabsStore).length < Config.getInstance().config.maxTabs) {
const tabId = generateUID();
this.deselectAllTabs();
this.tabsStore[tabId] = { ...this.tabDefaults, ...tabData, isSelected: true };
this.tabsDataStore[tabId] = new MynahUIDataStore(tabId, this.tabsStore[tabId].store ?? {});
this.informSubscribers('add', tabId, this.tabsStore[tabId]);
this.informSubscribers('selectedTabChange', tabId, this.tabsStore[tabId]);
return tabId;
}
};
public readonly removeTab = (tabId: string): string => {
const wasSelected = this.tabsStore[tabId].isSelected ?? false;
let newSelectedTab: MynahUITabStoreTab | undefined;
delete this.tabsStore[tabId];
this.tabsDataStore[tabId].resetStore();
delete this.tabsDataStore[tabId];
if (wasSelected) {
const tabIds = Object.keys(this.tabsStore);
if (tabIds.length > 0) {
this.deselectAllTabs();
this.selectTab(tabIds[tabIds.length - 1]);
newSelectedTab = this.tabsStore[this.getSelectedTabId()];
}
}
this.informSubscribers('remove', tabId, newSelectedTab);
return tabId;
};
public readonly selectTab = (tabId: string): void => {
this.informSubscribers('beforeTabChange', tabId, this.tabsStore[tabId]);
this.deselectAllTabs();
this.tabsStore[tabId].isSelected = true;
this.informSubscribers('selectedTabChange', tabId, this.tabsStore[tabId]);
};
/**
* Updates the store and informs the subscribers.
* @param data A full or partial set of store data model with values.
*/
public updateTab = (tabId: string, tabData?: Partial<MynahUITabStoreTab>, skipSubscribers?: boolean): void => {
if (this.tabsStore[tabId] != null) {
if (tabData?.isSelected === true && this.getSelectedTabId() !== tabId) {
this.selectTab(tabId);
}
this.tabsStore[tabId] = { ...this.tabsStore[tabId], ...tabData };
if (tabData?.store != null) {
if (this.tabsDataStore[tabId] === undefined) {
this.tabsDataStore[tabId] = new MynahUIDataStore(tabId);
}
this.tabsDataStore[tabId].updateStore(tabData?.store);
}
if (skipSubscribers !== true) {
this.informSubscribers('update', tabId, this.tabsStore[tabId]);
}
}
};
public static getInstance = (initialData?: MynahUITabStoreModel, defaults?: MynahUITabStoreTab): MynahUITabsStore => {
if (MynahUITabsStore.instance === undefined) {
MynahUITabsStore.instance = new MynahUITabsStore(initialData, defaults);
}
return MynahUITabsStore.instance;
};
/**
* Subscribe to changes of the tabsStore
* @param handler function will be called when tabs changed
* @returns subscriptionId which needed to unsubscribe
*/
public addListener = (eventName: keyof TabStoreSubscription, handler: (tabId: string, tabData?: MynahUITabStoreTab) => void): string => {
const subscriptionId: string = generateUID();
this.subscriptions[eventName][subscriptionId] = handler;
return subscriptionId;
};
/**
* Subscribe to changes of the tabs' data store
* @param handler function will be called when tabs changed
* @returns subscriptionId which needed to unsubscribe
*/
public addListenerToDataStore = (tabId: string, storeKey: keyof MynahUIDataModel, handler: (newValue: any, oldValue?: any) => void): string | null => {
if (this.tabsDataStore[tabId] !== undefined) {
return this.tabsDataStore[tabId].subscribe(storeKey, handler);
}
return null;
};
/**
* Unsubscribe from changes of the tabs' data store
* @param handler function will be called when tabs changed
* @returns subscriptionId which needed to unsubscribe
*/
public removeListenerFromDataStore = (tabId: string, subscriptionId: string, storeKey: keyof MynahUIDataModel): void => {
if (this.tabsDataStore[tabId] !== undefined) {
this.tabsDataStore[tabId].unsubscribe(storeKey, subscriptionId);
}
};
/**
* Unsubscribe from changes of the tabs store
* @param subscriptionId subscriptionId which is returned from subscribe function
*/
public removeListener = (eventName: keyof TabStoreSubscription, subscriptionId: string): void => {
if (this.subscriptions[eventName][subscriptionId] !== undefined) {
delete this.subscriptions[eventName][subscriptionId];
}
};
private readonly informSubscribers = (eventName: keyof TabStoreSubscription, tabId: string, tabData?: MynahUITabStoreTab): void => {
const subscriberKeys = Object.keys(this.subscriptions[eventName]);
subscriberKeys.forEach(subscriberKey => {
this.subscriptions[eventName][subscriberKey](tabId, tabData);
});
};
/**
* Returns the tab
* @param tabId Tab Id
* @returns info of the tab
*/
public getTab = (tabId: string): MynahUITabStoreTab | null => this.tabsStore[tabId] ?? null;
/**
* Returns the tab
* @param tabId Tab Id
* @returns info of the tab
*/
public getAllTabs = (): MynahUITabStoreModel => {
const clonedTabs = clone(this.tabsStore) as MynahUITabStoreModel;
Object.keys(clonedTabs).forEach(tabId => {
clonedTabs[tabId].store = clone(this.getTabDataStore(tabId).getStore() as object) ?? {};
});
return clonedTabs;
};
/**
* Returns the data store of the tab
* @param tabId Tab Id
* @returns data of the tab
*/
public getTabDataStore = (tabId: string): MynahUIDataStore => this.tabsDataStore[tabId];
/**
* Returns the data of the tab
* @param tabId Tab Id
* @returns data of the tab
*/
public getSelectedTabId = (): string => {
const tabIds = Object.keys(this.tabsStore);
return tabIds.find(tabId => this.tabsStore[tabId].isSelected === true) ?? '';
};
/**
* Clears store data and informs all the subscribers
*/
public removeAllTabs = (): void => {
Object.keys(this.tabsStore).forEach(tabId => {
this.removeTab(tabId);
});
};
/**
* Get all tabs length
* @returns tabs length
*/
public tabsLength = (): number => Object.keys(this.tabsStore).length;
/**
* Updates defaults of the tab store
* @param defaults MynahUITabStoreTab
*/
public updateTabDefaults = (defaults: MynahUITabStoreTab): void => {
this.tabDefaults = {
store: {
...this.tabDefaults.store,
...defaults.store
}
};
};
/**
* Updates defaults of the tab store
* @param defaults MynahUITabStoreTab
*/
public getTabDefaults = (): MynahUITabStoreTab => {
return this.tabDefaults;
};
public destroy = (): void => {
MynahUITabsStore.instance = undefined;
};
}