comindware.core.ui
Version:
Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.
460 lines (379 loc) • 15.3 kB
text/typescript
import { helpers } from 'utils';
import template from './templates/tabLayout.hbs';
import TabHeaderView from './TabHeaderView';
import StepperView from './StepperView';
import LayoutBehavior from '../behaviors/LayoutBehavior';
import LoadingBehavior from '../../views/behaviors/LoadingBehavior';
import TabModel from './models/TabModel';
import ConfigDiff from '../../components/treeEditor/classes/ConfigDiff';
import { ChildsFilter, TreeConfig, GraphModel } from '../../components/treeEditor/types';
import Backbone from 'backbone';
type Tab = { view?: Backbone.View, id: string, name: string, enabled?: boolean, visible?: boolean, error?: string };
type TabsList = Array<Tab>;
type TabsKeyValue = { [key: string]: Backbone.View };
type ShowTabOptions = { region: Marionette.Region, tabModel: TabModel, view: Backbone.View, regionEl: HTMLElement };
const classes = {
CLASS_NAME: 'layout__tab-layout',
PANEL_REGION: 'layout__tab-layout__panel-region',
HIDDEN: 'layout__tab-hidden'
};
const defaultOptions = {
headerClass: '',
bodyClass: ''
};
export default Marionette.View.extend({
initialize(options: { tabs: TabsList, showTreeEditor?: boolean, getView: Function }) {
_.defaults(this.options, defaultOptions);
this.tabsViewById = {};
this.showTreeEditor = options.showTreeEditor;
this.initializeTabCollection(options.tabs);
if (this.showTreeEditor) {
this.__initTreeEditor();
}
},
template: Handlebars.compile(template),
className(): string {
const classList = [];
classList.push(this.getOption('bodyClass') || '');
return `${classes.CLASS_NAME} ${classList.join(' ')}`;
},
regions: {
headerRegion: {
el: '.js-header-region',
replaceElement: true
},
treeEditorRegion: '.js-tree-editor-region',
stepperRegion: '.js-stepper-region',
loadingRegion: '.js-loading-region'
},
ui: {
panelContainer: '.js-panel-container'
},
behaviors: {
LayoutBehavior: {
behaviorClass: LayoutBehavior
},
LoadingBehavior: {
behaviorClass: LoadingBehavior,
region: 'loadingRegion'
}
},
onRender(): void {
const tabHeaderView = new TabHeaderView({
collection: this.__tabsCollection,
headerClass: this.getOption('headerClass')
});
this.listenTo(tabHeaderView, 'select', model => this.__handleSelect(model));
this.showChildView('headerRegion', tabHeaderView);
if (this.showTreeEditor) {
this.treeEditorView = this.treeEditor.getView();
this.showChildView('treeEditorRegion', this.treeEditorView);
const configDiff = this.treeEditor.getConfigDiff();
this.__tabsCollection.forEach((model: TabModel) => {
const isHidden = configDiff.get(model.id)?.isHidden;
if (typeof isHidden === 'boolean') {
model.set({ isHidden });
}
});
}
this.listenTo(this.__tabsCollection, 'change:selected', this.__onSelectedChanged);
this.listenTo(this.__tabsCollection, 'change:isHidden change:visible', this.__onChangeShowing);
if (this.isAllHiddenTab()) {
this.__setNoTabsState(true);
} else {
const selectedTab = this.__getSelectedTab();
if (this.getOption('deferRender') && !this.isAllHiddenTab()) {
this.renderTab(selectedTab, true);
} else {
this.__tabsCollection.forEach(model => {
this.renderTab(model, false);
});
}
if (selectedTab) {
this.selectTab(selectedTab.id);
}
}
this.__updateState();
if (this.getOption('showStepper')) {
const stepperView = new StepperView({ collection: this.__tabsCollection });
this.showChildView('stepperRegion', stepperView);
this.listenTo(stepperView, 'stepper:item:selected', this.__handleStepperSelect);
}
},
update(): void {
Object.values(this.tabsViewById).forEach(view => {
if (view && typeof view.update === 'function') {
view.update();
}
});
this.__updateState();
},
validate(): void {
let result;
Object.entries(this.tabsViewById).forEach(([tabId, view]) => {
if (view && typeof view.validate === 'function') {
const error = view.validate();
this.setTabError(tabId, error);
if (error) {
result = true;
}
}
});
return result;
},
getViewById(tabId: string) {
return this.tabsViewById[tabId];
},
selectTab(tabId: string): void | boolean {
const tab = this.__findTab(tabId);
if (tab.get('selected')) {
return;
}
const previousSelectedTab = this.__getSelectedTab();
if (previousSelectedTab) {
if (this.getOption('validateBeforeMove')) {
const view = this.tabsViewById[previousSelectedTab.id];
const errors = view?.form.validate();
this.setTabError(previousSelectedTab.id, errors);
if (errors) {
return false;
}
}
}
if (tab.get('enabled')) {
tab.set('selected', true);
if (!tab.get('isRendered') && this.isRendered()) {
this.renderTab(tab, Boolean(this.getOption('deferRender')));
}
this.selectTabIndex = this.__getTabIndex(tab);
}
// For IE (scroll position jumped up when tabs reselected)
if (previousSelectedTab) {
previousSelectedTab.set('selected', false);
}
},
addTabs(tabList: TabsList, selectFirst: boolean, index: number) {
if (!Array.isArray(tabList)) {
Core.InterfaceError.logError('tabs must be passed as Array');
return;
}
this.__prepareTabs(tabList);
this.__tabsCollection.add(tabList, { at: index || this.__tabsCollection.length });
if (selectFirst) {
this.selectTab(tabList[0].id);
}
},
setEnabled(tabId: string, enabled: boolean): void {
this.__findTab(tabId).set({ enabled });
},
setVisible(tabId: string, visible: boolean): void {
this.__findTab(tabId).set({ visible });
},
isAllHiddenTab() {
const visibleCollection = this.__tabsCollection.filter(tabModel => tabModel.isShow());
// all tabs hidden: show message instead of tab panel
if (!visibleCollection.length) {
return true;
}
return false;
},
getTabsCollection() {
return this.__tabsCollection;
},
__onChangeShowing(tab: Backbone.Model) {
const isShow = tab.isShow();
const visibleCollection = this.__tabsCollection.filter(tabModel => tabModel.isShow());
let newTabIndex;
let selectedtab = this.__getSelectedTab();
if (this.isAllHiddenTab()) {
this.__setNoTabsState(true);
selectedtab?.set('selected', false);
return;
}
// show first tab, other tabs was hidden before
if (isShow && this.hasNoVisibleTabs) {
this.selectTab(tab.id);
selectedtab = tab;
this.__setNoTabsState(false);
return;
}
this.__setNoTabsState(false);
// if we hide or show another tab, then nothing needs to be done
if (tab.id !== selectedtab.id) {
return;
}
// if we hide selected tab, then another tab must be selected. Let it be the closest one.
const tabIndex = this.__tabsCollection.indexOf(tab);
if (visibleCollection.length) {
const indexesOfVisibleCollection = visibleCollection.map((tabModel: TabModel) => this.__tabsCollection.indexOf(tabModel));
newTabIndex = this.__getClosestVisibleTab(indexesOfVisibleCollection, tabIndex);
} else {
newTabIndex = tabIndex;
}
this.selectTab(this.__tabsCollection.at(newTabIndex).id);
},
setTabError(tabId: string, error: string): void {
this.__findTab(tabId).set({ error });
},
setLoading(state: Boolean | Promise<any>): void {
this.loading.setLoading(state);
},
__getClosestVisibleTab(indexes: number[], tabIndex: number): number {
let min = this.__tabsCollection.length;
let newTabIndex = 0;
// find the closest index to given one
indexes.forEach(index => {
const newMin = Math.abs(index - tabIndex);
if (newMin < min) {
min = newMin;
newTabIndex = index;
}
});
return newTabIndex;
},
__setNoTabsState(hasNoTabs: boolean): void {
this.hasNoVisibleTabs = hasNoTabs;
this.__toggleHiddenAttribute(this.el.querySelector('.js-panel-container'), hasNoTabs);
this.__toggleHiddenAttribute(this.el.querySelector('.js-no-tabs-message'), !hasNoTabs);
},
__toggleHiddenAttribute(element: HTMLElement, flag: boolean): void {
if (flag) {
element.setAttribute('hidden', '');
return;
}
element.removeAttribute('hidden');
},
initializeTabCollection(tabs: Backbone.Collection | TabsList): void {
let tabList = [];
if (tabs instanceof Backbone.Collection) {
tabList = tabs.toJSON();
this.listenTo(tabs, 'add remove reset', () => {
this.__tabsCollection.reset(tabs.toJSON());
});
Core.InterfaceError.logError('Passing tabs as Backbone.Collection is deprecated');
} else if (Array.isArray(tabs)) {
tabList = [...tabs];
} else {
Core.InterfaceError.logError('tabs must be passed');
}
this.__prepareTabs(tabList);
this.__tabsCollection = new Backbone.Collection(tabList, { model: TabModel });
let selectedTab = this.__getSelectedTab();
if (!selectedTab) {
selectedTab = this.__tabsCollection.find((tabModel: Backbone.Model) => tabModel.isShow() && tabModel.get('enabled')) || this.__tabsCollection.at(0);
this.selectTab(selectedTab.id);
}
this.selectTabIndex = this.__getTabIndex(selectedTab);
},
renderTab(tabModel: Backbone.Model, isLoadingNeeded: boolean): void {
const regionEl = document.createElement('div');
regionEl.className = classes.PANEL_REGION;
this.ui.panelContainer.append(regionEl);
const region = this.addRegion(`${tabModel.id}TabRegion`, {
el: regionEl
});
const __view = this.tabsViewById[tabModel.id];
const view = typeof __view === 'function' ? __view(tabModel) : __view;
if (helpers.isPromise(view)) {
isLoadingNeeded && this.setLoading(true);
view.then((_view: Backbone.View) => {
this.showTab({ view: _view, tabModel, region, regionEl });
isLoadingNeeded && this.setLoading(false);
});
} else {
this.showTab({ view, tabModel, region, regionEl });
}
},
showTab({ view, tabModel, region, regionEl } = {}) {
if (!(view instanceof Backbone.View)) {
Core.InterfaceError.logError('Invalid view argument');
return;
}
this.__showTab({ region, tabModel, view, regionEl });
this.tabsViewById[tabModel.id] = view;
this.listenTo(view, 'all', (...args) => {
args[0] = `tab:${args[0]}`;
this.trigger(...args);
});
this.listenTo(view, 'change:visible', (model, visible) => this.setVisible(tabModel.id, visible));
this.listenTo(view, 'change:enabled', (model, enabled) => this.setEnabled(tabModel.id, enabled));
},
getTabRegion(tabId: number) {
return this.getRegion(`${tabId}TabRegion`);
},
__prepareTabs(tabList: TabsList) {
tabList.forEach((t: Tab) => {
this.tabsViewById[t.id] = t.view;
delete t.view;
});
},
__showTab(options: ShowTabOptions): void {
const { region, tabModel, view } = options;
region.show(view);
tabModel.set({ isRendered: true });
this.__updateTabRegion(tabModel);
},
__getSelectedTab(): TabModel {
const selectedTab = this.__tabsCollection.find((tabModel: TabModel) => tabModel.get('selected'));
return selectedTab;
},
__getTabIndex(model: TabModel): number {
return this.__tabsCollection.indexOf(model);
},
__findTab(tabId: string): Backbone.Model {
const tabModel = this.__tabsCollection.get(tabId);
if (!tabModel) {
helpers.throwInvalidOperationError(`TabLayout: tab '${tabId}' is not present in the collection.`);
}
return tabModel;
},
__handleSelect(model: Backbone.Model): void {
this.selectTab(model.id);
},
__onSelectedChanged(model: Backbone.Model): void {
this.__updateTabRegion(model);
},
__updateTabRegion(tabModel: Backbone.Model): void {
const region = this.getTabRegion(tabModel.id);
if (!region) {
return;
}
const regionEl = region.el;
const selected = tabModel.get('selected');
regionEl.classList.toggle(classes.HIDDEN, !selected);
this.trigger('changed:selectedTab', tabModel);
Core.services.GlobalEventService.trigger('popout:resize', false);
},
__handleStepperSelect(model: Backbone.Model): void {
this.__handleSelect(model);
},
__initTreeEditor(): void {
this.treeModel = new Backbone.Model({
name: 'Tabs', //TODO Localize, getNodeName
rows: this.__tabsCollection
});
this.__tabsCollection.forEach((tabModel: GraphModel) => {
tabModel.isContainer = true;
tabModel.childrenAttribute = 'tabComponents';
tabModel.set('tabComponents', new Backbone.Collection([{ id: _.uniqueId('treeItem'), name: 'tab content' }])); //TODO generate proper childrens
});
this.treeModel.id = _.uniqueId('treeModelRoot');
this.treeModel.isContainer = !!this.__tabsCollection.length;
this.treeModel.childrenAttribute = 'rows';
const treeEditorOptions: {
model: TreeConfig,
hidden: boolean,
configDiff: ConfigDiff,
childsFilter?: ChildsFilter
} = {
model: this.treeModel,
hidden: this.options.treeEditorIsHidden,
configDiff: this.options.treeEditorConfig
};
const childsFilter = this.options.treeEditorChildsFilter;
if (typeof childsFilter === 'function') {
treeEditorOptions.childsFilter = childsFilter;
}
this.treeEditor = new Core.components.TreeEditor(treeEditorOptions);
}
});