import { Layout, LayoutType, LayoutDensityType, Viewport } from "../public-api/interfaces"; import { dummyFunc } from "../globals/helpers/helpers"; import { isIOS } from '../utils/toolbox/src/browsers/browser-type'; import { isEqualObject } from '../utils/obj-eq-ckeck'; import { clone, Logger } from '../utils/toolbox/src'; import { GridException } from '../../texts/exception'; interface ViewportObject extends Viewport { name?: string; } // interface for input configuration interface InputConfiguration { readonly externalViewports: Array; readonly extLayout: Layout; readonly logger: Logger; readonly stores: any; } // interface for the config object of this component interface ConfigurationObject { defaultViewports?: Array; // all default viewports externalViewports?: Array; // user provided viewports (raw) rawCustomViewports?: Array; // user provided custom viewports (raw) rawModifiedViewports?: Array; // user provided modified default viewports (raw) sanitizedDefaultViewports?: Array; // use provided layout object in grid config merged on default viewports sanitizedCustomViewports?: Array; // user provided custom viewports (sanitized) sanitizedModifiedViewports?: Array; // user provided modified default viewports (sanitized) disableSwitch?: boolean; enableSwitch?: boolean; prevViewportObject?: ViewportObject; currViewportObject?: ViewportObject; defaultLayout: Layout; viewportChanged: boolean; } interface ViewportMap { index: number; prev: string | null; next: string | null; } interface ViewportMaps { [propName: string]: ViewportMap; } interface FollowerInterface{ recalculateLayout: Function } interface FollowerReference { [propName: string]: FollowerInterface; } interface ChildFactory{ childName: string; childClass: Function; childConfig?: object; callback?: Function; } interface Entryflags { [propName: string]: boolean; } enum DeviceType { MOBILE = "mobile", TABLET_PORTRAIT = "tablet-portrait", TABLET_LANDCSAPE = "tablet-landscape", DESKTOP = "desktop", LARGE_DESKTOP = "large-desktop" } enum OrientationEvents { Resize = "resize", OrientationChange = "orientationchange" } const DEFAULT_VIEWPORTS: Array = [ { name: DeviceType.MOBILE, minscreenwidth: 0, maxscreenwidth: 550, config: { columns: [], layout: { type: LayoutType.Card } } }, { name: DeviceType.TABLET_PORTRAIT, minscreenwidth: 551, maxscreenwidth: 1023, config: { columns: [], layout: { type: LayoutType.Row, density: LayoutDensityType.Compact } } }, { name: DeviceType.TABLET_LANDCSAPE, minscreenwidth: 1024, maxscreenwidth: 1199, config: { columns: [], layout: { type: LayoutType.Row, density: LayoutDensityType.Compact } } }, { name: DeviceType.DESKTOP, minscreenwidth: 1200, maxscreenwidth: 1399, config: { columns: [], layout: { type: LayoutType.Row, density: LayoutDensityType.Default } } }, { name: DeviceType.LARGE_DESKTOP, minscreenwidth: 1400, config: { columns: [], layout: { type: LayoutType.Row, density: LayoutDensityType.Comfortable } } } ], // checks for a valid number isDefined = (value: number): boolean => !isNaN(+value!) && value !== null && +value! !== Infinity, // @todo need to move this to utility // method for getting current screen width // for iOS devices, screen width and height do not alternate on changing screen orientation. If orientaion angle is 90 degeree then the orientation is landscape getScreenWidth = (): number => !isIOS ? window.screen.width : window.orientation === 90 || window.orientation === -90 ? window.screen.height : window.screen.width, // External viewport object validator. There are two scenarios where a custom viewport // is considered to be invalid: // 1. Both min and max screen width is not provided // 2. Both are provided but min screen width is greater than max screen width extViewportValidateQuery = (conf: ViewportObject): boolean => { if (!(isDefined(conf.minscreenwidth!) || isDefined(conf.maxscreenwidth!))) { return false; } else if ( isDefined(conf.minscreenwidth!) && isDefined(conf.maxscreenwidth!) ) { return conf.minscreenwidth! < conf.maxscreenwidth!; } return true; }, // checks if the custom viewport is modifying a default viewport isModifiedViewport = (viewport: ViewportObject): boolean => { let name: string = viewport.name || ""; return ( name === DeviceType.MOBILE || name === DeviceType.TABLET_PORTRAIT || name === DeviceType.TABLET_LANDCSAPE || name === DeviceType.DESKTOP || name === DeviceType.LARGE_DESKTOP ); }, // checks if the viewport is a custom viewport isCustomViewport = (viewport: ViewportObject): boolean => { let name: string = viewport.name || ""; return ( name !== DeviceType.MOBILE && name !== DeviceType.TABLET_PORTRAIT && name !== DeviceType.TABLET_LANDCSAPE && name !== DeviceType.DESKTOP && name !== DeviceType.LARGE_DESKTOP ); }, // gets all unique viewports in an array of viewports with the one defined later is considered getUniqueViewports = ( viewports: Array ): Array => { let revViewports: Array = viewports.reverse(), uniqueViewports: Array, entryFlags: Entryflags = {}; uniqueViewports = revViewports.filter((viewport: ViewportObject) => { if (!entryFlags[viewport.name!]) { return (entryFlags[viewport.name!] = true); } return false; }); return uniqueViewports.reverse(); }, // function that for a given width, it fits into which viewport getContainingViewport = ( value: number, viewports: Array ): ViewportObject => { let i: number, viewport: ViewportObject, containingViewport: ViewportObject; for (i = viewports.length - 1; i >= 0; i--) { viewport = viewports[i]; if (!viewport.maxscreenwidth && value >= viewport.minscreenwidth!) { containingViewport = viewport; break; } else if ( value >= viewport.minscreenwidth! && value <= viewport.maxscreenwidth! ) { containingViewport = viewport; break; } } return containingViewport!; }, // default viewport maps defaultViewportMap: ViewportMaps = { [DeviceType.MOBILE]: { index: 0, prev: null, next: DeviceType.TABLET_PORTRAIT }, [DeviceType.TABLET_PORTRAIT]: { index: 1, prev: DeviceType.MOBILE, next: DeviceType.TABLET_LANDCSAPE }, [DeviceType.TABLET_LANDCSAPE]: { index: 2, prev: DeviceType.TABLET_PORTRAIT, next: DeviceType.DESKTOP }, [DeviceType.DESKTOP]: { index: 3, prev: DeviceType.TABLET_LANDCSAPE, next: DeviceType.LARGE_DESKTOP }, [DeviceType.LARGE_DESKTOP]: { index: 4, prev: DeviceType.DESKTOP, next: null } }; class ViewportManager { config: ConfigurationObject; public orientationHandler: Function; public currentLayout: Layout = {}; private _followers:FollowerReference = {}; private _allowSwitch: boolean = true; private _dispatchEvent: Function = dummyFunc; private _extLayout: Layout = {}; private _logger: Logger; private _stores: any; currentScreenWidth: number; constructor(input: InputConfiguration) { this.config = { viewportChanged: false, defaultLayout: { type: LayoutType.Row } }; this._stores = input.stores; this._logger = input.logger; this.config.externalViewports = getUniqueViewports(input.externalViewports); this._extLayout = input.extLayout; this._setDefaults(); this.currentScreenWidth = getScreenWidth(); this.orientationHandler = (): void => { this._allowSwitch && this._calculateViewport(this.currentScreenWidth = getScreenWidth()); }; this._stores.visualUtils.subscribe((util: any) => { this._dispatchEvent = util.dispatchEvent!; }); this._configure(); } setSwitch(allow: boolean = true): void{ this._allowSwitch = allow; } _setDefaults(): void { let config: ConfigurationObject = this.config; // all default viewports config.defaultViewports = DEFAULT_VIEWPORTS; config.disableSwitch = false; config.enableSwitch = true; } _configure(): void { let config: ConfigurationObject = this.config; config.rawCustomViewports = config.externalViewports!.filter( isCustomViewport ); config.rawModifiedViewports = config.externalViewports!.filter( isModifiedViewport ); config.sanitizedDefaultViewports = this._sanitizeDefaultViewports(this._extLayout); config.sanitizedCustomViewports = this._sanitizeCustomViewports( config.rawCustomViewports ); config.sanitizedModifiedViewports = this._sanitizeModifiedViewports( config.rawModifiedViewports ); // calculate layout initial screen this._calculateViewport(this.currentScreenWidth); } /** * Method that merged external layout with default viewport layouts * @param extLayout * @returns Array array of sanitized default viewports */ _sanitizeDefaultViewports(extLayout: Layout = {}){ let sanitizedDefaultViewports: Array = [], defaultViewports = this.config.defaultViewports!, viewport: Viewport; for (let i = 0; i < defaultViewports.length; i++){ viewport = clone(defaultViewports[i]); viewport.config!.layout = { ...viewport.config!.layout, ...extLayout }; sanitizedDefaultViewports.push(viewport); } return sanitizedDefaultViewports; } /** * Method that sanitizes custom viewports * @param customViewports Array array of custom viewports */ _sanitizeCustomViewports( customViewports: Array ): Array { let config: ConfigurationObject = this.config, defaultViewports: Array = config.defaultViewports || [], layout: Layout, sanitizedCustomViewports: Array; sanitizedCustomViewports = customViewports.filter(extViewportValidateQuery); // if any viewport is provided without vaid info then raise an error if (sanitizedCustomViewports.length < customViewports.length){ this._logger.error(GridException.viewportAdditionFail); } sanitizedCustomViewports.forEach((viewport: ViewportObject): void => { if ( !isDefined(viewport.minscreenwidth!) || !isDefined(viewport.maxscreenwidth!) ) { if (isDefined(viewport.minscreenwidth!)) { viewport.maxscreenwidth = getContainingViewport( viewport.minscreenwidth!, defaultViewports! )!.maxscreenwidth; } else { viewport.minscreenwidth = getContainingViewport( viewport.maxscreenwidth!, defaultViewports! )!.minscreenwidth; } } layout = (viewport.config && viewport.config.layout) || {}; if (layout.type === LayoutType.Row && !layout.density) { layout.density = LayoutDensityType.Default; } else if (layout.density && !layout.type) { layout.type = LayoutType.Row; } else if (layout.cardtemplate && !layout.type) { layout.type = LayoutType.Card; } }); return sanitizedCustomViewports; } /** * Method that sanitizes modified viewports * @param modifiedViewports Array array of modified viewports */ _sanitizeModifiedViewports( modifiedViewports: Array ): Array { let config: ConfigurationObject = this.config, defaultViewports: Array = config.defaultViewports!, // default viewports defaultConfig: ViewportObject, // default config of current modifiewd viewport viewportMeta: ViewportMap, // meta data of the current modified viewport-> index, prev viewport and next viewport prevModifiedViewport: ViewportObject | false, // modified config of its previous linked viewport (if any) nextModifiedViewport: ViewportObject | false, // modified config of its next linked viewport (if any) prevDefaultViewport: ViewportObject, // previous default viewport nextDefaultViewport: ViewportObject, // next default viewport finalModifiedViewports: Array = [], // final modified and sanitized viewports sortedModifiedViewports: Array; // if no modified viewports are provided by user if (!modifiedViewports.length) { return []; } // sort modified viewports sortedModifiedViewports = modifiedViewports.sort((viewport1, viewport2) => { return ( defaultViewportMap[viewport1.name!].index - defaultViewportMap[viewport2.name!].index ); }); sortedModifiedViewports.forEach((viewport, i) => { viewportMeta = defaultViewportMap[viewport.name!]; defaultConfig = defaultViewports[viewportMeta.index]; prevDefaultViewport = defaultViewports[viewportMeta.index - 1] && defaultViewports[viewportMeta.index - 1]; nextDefaultViewport = defaultViewports[viewportMeta.index + 1] && defaultViewports[viewportMeta.index + 1]; prevModifiedViewport = sortedModifiedViewports[i - 1] && sortedModifiedViewports[i - 1].name === prevDefaultViewport.name && sortedModifiedViewports[i - 1]; nextModifiedViewport = sortedModifiedViewports[i + 1] && sortedModifiedViewports[i + 1].name === nextDefaultViewport.name && sortedModifiedViewports[i + 1]; viewport.config = viewport.config || {}; // if only min screen min width is not provided if (!isDefined(viewport.minscreenwidth!)) { if (prevModifiedViewport && prevModifiedViewport.maxscreenwidth) { viewport.minscreenwidth = prevModifiedViewport.maxscreenwidth + 1; } else { viewport.minscreenwidth = defaultConfig.minscreenwidth; } viewport.config!.layout = { ...defaultConfig.config!.layout, ...viewport.config!.layout }; finalModifiedViewports.push(viewport); // if the next default viewport if not modified and the diffrence between its min value // and the current modified viewport's max value is greater than 1 then adjust the next // viewport's min screen width to make it continuous if ( !nextModifiedViewport && nextDefaultViewport && Math.abs( nextDefaultViewport.minscreenwidth! - viewport.maxscreenwidth! ) > 1 ) { finalModifiedViewports.push({ name: nextDefaultViewport.name, minscreenwidth: viewport.maxscreenwidth! + 1, maxscreenwidth: nextDefaultViewport.maxscreenwidth, config:{ columns: [], layout:nextDefaultViewport.config!.layout } }); } } else if (!isDefined(viewport.maxscreenwidth!)) { // if only max screen min width is not provided if (nextModifiedViewport && nextModifiedViewport.minscreenwidth) { viewport.maxscreenwidth = nextModifiedViewport.minscreenwidth - 1; } else { viewport.maxscreenwidth = defaultConfig.maxscreenwidth; } viewport.config!.layout = { ...defaultConfig.config!.layout, ...viewport.config!.layout }; // if the previous default viewport if not modified and the diffrence between its max value // and the current modified viewport's min value is greater than 1 then adjust the next // viewport's max screen width to make it continuous if ( !prevModifiedViewport && prevDefaultViewport && Math.abs( viewport.minscreenwidth! - prevDefaultViewport.maxscreenwidth! ) > 1 ) { finalModifiedViewports.push({ name: prevDefaultViewport.name, minscreenwidth: prevDefaultViewport.minscreenwidth, maxscreenwidth: viewport.minscreenwidth! - 1, config: { columns: [], layout: prevDefaultViewport.config!.layout } }); } finalModifiedViewports.push(viewport); } else { // if both min and max screen width is provided viewport.config!.layout = { ...defaultConfig.config!.layout, ...viewport.config!.layout }; // if the previous default viewport if not modified and the diffrence between its max value // and the current modified viewport's min value is greater than 1 then adjust the next // viewport's max screen width to make it continuous if ( !prevModifiedViewport && prevDefaultViewport && Math.abs( viewport.minscreenwidth! - prevDefaultViewport.maxscreenwidth! ) > 1 ) { finalModifiedViewports.push({ name: prevDefaultViewport.name, minscreenwidth: prevDefaultViewport.minscreenwidth, maxscreenwidth: viewport.minscreenwidth! - 1, config: { columns: [], layout: prevDefaultViewport.config!.layout } }); } finalModifiedViewports.push(viewport); // if the next default viewport if not modified and the diffrence between its min value // and the current modified viewport's max value is greater than 1 then adjust the next // viewport's min screen width to make it continuous if ( !nextModifiedViewport && nextDefaultViewport && Math.abs( nextDefaultViewport.minscreenwidth! - viewport.maxscreenwidth! ) > 1 ) { finalModifiedViewports.push({ name: nextDefaultViewport.name, minscreenwidth: viewport.maxscreenwidth! + 1, maxscreenwidth: nextDefaultViewport.maxscreenwidth, config: { columns:[], layout: nextDefaultViewport.config!.layout } }); } } }); return finalModifiedViewports; } /** * Method for dynamically adding a new viewport * @param viewport Viewport object */ addViewport(viewport: ViewportObject): void { let config: ConfigurationObject = this.config, existingConfig: ViewportObject, atIndex: number; // check if it is a valid custom viewport if (isCustomViewport(viewport) && extViewportValidateQuery(viewport)) { if ( (existingConfig = config.rawCustomViewports!.filter( (currViewport, index) => { if (currViewport.name === viewport.name) { atIndex = index; return true; } return false; } )[0]) ) { config.rawCustomViewports![atIndex!] = { ...existingConfig, ...viewport }; } else { config.rawCustomViewports!.push(viewport); } config.sanitizedCustomViewports = this._sanitizeCustomViewports( config.rawCustomViewports! ); } else { if ( (existingConfig = config.rawModifiedViewports!.filter( (currViewport, index) => { if (currViewport.name === viewport.name) { atIndex = index; return true; } return false; } )[0]) ) { config.rawModifiedViewports![atIndex!] = { ...existingConfig, ...viewport }; } else { config.rawModifiedViewports!.push(viewport); } config.sanitizedModifiedViewports = this._sanitizeCustomViewports( config.rawModifiedViewports! ); } this._calculateViewport(this.currentScreenWidth); } /** * Method that returns all valid viewports * @returns Array All viewport objects including defaults and customs */ getAllViewports(): Array { let config: ConfigurationObject = this.config; return [ ...config.rawModifiedViewports!, ...config.defaultViewports!.filter(defaultViewport => { // get all default viewports those are not modified return !config.rawModifiedViewports!.filter(modifiedViewport => { return modifiedViewport.name === defaultViewport.name; }).length; }), ...config.rawCustomViewports! ]; } /** * Method to add a follower to viewport manager * @param childName name of the child * @param childClass constructor of the child * @param childConfig configuration of the child * @param callback callback after initialization of the child */ addFollower(childName: string, childClass: Function, childConfig: object, callback?: Function):void { let manager = this, currenViewport:ViewportObject = manager.getCurrentViewport(), _clildClass:any = childClass, child; manager._followers[childName] = child = new _clildClass({ layoutConfig: { ...currenViewport.config!.layout }, ...childConfig }); callback && callback.call(child); } /** * Method that calculates the proper viewport for a given screen width * @param screenWidth the screen width */ _calculateViewport(screenWidth: number): void { let config: ConfigurationObject = this.config, sanitizedDefaultViewports: Array = config.sanitizedDefaultViewports!, sanitizedCustomViewports: Array = config.sanitizedCustomViewports!, sanitizedModifiedViewports: Array = config.sanitizedModifiedViewports!, currViewportObject: ViewportObject | undefined = config.currViewportObject, viewportChanged: boolean = false, followers:FollowerReference = this._followers, derivedViewport: ViewportObject, dispatchEvent: Function = this._dispatchEvent; this._stores.visualUtils.subscribe((util: any) => { dispatchEvent = util.dispatchEvent!; }); // Order of priority: // 1. External custom viewports // 2. External modified viewports // 3. Default viewports derivedViewport = (getContainingViewport( screenWidth, sanitizedCustomViewports! ) || getContainingViewport(screenWidth, sanitizedModifiedViewports!) || getContainingViewport(screenWidth, sanitizedDefaultViewports!))!; // if previously a viewport was derived then check if the viewport has changed and then update respective values if (currViewportObject) { viewportChanged = !isEqualObject(currViewportObject, derivedViewport); viewportChanged && (config.prevViewportObject = config.currViewportObject); } else { // set layout for initial render this.currentLayout = Object.assign({}, config.defaultLayout, derivedViewport.config && derivedViewport.config.layout); } config.currViewportObject = derivedViewport; // recalculate child layouts if viewport has changed if (viewportChanged){ this.currentLayout = Object.assign({}, config.defaultLayout, derivedViewport.config && derivedViewport.config.layout, config.defaultLayout); // dispatch layoutChanged public event dispatchEvent.call(this, 'layoutChanged', { layout: this.currentLayout, viewport: derivedViewport }); // if layout type has changed then dispatch public event if (this.currentLayout !== config.prevViewportObject!.config!.layout!.type){ dispatchEvent.call(this, 'layoutTypeChanged', { layoutType: this.currentLayout.type, prevLayoutType: config.prevViewportObject!.config!.layout!.type, layout: this.currentLayout, viewport: derivedViewport }); } for (let key in followers){ followers[key].recalculateLayout(this.currentLayout); } } config.viewportChanged = viewportChanged; // @todo: fire all public events here with validation } /** * Method for getting current viewport object * @returns ViewportObject current viewport object */ getCurrentViewport(): ViewportObject { return this.config.currViewportObject!; } /** * Returns current screen width * @returns Number current screen width */ getScreenCurrentWidth(): number{ return this.currentScreenWidth; } /** * Method to get the stores */ getStores(): any{ return this._stores; } } export default ViewportManager; export { DEFAULT_VIEWPORTS, ViewportObject, InputConfiguration, ConfigurationObject, ViewportMap, ViewportMaps, DeviceType, OrientationEvents };