/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import ResponsiveSplitter, { $ResponsiveSplitterSettings } from "sap/ui/layout/ResponsiveSplitter"; import SplitterLayoutData from "sap/ui/layout/SplitterLayoutData"; import FixFlex from "sap/ui/layout/FixFlex"; import VBox from "sap/m/VBox"; import SplitPane from "sap/ui/layout/SplitPane"; import PaneContainer from "sap/ui/layout/PaneContainer"; import type SearchModel from "sap/esh/search/ui/SearchModel"; import { Orientation } from "sap/ui/core/library"; import Int from "sap/ui/model/odata/type/Int"; import Control from "sap/ui/core/Control"; import Text from "sap/m/Text"; import { AggregationBindingInfo, PropertyBindingInfo } from "sap/ui/base/ManagedObject"; import UIEvents from "../UIEvents"; import SearchFacetList from "./facets/SearchFacetList"; import { SearchBasketApi } from "./basket/SearchBasketModel"; export interface $SearchLayoutResponsiveSettings extends $ResponsiveSplitterSettings { // properties searchIsBusy: boolean | PropertyBindingInfo; busyDelay: number | PropertyBindingInfo; // facets showFacets: boolean | PropertyBindingInfo; facetPanelResizable: boolean | PropertyBindingInfo; facetPanelWidthInPercent: number | PropertyBindingInfo; facetPanelMinWidth: int | PropertyBindingInfo; facetPanelContent: SearchFacetList | AggregationBindingInfo; animateFacetTransition?: boolean | PropertyBindingInfo; // basket showBasket: boolean | PropertyBindingInfo; basketPanelResizable: boolean | PropertyBindingInfo; basketPanelWidthInPercent: number | PropertyBindingInfo; basketPanelMinWidth: int | PropertyBindingInfo; basketPanelContent: SearchBasketApi | Control | AggregationBindingInfo; animateBasketTransition?: boolean | PropertyBindingInfo; // results resultPanelContent: FixFlex; } /** * @namespace sap.esh.search.ui.controls */ export default class SearchLayoutResponsive extends ResponsiveSplitter { private _previousFacetPanelWidthSize: number; private _previousBasketPanelWidthSize: number; private _paneMainContainer: PaneContainer; private _paneLeft: SplitPane; private _paneMiddle: SplitPane; private _paneRight: SplitPane; private _facetPanelWidthSizeIsOutdated: boolean; private _basketPanelWidthSizeIsOutdated: boolean; private _resultPanelContent: FixFlex; private _facetPanelContent: Control; private _basketPanelContent: Control; static readonly metadata = { properties: { searchIsBusy: { type: "boolean", }, busyDelay: { type: "int", }, // facets showFacets: { type: "boolean", }, facetPanelResizable: { type: "boolean", defaultValue: false, }, facetPanelWidthInPercent: { type: "int", defaultValue: 25, }, facetPanelMinWidth: { type: "int", defaultValue: 288, }, animateFacetTransition: { type: "boolean", defaultValue: false, }, // basket showBasket: { type: "boolean", }, basketPanelResizable: { type: "boolean", defaultValue: true, }, basketPanelWidthInPercent: { type: "int", defaultValue: 100, }, basketPanelMinWidth: { type: "int", defaultValue: 288, }, animateBasketTransition: { type: "boolean", defaultValue: false, }, }, aggregations: { resultPanelContent: { type: "object", multiple: false, }, facetPanelContent: { type: "object", multiple: false, }, basketPanelContent: { type: "object", multiple: false, }, }, }; constructor(sId?: string, options?: Partial<$SearchLayoutResponsiveSettings>) { super(sId, options); // facets const facetsDummyContainer = new VBox("", { items: [new Text()], // dummy for initialization }); this._paneLeft = new SplitPane({ requiredParentWidth: 10, // use minimal width -> single pane mode disabled content: facetsDummyContainer, }); // pane middle, content this._paneMiddle = new SplitPane({ requiredParentWidth: 10, // use minimal width -> single pane mode disabled content: this._resultPanelContent, }); // basket const basketDummyContainer = new VBox("", { items: [new Text()], // dummy for initialization }); this._paneRight = new SplitPane({ requiredParentWidth: 10, // use minimal width -> single pane mode disabled content: basketDummyContainer, }); // facet panel "hidden" this._paneLeft.setLayoutData( new SplitterLayoutData({ size: "0%", // width resizable: false, }) ); // basket panel "hidden" this._paneRight.setLayoutData( new SplitterLayoutData({ size: "0%", // width resizable: false, }) ); // panes this._paneMainContainer = new PaneContainer({ orientation: Orientation.Horizontal, panes: [this._paneLeft, this._paneMiddle, this._paneRight], resize: () => { this.triggerUpdateLayout(); }, }); this._paneMainContainer.setLayoutData( new SplitterLayoutData({ size: "100%", // height resizable: false, }) ); const paneContainer = this._paneMainContainer; this.setRootPaneContainer(paneContainer); this.setDefaultPane(this._paneMiddle); // not used } getFacetPanelContent(): Control { const facetContainer = this._paneLeft; if (facetContainer?.getContent()) { return facetContainer.getContent(); } return undefined; } setFacetPanelContent(oControl: Control): void { this._facetPanelContent = oControl; const facetContainer = this._paneLeft; if (facetContainer) { facetContainer.setContent(oControl); } } getBasketPanelContent(): Control { const basketContainer = this._paneRight; if (basketContainer?.getContent()) { return basketContainer.getContent(); } return undefined; } setBasketPanelContent(oControl: Control): void { if (oControl) { oControl.addStyleClass("sapElisaSearchBasketPanelContent"); this._basketPanelContent = oControl; const basketContainer = this._paneRight; if (basketContainer) { basketContainer.setContent(oControl); } } } setResultPanelContent(oSearchResultPanel: FixFlex): void { this._resultPanelContent = oSearchResultPanel; if (this._paneMiddle) { this._paneMiddle.setContent(oSearchResultPanel); } } setSearchIsBusy(isBusy: boolean): void { const searchModel = this.getModel() as SearchModel; if (!searchModel) { return; } const searchCompositeControl = searchModel.getSearchCompositeControlInstanceByChildControl(this); if (!searchCompositeControl) { return; } searchCompositeControl.setBusyIndicatorDelay(this.getProperty("busyDelay")); searchCompositeControl.setBusy(isBusy); } setShowFacets(showFacets: boolean): SearchLayoutResponsive { if (!this._paneMiddle) { return; } this.updateLayout(showFacets, this.getProperty("showBasket")); this.setProperty("showFacets", showFacets); return this; // return "this" to allow method chaining } setShowBasket(showBasket: boolean): SearchLayoutResponsive { if (!this._paneMiddle) { return; } this.updateLayout(this.getProperty("showFacets"), showBasket); this.setProperty("showBasket", showBasket); return this; // return "this" to allow method chaining } setFacetPanelWidthInPercent(facetPanelWidthInPercentValue: Int): SearchLayoutResponsive { // the 3rd parameter supresses rerendering this.setProperty("facetPanelWidthInPercent", facetPanelWidthInPercentValue, true); // this validates and stores the new value this._facetPanelWidthSizeIsOutdated = true; return this; // return "this" to allow method chaining } /* call from i.e. result view after toolbar changes */ triggerUpdateLayout() { const paneLeftContainerLayoutData = this?._paneLeft.getLayoutData(); const widthString = paneLeftContainerLayoutData.getProperty("size").replace("%", ""); const facetPanelPaneWidth = parseInt(widthString); const facetsAreVisible = facetPanelPaneWidth > 0; let basketIsVisible = false; if (this._basketPanelContent) { const paneRightContainerLayoutData = this?._paneRight?.getLayoutData(); if (paneRightContainerLayoutData) { const basketWidthString = paneRightContainerLayoutData.getProperty("size").replace("%", ""); const basketPanelWidth = parseInt(basketWidthString); basketIsVisible = basketPanelWidth > 0; } else { basketIsVisible = false; } } this.updateLayout(facetsAreVisible, basketIsVisible); } updateLayout(facetsAreVisible: boolean, basketIsVisible: boolean): void { // update facets // adjust the facet content const facetContainer = this._paneLeft; if (facetContainer?.getContent() instanceof VBox) { const vBoxItems = (facetContainer.getContent() as VBox).getItems(); if (vBoxItems?.length > 0 && vBoxItems[0] instanceof Text) { facetContainer.setContent(this._facetPanelContent); } } // animation if (this._facetPanelContent) { // robustness when triggered by constructor if (this.getProperty("animateFacetTransition")) { this._facetPanelContent.addStyleClass("sapUshellSearchFacetAnimation"); } else { this._facetPanelContent.removeStyleClass("sapUshellSearchFacetAnimation"); } } // facets width / splitter position if (facetContainer?.getContent()) { let currentFacetPanelWidthSize; const paneLeftContainerLayoutData = this?._paneLeft.getLayoutData(); if (!facetsAreVisible) { facetContainer.getContent().setVisible(false); facetContainer.setLayoutData( new SplitterLayoutData({ size: "0%", // width minSize: 0, resizable: false, }) ); } else { facetContainer.getContent().setVisible(true); const oModel = this.getModel() as SearchModel; let facetPanelMinWidth = this.getProperty("facetPanelMinWidth"); if (oModel.config.optimizeForValueHelp) { currentFacetPanelWidthSize = 0; facetPanelMinWidth = 0; } else if (this._facetPanelWidthSizeIsOutdated) { currentFacetPanelWidthSize = this.getProperty("facetPanelWidthInPercent"); this._facetPanelWidthSizeIsOutdated = false; } else { currentFacetPanelWidthSize = parseInt( paneLeftContainerLayoutData.getProperty("size").replace("%", "") ); if (currentFacetPanelWidthSize < 1) { if (this._previousFacetPanelWidthSize) { currentFacetPanelWidthSize = this._previousFacetPanelWidthSize; } else { currentFacetPanelWidthSize = this.getProperty("facetPanelWidthInPercent"); } } } this._paneLeft.setLayoutData( new SplitterLayoutData({ size: currentFacetPanelWidthSize + "%", minSize: facetPanelMinWidth, resizable: this.getProperty("facetPanelResizable"), }) ); this._previousFacetPanelWidthSize = currentFacetPanelWidthSize; // remember width to restore when showing facets (after having closed them before) } } // update basket // adjust the basket content const basketContainer = this._paneRight; if (basketContainer?.getContent() instanceof VBox) { const vBoxItems = (basketContainer.getContent() as VBox).getItems(); if (vBoxItems?.length > 0 && vBoxItems[0] instanceof Text) { if (this._basketPanelContent) { basketContainer.setContent(this._basketPanelContent); } } } // animation if (this._basketPanelContent) { // robustness when triggered by constructor if (this.getProperty("animateBasketTransition")) { this._basketPanelContent.addStyleClass("sapUshellSearchBasketAnimation"); } else { this._basketPanelContent.removeStyleClass("sapUshellSearchBasketAnimation"); } } // basket width / splitter position if (basketContainer?.getContent()) { let currentBasketPanelWidthSize; if (!basketIsVisible) { basketContainer.getContent().setVisible(false); basketContainer.setLayoutData( new SplitterLayoutData({ size: "0%", // width minSize: 0, resizable: false, }) ); currentBasketPanelWidthSize = 0; } else { basketContainer.getContent().setVisible(true); const oModel = this.getModel() as SearchModel; let basketPanelMinWidth = this.getProperty("basketPanelMinWidth"); if (oModel.config.optimizeForValueHelp) { currentBasketPanelWidthSize = 0; basketPanelMinWidth = 0; } else if (this._basketPanelWidthSizeIsOutdated) { currentBasketPanelWidthSize = this.getProperty("basketPanelWidthInPercent"); this._basketPanelWidthSizeIsOutdated = false; } else { const paneRightContainerLayoutData = basketContainer?.getLayoutData(); if (paneRightContainerLayoutData) { currentBasketPanelWidthSize = parseInt( paneRightContainerLayoutData.getProperty("size").replace("%", "") ); } else { currentBasketPanelWidthSize = 0; } if (currentBasketPanelWidthSize < 1) { if (this._previousBasketPanelWidthSize) { currentBasketPanelWidthSize = this._previousBasketPanelWidthSize; } else { currentBasketPanelWidthSize = this.getProperty("basketPanelWidthInPercent"); } } } basketContainer.setLayoutData( new SplitterLayoutData({ size: currentBasketPanelWidthSize + "%", minSize: basketPanelMinWidth, resizable: this.getProperty("basketPanelResizable"), }) ); this._previousBasketPanelWidthSize = currentBasketPanelWidthSize; // remember width to restore when showing basket (after having closed it before) } const model = this.getModel() as SearchModel; model?.setBasketPanelWidthPercent(currentBasketPanelWidthSize ?? 0); } else { (this.getModel() as SearchModel)?.setBasketPanelWidthPercent(0); } this._paneMainContainer.setLayoutData( new SplitterLayoutData({ size: "100%", resizable: false, }) ); const handleAnimationEnd = () => { (this.getModel() as SearchModel).notifySubscribers(UIEvents.ESHSearchLayoutChanged, null); }; const searchFacets = document.querySelector(".sapUiFixFlexFixed"); if (searchFacets) { const onTransitionEnd = () => { handleAnimationEnd(); searchFacets.removeEventListener("transitionend", onTransitionEnd); }; searchFacets.addEventListener("transitionend", onTransitionEnd); } } static renderer = { apiVersion: 2, }; }