import { Layout, DerivedColumnOptions, DomContainerProps, LayoutDensityType, DerivedRowOptions, LayoutType, ColumnType, templateFn, RowSelection } from '../public-api/interfaces'; import { parseNumber, parseBoolean } from '../globals/helpers/helpers'; import { InlinechartDimState, getInlineChartDimension } from '../utils/managers/inline-chart-space-manager'; import { densityType, layoutDensity, validLayoutType, DENSITY_TO_PX_MAP, DEFAULT_ROW_HEIGHT, DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_LEFT_MARGIN, DEFAULT_ROW_RIGHT_MARGIN, DEFAULT_CARD_WIDTH, DEFAULT_CARD_PADDING, DEFAULT_CARD_INFO_ROW_HEIGHT_CARD, CARD_TEMPLATE_ONE_CONFIG, DEFAULT_CARD_CONTAINER_LEFT_PADDING, DEFAULT_CARD_CONTAINER_RIGHT_PADDING, CardTemplatePos, LayoutObject, VizRecordDomainConfig, LayoutState, LayoutInputConfig, RowConfig, CardConfig, StoreUnsubscribeFn, CellDimState, RowDimState, CardCellState, DEFAULT_CHECKBOX_COLUMN_WIDTH, getMinColumnWidth, columnIndexWithMinMaxContent } from '../globals/helpers/helpers'; import { mergeDeep, getSpanDimension } from '../utils/toolbox/src'; import { isIE11, isEdge } from "../utils/toolbox/src/browsers/browser-type"; /** * @description used to parse given layout type * @param type layout type * @returns valid layout * @since 0.0.1 */ function parseLayout(type: string) { let layoutInput: LayoutType = type.toLowerCase(); if (layoutInput === LayoutType.Card) { return LayoutType.Card; } return LayoutType.Row; } /** * @description used to parse given density * @param density density string to be considered eg. default , comfortable , compact * @returns valid density string * @since 0.0.1 */ function parseDensity(density: densityType) { let density_lower: densityType = density.toLowerCase(); if (DENSITY_TO_PX_MAP[density_lower]) { return density_lower; } return LayoutDensityType.Default; } /** * @class LayoutManager * @classdesc LayoutManager class consumes user provided columnsConfig, * rowConfig, layout definitions and calculate dimentation for the grid/card * layout. * @since 0.0.1 */ class LayoutManager { // class Variable declarations private layout: LayoutState = {}; private _layoutObject: Layout; private _stores: any; private defaultDensityToPxMap: layoutDensity; private defaultColumnWidth: number; private rowConfig: RowConfig = {}; private density: densityType = LayoutDensityType.Default; private currentLayoutType: validLayoutType; private domContainerDim: DomContainerProps; private cardConfig: CardConfig = {}; private columnsConfig: Array; private selectionConfig: RowSelection; private storeUnsubscribeFn: StoreUnsubscribeFn = {}; private vizRecDomain: VizRecordDomainConfig = {}; private shouldRecalculateLayout: boolean = false; private defaultColumnOptions: any; private groupLevel: any; // autoHeight: boolean; // layout: string; /** * @constructor * @param config type layoutInputConfig configurations that need to be passed * to instantiate layout manager */ constructor(config: LayoutInputConfig) { let layoutConf: Layout = config.layoutConfig || {}, rowOptionsConf: DerivedRowOptions = config.rowOptionsConfig || {}, rowConf: RowConfig = this.rowConfig, cardConf: CardConfig = this.cardConfig; this._layoutObject = layoutConf; this.columnsConfig = config.columnsConfig || []; this.groupLevel = config.groupLevel; this.selectionConfig = config.selectionConfig || {}; this.defaultDensityToPxMap = DENSITY_TO_PX_MAP; this.defaultColumnWidth = DEFAULT_COLUMN_WIDTH; this._stores = config.stores; // setting layout type and density this.currentLayoutType = layoutConf.type ? parseLayout(layoutConf.type) : LayoutType.Row; this.domContainerDim = config.domContainerDim; this.defaultColumnOptions = config.defaultColumnOptions; // row configurations rowConf.defaultRowHeight = DEFAULT_ROW_HEIGHT; this.density = layoutConf.density ? parseDensity(layoutConf.density) : LayoutDensityType.Default; rowConf.headerRowHeight = rowOptionsConf.pxHeaderRowHeight ? rowOptionsConf.pxHeaderRowHeight : rowConf.defaultRowHeight; rowConf.rowHeight = rowOptionsConf.pxRowHeight ? rowOptionsConf.pxRowHeight : rowConf.defaultRowHeight; rowConf.densedHeaderRowHeight = this.calculateRowHeight(rowConf.headerRowHeight); rowConf.densedRowHeight = this.calculateRowHeight(rowConf.rowHeight); // card configuration cardConf.defaultCardWidth = DEFAULT_CARD_WIDTH; cardConf.defaultRowLeftMargin = DEFAULT_ROW_LEFT_MARGIN; cardConf.defaultRowRightMargin = DEFAULT_ROW_RIGHT_MARGIN; cardConf.defaultCardPadding = DEFAULT_CARD_PADDING; cardConf.numCards = parseNumber(layoutConf.numcards, this.cardConfig.defaultNumCards); cardConf.cardtemplate = layoutConf.cardtemplate; // TO-DO , to be replaced when row left, right margin and card padding will be introduced // cardConf.rowLeftMargin = layoutConf.rowLeftMargin ? layoutConf.rowLeftMargin : this.cardConfig.defaultNumCards; // cardConf.rowRightMargin = layoutConf.rowRightMargin ? layoutConf.rowRightMargin : this.cardConfig.defaultNumCards; // cardConf.cardPadding = layoutConf.cardPadding ? layoutConf.cardPadding : this.cardConfig.defaultNumCards; cardConf.rowLeftMargin = this.cardConfig.defaultRowLeftMargin; cardConf.rowRightMargin = this.cardConfig.defaultRowRightMargin; cardConf.cardPadding = this.cardConfig.defaultCardPadding; cardConf.cardTempPlateConfig = CARD_TEMPLATE_ONE_CONFIG; this.storeUnsubscribeFn.vizRecDomainUnsub = this._stores.vizRecordDomain.subscribe((tmpVizRecDom: any) => { this.vizRecDomain = tmpVizRecDom; this.calculatelayout(); }); //setting autoHeight if enabled parseBoolean(layoutConf.autoheight) ? this._stores.autoHeight.set(true): this._stores.autoHeight.set(false); } /** * @description calculates row height when line height provided w.r.t. current density * @param rowHeight rowHeight to be considered for rows * @returns calcuated rowHeight */ calculateRowHeight(rowHeight: number): number { return this.density ? rowHeight + (2 * this.defaultDensityToPxMap[this.density]) : rowHeight; } setHeaderRowHeight (headerRowHeight: number) { if (headerRowHeight !== this.rowConfig.headerRowHeight){ this.shouldRecalculateLayout = true; this.rowConfig.headerRowHeight = headerRowHeight; this.setDensedHeaderRowHeight(); } } setBodyRowHeight (bodyRowHeight: number) { if (bodyRowHeight !== this.rowConfig.rowHeight){ this.shouldRecalculateLayout = true; this.rowConfig.rowHeight = bodyRowHeight; this.setDensedBodyRowHeight(); } } /** * Method to set both header and body row height * @param rowHeight the row height */ setRowHeight(rowHeight: number): void{ this.setHeaderRowHeight(rowHeight); this.setBodyRowHeight(rowHeight); } /** * @description function that calcluates header row height * @param headerRowHeight contains number value that is considered as a line heigt of content for headers */ setDensedHeaderRowHeight(): void { let headerRowHeight: number = this.getHeaderRowHeight(); this.rowConfig.densedHeaderRowHeight = this.calculateRowHeight(headerRowHeight); } /** * Getter for densed header row height * @returns Number densed header row height */ getDensedHeaderRowHeight(): number { return this.rowConfig.densedHeaderRowHeight; } /** * @description getter method for Header row Height * @returns header Row Height calculated * */ getHeaderRowHeight(): number { return this.rowConfig.headerRowHeight; } /** * @description function that calcluates row height * @param rowHeight contains number value that is considered as a line heigt of content for rows */ setDensedBodyRowHeight(): void { let rowHeight: number = this.getRowHeight(); this.rowConfig.densedRowHeight = this.calculateRowHeight(rowHeight); } /** * Getter for densed body row height * @returns Number densed body row height */ getDensedBodyRowHeight(): number { return this.rowConfig.densedRowHeight; } /** * Method to set row selection config * @param configObj the config object */ setSelectionConfig(configObj: RowSelection){ let selectionConfig: RowSelection = this.selectionConfig, prevConfig = { ...selectionConfig }, isEnableChanged: boolean, isShowCheckBoxChanged: boolean, shouldRecalculateLayout = false; Object.assign(selectionConfig, configObj); isEnableChanged = prevConfig.enable !== selectionConfig.enable; isShowCheckBoxChanged = prevConfig.enableselectioncheckbox !== selectionConfig.enableselectioncheckbox; /** * recalculate layout for the given scenarios * 1. selection enable changed, when enable set to true, and enableselectioncheckbox is changed or not changed but prev value was true * 2. selection enable changed, enable set to false and previously enableselectioncheckbox was true * 3. enable not changed, and still true, enableselection checkbox values set to true as well */ if (isEnableChanged) { if (selectionConfig.enable && selectionConfig.enableselectioncheckbox) { shouldRecalculateLayout = true; } else if (!selectionConfig.enable && prevConfig.enableselectioncheckbox) { shouldRecalculateLayout = true; } } else { if (selectionConfig.enable && isShowCheckBoxChanged) { shouldRecalculateLayout = true; } } this.shouldRecalculateLayout = shouldRecalculateLayout; } /** * @description getter method for row Height * @returns Row Height calculated * */ getRowHeight(): number { return this.rowConfig.rowHeight; } getRowConfig(): RowConfig{ return this.rowConfig; } getCardConfig(): CardConfig{ return this.cardConfig; } setRowOptions(option: string, value: any){ switch(option){ case 'pxRowHeight': this.setBodyRowHeight(value); break; case 'pxHeaderRowHeight': this.setHeaderRowHeight(value); break; case 'selection': this.setSelectionConfig(value); break; default: return; } if (this.shouldRecalculateLayout){ this.calculatelayout(); this.shouldRecalculateLayout = false; } } /** * Method to get corresponding row option * @param option row option */ getRowOption(option: any): any{ switch(option){ case 'rowheight': return this.getRowHeight(); case 'headerrowheight': return this.getHeaderRowHeight(); case 'selection': return this.selectionConfig; default: return; } } /** * @description getter method for current layout type * @returns layout type calculated */ getCurrentLayoutType(): validLayoutType { return this.currentLayoutType; } /** * @description setter method for current layout type * @param layoutType valid layout type like row, card */ setCurrentLayoutType(layoutType: validLayoutType): void { this.currentLayoutType = layoutType; } /** * Setter API for num cards * @param numcards num cards */ setNumCards(numcards: number){ this.cardConfig.numCards = parseNumber(numcards, this.cardConfig.defaultNumCards); } /** * Getter API for num cards * @returns number of cards for card layout */ getNumCards(): number{ return this.cardConfig.numCards; } /** * Setter API for card template * @param templeFn num cards */ setCardTemplate(templeFn: templateFn){ this.cardConfig.cardtemplate = templeFn; } /** * Getter API for card template * @returns current card template for card layout */ getCardTemplate(): templateFn{ return this.cardConfig.cardtemplate!; } /** * Setter for current layout * @param layoutObj */ setLayout(layoutObj: Layout){ let currentLayOutObj: Layout = this._layoutObject; Object.assign(currentLayOutObj, layoutObj); this.setCurrentLayoutType(currentLayOutObj.type!); this.setDensity(currentLayOutObj.density!); this.recalculateDensedRowHeights(); this.setNumCards(currentLayOutObj.numcards!); this.setCardTemplate(currentLayOutObj.cardtemplate!); this.calculatelayout(); } /** * Getter for current layout */ getLayout():Layout{ return this._layoutObject; } /** * @description getter method of density property * @returns current density property set */ getDensity(): densityType | undefined { return this.density; } /** * Method for recalculating densed header and body row heights */ recalculateDensedRowHeights(){ this.setDensedHeaderRowHeight(); this.setDensedBodyRowHeight(); } /** * @description sets current density * @param density valid density type like default, compact, comfortable */ setDensity(density: densityType): void { if (DENSITY_TO_PX_MAP[density]) { this.density = density; } } /** * @description getter method for domContainer property * @returns current domContainer dimnetions */ getDomContainerDim (): DomContainerProps { return this.domContainerDim; } /** * @description setter method for domcontainer dimnentions * @param dim dimention to be considered for domcontainer */ setDomContainerDim (dim: DomContainerProps): void { this.domContainerDim = dim; } /** * @description getter method for columnsConfig property */ getColumnsConfig (): Array { return this.columnsConfig; } /** * @description setter method for columnsConfig property * @param columnsConfig holds the configuration to set for columns */ setColumnsConfig (columnsConfig: Array): void { this.columnsConfig = columnsConfig; } /** * @description method to calculate layout for grid/card. Generates LayoutState which * consists of headerDimState and rowState (to hold dimnetional information for header and rows). * */ calculatelayout(): void { let columnsConfig: Array = this.columnsConfig, selectionConf: RowSelection = this.selectionConfig, rowHeight: number = this.rowConfig.rowHeight, cellLeft: number = 0, cellDimState: Array = [], rowDimState: Array = [], totalWidth: number = 0, totalBodyHeight: number = 0, searchable = this.defaultColumnOptions && this.defaultColumnOptions.searchable, isSearchEnabled = typeof searchable === 'boolean' ? searchable : searchable && searchable.enable, headerContainerHeight: any; if (this.currentLayoutType !== LayoutType.Card) { /** * Width calculation for starting cells of a column, factors considered width, minwidth, maxWIdth * and defaultWidth specified by us. * columnsConfig: { * width: val, * minwidth: val, * maxwidth: val * } * ignoring all factors and setting with if width is provided in the config * if width not provided but our default width falls under user specified min and maxwidth then * considering the defaultWidth, else updating currentWIdth based on min and max Width */ //if selection enabled, adding layout spae for selection checkbox column in the begining if (selectionConf.enable && selectionConf.enableselectioncheckbox) { cellDimState.push({ left: 0, width: DEFAULT_CHECKBOX_COLUMN_WIDTH }); cellLeft+= DEFAULT_CHECKBOX_COLUMN_WIDTH; } for (let index = 0; index < columnsConfig.length; index++) { const columnConfig: DerivedColumnOptions = columnsConfig[index]; let currColWidth: number = columnConfig.pxWidth || this.defaultColumnWidth, cellDim: CellDimState; cellDim = { left: cellLeft, width: currColWidth }; totalWidth += currColWidth; if (columnConfig.type === ColumnType.Chart) { cellDim.inlinechartDim = getInlineChartDimension(columnConfig, currColWidth, this.rowConfig.densedRowHeight); } cellDimState.push(cellDim); cellLeft += currColWidth; } /** * incrementally setting row top and height for visible rows */ for (let index = 0; index <= this.vizRecDomain.end - this.vizRecDomain.start; index++) { rowDimState.push({ top: (rowDimState.length === 0) ? 0 : rowDimState[index - 1].top + rowDimState[index - 1].height, height: this.rowConfig.densedRowHeight }); totalBodyHeight += this.rowConfig.densedRowHeight; } headerContainerHeight = this.rowConfig.densedHeaderRowHeight; if(isSearchEnabled){ headerContainerHeight += this.rowConfig.densedHeaderRowHeight; // eslint-disable-next-line no-undefined } if(this.groupLevel !== undefined && Number(this.groupLevel)){ headerContainerHeight += this.groupLevel * this.rowConfig.densedHeaderRowHeight; } this.layout = { rowLayout: { headerDimState: { height: headerContainerHeight, cell: cellDimState }, totalBodyHeight, totalWidth:cellLeft, rowDimState } }; } else { let cardConf: CardConfig = this.cardConfig, cardTempPlateConfig: CardTemplatePos = cardConf.cardTempPlateConfig, finalNoOfCards: number, finalCardWidth: number, finalCardHeight: number, cardCellState:Array= [], remainingContainerWidth: number = this.domContainerDim.width - cardConf.rowLeftMargin - cardConf.rowRightMargin - ((isIE11 || isEdge) ? 19 : 0); /** * width calculation for card layout */ // if noCards specified if (cardConf.numCards) { remainingContainerWidth = remainingContainerWidth - cardConf.cardPadding * (cardConf.numCards - 1); finalNoOfCards = cardConf.numCards; } else if (remainingContainerWidth <= cardConf.defaultCardWidth) { finalNoOfCards = 1; } else { // numCards is not specified calculate cardwidth spmartly let tmpCardCount: number = remainingContainerWidth / cardConf.defaultCardWidth, cardRoundOf:number = Math.round(tmpCardCount), remainingWidhExcludingPadding:number = remainingContainerWidth - ((cardRoundOf - 1) * cardConf.cardPadding), tmpCardCountExcludingPadding:number = remainingWidhExcludingPadding / cardConf.defaultCardWidth; if ((tmpCardCount % 1) >= 0.5 && (tmpCardCountExcludingPadding % 1) < 0.5) { finalNoOfCards = Math.round(tmpCardCountExcludingPadding); remainingContainerWidth -= ((Math.round(tmpCardCountExcludingPadding) - 1) * cardConf.cardPadding); } else { remainingContainerWidth = remainingWidhExcludingPadding; finalNoOfCards = cardRoundOf; } } finalCardWidth = remainingContainerWidth/finalNoOfCards; for (let index:number = 0; index < columnsConfig.length; index++) { let inlinechartDim: InlinechartDimState ={}; const columnConfig = columnsConfig[index]; if (columnConfig.type === 'chart') { inlinechartDim = getInlineChartDimension(columnConfig, (finalCardWidth - DEFAULT_CARD_CONTAINER_LEFT_PADDING - DEFAULT_CARD_CONTAINER_RIGHT_PADDING) * 0.6, DEFAULT_CARD_INFO_ROW_HEIGHT_CARD); } cardCellState.push({ width: finalCardWidth * 0.6, height: DEFAULT_CARD_INFO_ROW_HEIGHT_CARD, inlinechartDim }); } finalCardHeight = cardTempPlateConfig.cardTopPadding + cardTempPlateConfig.cardBottomPadding + cardTempPlateConfig.cardBorderTop + cardTempPlateConfig.cardBorderBottom + ((cardTempPlateConfig.cardInfoRowTopPadding + cardTempPlateConfig.cardInfoRowBottomPadding + cardTempPlateConfig.cardInfoRowHeight) * columnsConfig.length); this.layout = { cardLayout: { width: finalCardWidth, height: finalCardHeight, numCards: finalNoOfCards, paddingBetweenCards: cardConf.cardPadding, startPadding: cardConf.rowLeftMargin, cardtemplate: cardConf.cardtemplate, cellState: cardCellState } }; } this._stores.layoutObject.update((tmpLayoutObject: LayoutObject)=> { tmpLayoutObject.layout = this._layoutObject; tmpLayoutObject.layoutState = this.getLayoutState(); return tmpLayoutObject; }); } /** * @description returns current Layout state holding calculated dimnetional information * @returns current Layout state holding calculated dimnetional information */ getLayoutState(): LayoutState { return this.layout; } /** * @description function to apply sizetofit api support * @param index columnIndex , from the column we are starting sizeToFit operation * @param length length of the column * @param remainingWidth remaining width to be allocated */ sizeColumnsToFit (columnIndex: number = 0, length: number, remainingWidth: number = 0): boolean { let tmpRemainingWidth: number = remainingWidth, columnsConfig: DerivedColumnOptions[] = mergeDeep(this.columnsConfig); for (let index = columnIndex; index < length; index++) { let columnConfig:DerivedColumnOptions = columnsConfig[index], currColWidth: number, currColumnMinWidth: number | undefined, currColumnMaxWidth: number | undefined; if (tmpRemainingWidth < getMinColumnWidth()) return false; currColWidth = tmpRemainingWidth /(length - index); currColumnMinWidth = columnConfig.pxMinWidth, currColumnMaxWidth = columnConfig.pxMaxWidth; // ignoring sizetofit api for rich text and inline chart columns if (columnConfig.type === ColumnType.HTML || columnConfig.type === ColumnType.Chart) { tmpRemainingWidth -= columnConfig.pxWidth; } else { // finding columnwidth comparing with currColumnMinWidth, currColumnMaxWidth, validating whether that is defined // using -Infinity and +infinity. currColWidth = Math.min(Math.max(currColWidth, currColumnMinWidth || -Infinity), currColumnMaxWidth || Infinity); columnConfig.pxWidth = currColWidth; tmpRemainingWidth -= currColWidth; } } this.setColumnsConfig(columnsConfig); this.calculatelayout(); return true; } sizeColumnsToContent(columnsOptions: columnIndexWithMinMaxContent[], document: Document, container: HTMLElement) { let columnsConfig: DerivedColumnOptions[] = mergeDeep(this.columnsConfig), shouldCalculateLayout: boolean = false; for (let index = 0; index < columnsOptions.length; index++) { let inputColumn = columnsOptions[index], maxWidth, columnConfig = columnsConfig[inputColumn.columnIndex]; // ignoring for columntype chart and rich text if(columnConfig.type === ColumnType.HTML || columnConfig.type === ColumnType.Chart) continue; maxWidth = getSpanDimension(document, container, {}, inputColumn.content.max).width; if (maxWidth) { columnConfig.pxWidth = maxWidth + 32; shouldCalculateLayout = true; } } if (shouldCalculateLayout) { this.setColumnsConfig(columnsConfig); this.calculatelayout(); } } /** * Method to get the stores */ getStores(): any{ return this._stores; } } export default LayoutManager;