UNPKG

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
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); } });