/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import SearchCompositeControl from "../SearchCompositeControl"; import SearchModel from "../SearchModel"; import SearchFieldGroup from "../controls/searchfieldgroup/SearchFieldGroup"; import SearchFilterBar from "sap/esh/search/ui/controls/SearchFilterBar"; import SearchNlqExplainBar from "sap/esh/search/ui/controls/SearchNlqExplainBar"; import UIEvents, { IUiEventParametersResultViewSelectionChanged, IUiEventParametersResultViewTypeChanged, IUiEventParametersShowResultDetail, IUiEventParametersBasketChanged, IUiEventParametersPublicModelChanged, } from "../UIEvents"; import Control from "sap/ui/core/Control"; import Element from "sap/ui/core/Element"; import * as SearchHelper from "sap/esh/search/ui/SearchHelper"; import { SearchBasketApi } from "../controls/basket/SearchBasketModel"; import OverflowToolbar from "sap/m/OverflowToolbar"; import OverflowToolbarLayoutData from "sap/m/OverflowToolbarLayoutData"; import { OverflowToolbarPriority, ToolbarDesign, ButtonType, PlacementType, BarDesign, FlexJustifyContent, } from "sap/m/library"; import ToolbarSpacer from "sap/m/ToolbarSpacer"; import Button from "sap/m/Button"; import BindingMode from "sap/ui/model/BindingMode"; import Bar from "sap/m/Bar"; import VBox from "sap/m/VBox"; import VerticalLayout from "sap/ui/layout/VerticalLayout"; import FixFlex from "sap/ui/layout/FixFlex"; import SearchLayoutResponsive, { $SearchLayoutResponsiveSettings, } from "sap/esh/search/ui/controls/SearchLayoutResponsive"; import SearchResultContainer from "../controls/resultview/SearchResultContainer"; import SearchCountBreadcrumbs from "../controls/resultview/SearchCountBreadcrumbs"; import SearchSelectionBar from "../controls/SearchSelectionBar"; import SearchFacetList from "../controls/facets/SearchFacetList"; import MessagePopover from "sap/m/MessagePopover"; import MessageItem from "sap/m/MessageItem"; import IllustratedMessage, { $IllustratedMessageSettings } from "sap/m/IllustratedMessage"; import IllustratedMessageType from "sap/m/IllustratedMessageType"; import IllustratedMessageSize from "sap/m/IllustratedMessageSize"; import ViewSettingsDialog from "sap/m/ViewSettingsDialog"; import ViewSettingsItem from "sap/m/ViewSettingsItem"; import IconPool from "sap/ui/core/IconPool"; import i18n from "../i18n"; import errors, { ProgramError } from "../error/errors"; import { callConfigExit } from "../error/callConfigExit"; import { ComplexCondition } from "../sinaNexTS/sina/ComplexCondition"; import { SelectionMode } from "../SelectionMode"; import SearchBasketSapMList, { $SearchBasketSapMListSettings } from "../controls/basket/SearchBasketSapMList"; import SearchBasketSapMTable, { $SearchBasketSapMTableSettings, } from "../controls/basket/SearchBasketSapMTable"; import SearchBasketSapUiTable, { $SearchBasketSapUiTableSettings, } from "../controls/basket/SearchBasketSapUiTable"; import { TechnicalEventType } from "../eventlogging/TechnicalEvents"; import { convertPixelToRem } from "../uiHelper"; import { openShowMoreDialog } from "../controls/OpenShowMoreDialog"; import Log from "sap/base/Log"; let messagePopoverId = 1; export default class SearchPageAssembler { private compositeControl: SearchCompositeControl; private readonly oLogger = Log.getLogger("sap.esh.search.ui.compositecontrol.SearchPageAssembler"); constructor(compositeControl: SearchCompositeControl) { this.compositeControl = compositeControl; } private subscribeToModelEvents(): void { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; if (oModel?.subscribe) { // search started if (!cc.subscribeDone_SearchStarted) { oModel.subscribe(UIEvents.ESHSearchStarted, cc.onAllSearchStarted, cc); cc.subscribeDone_SearchStarted = true; } // search finished if (!cc.subscribeDone_SearchFinished) { oModel.subscribe(UIEvents.ESHSearchFinished, cc.onAllSearchFinished, cc); cc.subscribeDone_SearchFinished = true; } // selection changed if (!cc.subscribeDone_SelectionChanged) { oModel.subscribe(UIEvents.ESHSelectionChanged, cc.onSelectionChanged, cc); cc.subscribeDone_SelectionChanged = true; } // result view type changed if (!cc.subscribeDone_ResultViewTypeChanged) { oModel.subscribe(UIEvents.ESHResultViewTypeChanged, cc.onResultViewTypeChanged, cc); cc.subscribeDone_ResultViewTypeChanged = true; } // show result detail if (!cc.subscribeDone_ShowResultDetail) { oModel.subscribe(UIEvents.ESHShowResultDetail, cc.onShowResultDetail, cc); cc.subscribeDone_ShowResultDetail = true; } // basket changed if (!cc.subscribeDone_BasketChanged) { oModel.subscribe(UIEvents.ESHBasketChanged, cc.onBasketChanged, cc); cc.subscribeDone_BasketChanged = true; } // public model changed if (!cc.subscribeDone_PublicModelChanged) { oModel.subscribe(UIEvents.ESHPublicModelChanged, cc.onPublicModelChanged, cc); cc.subscribeDone_PublicModelChanged = true; } } } onAfterRendering(): void { this.subscribeToModelEvents(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars onAllSearchStarted(eventId: string, parameters: unknown): void { this.compositeControl.fireEvent("searchStarted"); } // eslint-disable-next-line @typescript-eslint/no-unused-vars onAllSearchFinished(eventId: string, parameters: unknown): void { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; cc.assignDragDropConfig(); // reassign drag&drop config (result view regenerated/switched) cc.chooseNoResultScreen(); // there can be custom no-result-screens, depending on data source cc.oFocusHandler.setFocus(); // the search-app container of FLP has ID "viewPortContainer" if (oModel.config.isUshell) { const viewPortContainer = Element.getElementById("viewPortContainer") as { switchState?: (state: string) => void; } | null; // sap.ushell.ui.AppContainer if (viewPortContainer?.switchState) { viewPortContainer.switchState("Center"); } } cc.fireEvent("searchFinished"); // restore selection based on basket items if (oModel.config.basketLinkByResultViewItemSelection) { if (cc.oSearchBaskets.length > 0) { for (const basket of cc.oSearchBaskets) { const oSearchBasketModel = (basket as SearchBasketApi).getBasketModel(); oSearchBasketModel.syncResultViewSelection( cc.getPublicSearchModel().getProperty("/results/items"), true ); } } } // optimize layout (css) setTimeout(() => { // search toolbar ('overflow' of buttons at the right) cc.adjustSearchbarCustomGenericButtonWidth(); }, 0); } onSelectionChanged(eventId: string, parameters: IUiEventParametersResultViewSelectionChanged): void { const cc = this.compositeControl; if (cc.oSearchBaskets.length > 0) { for (const basket of cc.oSearchBaskets) { const oSearchBasketModel = (basket as SearchBasketApi).getBasketModel(); if (oSearchBasketModel?.getProperty("/_skipSelectionChange")) { continue; } const oModel = cc.getModelInternal() as SearchModel; if ( oModel.config.basketLinkByResultViewItemSelection && (oModel.getProperty("/basketPanelVisibility") || cc.oSearchBaskets.length > 0) ) { try { // update basket items oSearchBasketModel.updateBasketItems( cc.getPublicSearchModel().getProperty("/results/items") ); oModel.setProperty("/basketCount", oSearchBasketModel.getProperty("/rows").length); } catch (oError) { cc.errorHandler.onError(oError); } } } } cc.fireEvent("selectionChanged"); // deprecated as of version 1.143.0, use 'resultViewSelectionChanged' instead cc.fireEvent("resultViewSelectionChanged", parameters); } onResultViewTypeChanged(eventId: string, parameters: IUiEventParametersResultViewTypeChanged): void { this.compositeControl.fireEvent("resultViewTypeChanged", parameters); } onShowResultDetail(eventId: string, parameters: IUiEventParametersShowResultDetail): void { const cc = this.compositeControl; const publicItem = cc .getSearchResultSet() .items.filter((item) => item.key === parameters.resultItemKey)[0]; const publicItemClone = Object.assign({}, publicItem); // avoid side effects cc.fireEvent("showResultDetail", { publicItem: publicItemClone }); } onBasketChanged(eventId: string, parameters: IUiEventParametersBasketChanged): void { this.compositeControl.fireEvent("basketChanged", parameters); } onPublicModelChanged(eventId: string, parameters: IUiEventParametersPublicModelChanged): void { const cc = this.compositeControl; // some public model changes need rerendering updates // - selected (checkboxes enabled or disabled) // - selectionEnabled (checkboxes enabled or disabled) // - customItemStyleClass (item style class changed) if (parameters.changedPropertyIds.includes("selected")) { SearchHelper.updateResultViewSelected(cc.searchResultList); SearchHelper.updateResultViewSelected(cc.searchResultTable); SearchHelper.updateResultViewSelected(cc.searchResultGrid); } if (parameters.changedPropertyIds.includes("selectionEnabled")) { SearchHelper.updateResultViewSelectionEnablement( cc.searchResultList, cc.getResultviewSelectionMode() === SelectionMode.OneItem ); SearchHelper.updateResultViewSelectionEnablement( cc.searchResultTable, cc.getResultviewSelectionMode() === SelectionMode.OneItem ); SearchHelper.updateResultViewSelectionEnablement( cc.searchResultGrid, cc.getResultviewSelectionMode() === SelectionMode.OneItem ); } if (parameters.changedPropertyIds.includes("attributeData")) { // invalidate result view bindings cc.searchResultList.getBinding("items")?.refresh(); cc.searchResultTable.getBinding("rows")?.refresh(); cc.searchResultGrid.getBinding("items")?.refresh(); const grid = cc.searchResultGrid; if (grid["getContent"]) { const gridContent = grid["getContent"](); if (gridContent) { grid.getBinding("content")?.refresh(true); } } } if (parameters.changedPropertyIds.includes("customItemStyleClass")) { SearchHelper.resultItemCustomStyleClassSetter(cc.searchResultList); SearchHelper.resultItemCustomStyleClassSetter(cc.searchResultTable); SearchHelper.resultItemCustomStyleClassSetter(cc.searchResultGrid); } } assembleContent(): void { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; // developer assertion: basketAddButton has no effect when basketLinkByResultViewItemSelection=true // because items are added/removed via result view selection — an explicit add button is redundant if (oModel.config.basketLinkByResultViewItemSelection && oModel.config.basketAddButton) { this.oLogger.warning( "Configuration issue: 'basketAddButton' is set to true but has no effect when " + "'basketLinkByResultViewItemSelection' is true. " + "Items are added to the basket automatically via result view item selection. " + "Please set 'basketAddButton' to false or remove it from the configuration." ); } // create search container if (!oModel.config.isUshell) { cc.oSearchFieldGroup = cc.createSearchFieldGroup(); } this.subscribeToModelEvents(); cc.oFilterButton = cc.toolbarAssembler.assembleFilterButton(); cc.oSearchBarTitle = cc.toolbarAssembler.assembleSearchBarTitle(); cc.oBasketButtons = cc.toolbarAssembler.assembleBasketButtons(); cc.oDataSourceTabBar = cc.toolbarAssembler.assembleDataSourceTabBar(); const genericButtonsToolbarData = cc.toolbarAssembler.assembleGenericButtonsToolbar(); cc.oGenericItemsToolbar = genericButtonsToolbarData.toolbar; const toolbarLeftContent: Array = []; // selection toggle button if ( oModel?.config?.showSelectionToggleButton === true && oModel?.config?.resultviewSelectionMode !== SelectionMode.None ) { cc.oSelectionButton = cc.toolbarAssembler.assembleResultviewSelectionButton(); genericButtonsToolbarData.toolbar.addContent(cc.oSelectionButton); } // add basket buttons if ( oModel?.config?.resultviewSelectionMode !== SelectionMode.None && oModel?.config?.basketButtonsLocation === "End" ) { for (const btn of cc.oBasketButtons) { genericButtonsToolbarData.toolbar.addContent(btn); } } let spacerNeeded; // search bar title if (oModel?.config?.searchBarTitle && oModel?.config?.searchBarTitle.length > 0) { toolbarLeftContent.push(cc.oSearchBarTitle); } // basket button(s) if (oModel?.config?.basketButtonsLocation === "Begin") { for (const btn of cc.oBasketButtons) { toolbarLeftContent.push(btn); } } // facet button if (oModel?.config?.facetToggleButtonLocation === "End") { genericButtonsToolbarData.toolbar.addContent(cc.oFilterButton); spacerNeeded = true; } else { toolbarLeftContent.push(cc.oFilterButton); } // data source tab bar if (spacerNeeded) { toolbarLeftContent.push(new ToolbarSpacer()); } toolbarLeftContent.push(cc.oDataSourceTabBar); const searchBarContent = toolbarLeftContent.concat(genericButtonsToolbarData.toolbar); cc.oSearchBar = new OverflowToolbar(cc.getId() + "-searchBar", { width: "100%", visible: { parts: [{ path: "/count" }], formatter: (count) => { return count !== 0 || oModel?.config?.showSearchBarForNoResults; }, }, content: searchBarContent, design: ToolbarDesign.Transparent, }); cc.oSearchBar.addStyleClass("sapUshellSearchBar"); if (genericButtonsToolbarData.hasCustomButtons) { cc.oDataSourceTabBar.addStyleClass("sapUshellSearchBarHasCustomButtons"); } else { cc.oDataSourceTabBar.removeStyleClass("sapUshellSearchBarHasCustomButtons"); } if (oModel.config.optimizeForValueHelp) { cc.oSearchBar.addStyleClass("sapUshellSearchBarValueHelp"); } cc.oNlqExplainBar = new SearchNlqExplainBar(cc.getId() + "-nlqExplainBar", { visible: { parts: [ { path: "/nlqResult/success", }, { path: "/isBusy", }, ], formatter: (success, isBusy) => { const cfg = (cc.getModelInternal() as SearchModel).config; return !!success && !isBusy && cfg.aiNlqExplainBar; }, }, }); cc.oFilterBar = new SearchFilterBar(cc.getId() + "-searchFilterBar"); cc.createSearchPage(cc.getId()); } createSearchFieldGroup(): SearchFieldGroup { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; if (oModel.config.isWebCompSearchFieldGroupEnabled()) { throw new ProgramError(null, "webcomponents are only supported for ushell"); } else { return cc.createClassicSearchFieldGroup(); } } createClassicSearchFieldGroup(): SearchFieldGroup { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; const oSearchFieldGroup = new SearchFieldGroup(cc.getId() + "-searchInputHelpPageSearchFieldGroup"); if (oModel.config.exclusiveDataSource) { oSearchFieldGroup.setSelectActive(false); } oSearchFieldGroup.setCancelButtonActive(false); oSearchFieldGroup.addStyleClass("sapUshellSearchInputHelpPageSearchFieldGroup"); oSearchFieldGroup.input.setShowValueHelp(false); oModel.setProperty("/inputHelp", oSearchFieldGroup.input); if (oModel.config.optimizeForValueHelp) { oSearchFieldGroup.setActionsMenuButtonActive(true); oSearchFieldGroup.setSelectQsDsActive(true); oSearchFieldGroup.addStyleClass("sapUiTinyMarginBegin"); oSearchFieldGroup.addStyleClass("sapUiTinyMarginEnd"); } return oSearchFieldGroup; } createSearchPage(idPrefix: string): void { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; cc.oFilterBar.bindProperty("visible", { parts: [{ path: "/count" }, { path: "/facetVisibility" }, { path: "/uiFilter/rootCondition" }], formatter: (count: number, facetVisibility: boolean, rootCondition: ComplexCondition) => { let filterBarVisible = false; if (!facetVisibility && rootCondition && rootCondition.hasFilters()) { filterBarVisible = true; } // There are conditions only from static hierarchy facet (Collection area), no condition from dynamic static facet or attribute facet ('Filter By' area). // Hide the filter bar if (rootCondition && oModel.hasStaticHierarchyFacetFilterConditionOnly()) { filterBarVisible = false; } // exit isFilterBarVisible if (rootCondition && typeof oModel.config.isFilterBarVisible === "function") { const result = callConfigExit( "isFilterBarVisible", oModel.config.applicationComponent, () => oModel.config.isFilterBarVisible(rootCondition), cc.errorHandler ); if (result === false) { filterBarVisible = false; } } return filterBarVisible; }, }); cc.oFooter = cc.createFooter(cc.getId()); // not available for device type 'phone' if (cc.oFooter) { cc.oFooter.bindProperty("visible", { parts: [{ path: "/errors/length" }], formatter: (numberErrors: number): boolean => { return numberErrors > 0; }, }); } cc.searchContainer = cc.createSearchContainer(idPrefix); let searchPageItems = []; if (cc.oSearchFieldGroup && oModel?.config?.searchInputLocation === "Top") { searchPageItems.push(cc.oSearchFieldGroup); } searchPageItems = searchPageItems.concat([ cc.oSearchBar, cc.oNlqExplainBar, cc.oFilterBar, cc.searchContainer, cc.oFooter, ]); for (const searchPageItem of searchPageItems) { cc.addContent(searchPageItem); } cc.setWidth("100%"); } createNoResultScreen(idPrefix: string, type: "default" | "folder"): VBox { const cc = this.compositeControl; const additionalContent = []; const illustrationSize = IllustratedMessageSize.Auto; let title; let description; let illustrationType; if (type === "folder") { title = { parts: [{ path: "/queryFilter/searchTerm" }], formatter: (searchTerm) => i18n.getText("no_results_info_1_folderEmpty", [searchTerm]), }; description = i18n.getText("no_results_info_2_folderEmpty"); illustrationType = IllustratedMessageType.NoEntries; } else { title = { parts: [{ path: "/queryFilter/searchTerm" }], formatter: (searchTerm) => i18n.getText("no_results_info_1", [searchTerm]), }; description = i18n.getText("no_results_info_2"); illustrationType = IllustratedMessageType.NoSearchResults; } const illustratedMessageSettings: $IllustratedMessageSettings = { title: title, description: description, illustrationSize: illustrationSize, illustrationType: illustrationType, additionalContent: additionalContent, }; let noResultsScreenIdPrefix; if (type === "folder") { noResultsScreenIdPrefix = `${idPrefix}-searchContainerResultsView-noResultFolderScreen`; } else { noResultsScreenIdPrefix = `${idPrefix}-searchContainerResultsView-noResultScreen`; } const oIllustratedMessage = new IllustratedMessage( `${noResultsScreenIdPrefix}-searchContainerResultsView-noResultScreen-illustratedMessage`, illustratedMessageSettings ).addStyleClass("sapElisaNoResultScreen"); oIllustratedMessage.addStyleClass("sapUiMediumMarginTop"); const noResultScreen = new VBox( `${noResultsScreenIdPrefix}-searchContainerResultsView-noResultScreen`, { items: oIllustratedMessage, width: "100%", justifyContent: FlexJustifyContent.Center, visible: { parts: [{ path: "/count" }, { path: "/isBusy" }, { path: "/firstSearchWasExecuted" }], formatter: function (count, isBusy, firstSearchWasExecuted) { return count === 0 && !isBusy && firstSearchWasExecuted; }, }, } ); if (type === "folder") { cc.noResultScreenFolder = noResultScreen; } else { cc.noResultScreen = noResultScreen; } return noResultScreen; } createSearchContainer(idPrefix: string): SearchLayoutResponsive { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; // total count hidden element for ARIA purposes cc.countBreadcrumbsHiddenElement = cc.resultViewsAssembler.assembleCountBreadcrumbsHiddenElement(); // center area cc.resultView = cc.resultViewsAssembler.assembleResultView(idPrefix); // main result list cc.oSearchCountBreadcrumbs = new SearchCountBreadcrumbs( cc.getId() + "-SearchCountBreadcrumbs" ).addStyleClass("sapElisaSearchContextBarContent"); cc.oContextBar = new Bar(cc.getId() + "-searchContextBar", { contentLeft: cc.oSearchCountBreadcrumbs, design: { parts: [ { path: "/isFolderMode", }, ], formatter: (isFolderMode: boolean): string => { if (oModel.config.folderMode === true && isFolderMode === true) { cc.oContextBar.addStyleClass("sapElisaSearchContextBarFolderMode"); } else { cc.oContextBar.removeStyleClass("sapElisaSearchContextBarFolderMode"); } return BarDesign.Auto; }, }, }).addStyleClass("sapElisaSearchContextBar"); cc.oSearchSelectionBar = new SearchSelectionBar(cc.getId() + "-searchSelectionBar"); cc.oContextBarContainer = new VerticalLayout("", { content: [cc.oSearchSelectionBar, cc.oContextBar], }); cc.oContextBarContainer.enhanceAccessibilityState = function (oElement, mAriaProps) { mAriaProps["role"] = "heading"; mAriaProps["level"] = 2; }; cc.oContextBarContainer.addStyleClass("sapElisaSearchContextBarContainer"); cc.oContextBarContainer.addStyleClass("sapUiNoMarginBegin"); cc.oContextBarContainer.addStyleClass("sapUiNoMarginEnd"); cc.oSearchResultContainer = new SearchResultContainer(`${idPrefix}-searchContainerResultsView`); cc.oSearchResultContainer.setNoResultScreen(cc.createNoResultScreen(idPrefix, "default")); cc.resultView.push(cc.oSearchResultContainer.getNoResultScreen()); cc.resultView.forEach((element) => { cc.oSearchResultContainer.addContent(element); }); cc.oSearchResultPanel = new FixFlex(`${idPrefix}-searchResultPanel`, { fixContent: [cc.oContextBarContainer], flexContent: cc.oSearchResultContainer, }).addStyleClass("sapElisaResultPanel"); // define group for F6 handling cc.oSearchResultPanel.data("sap-ui-fastnavgroup", "true", true /* write into DOM */); // container for normal search, result view and facets const searchLayoutResponsiveSettings: Partial<$SearchLayoutResponsiveSettings> = { resultPanelContent: cc.oSearchResultPanel, searchIsBusy: { path: "/isBusy" }, busyDelay: { path: "/busyDelay" }, // facets facetPanelResizable: oModel.config.facetPanelResizable, facetPanelWidthInPercent: oModel.config.facetPanelWidthInPercent, facetPanelContent: new SearchFacetList(`${cc.getId()}-SearchFacetList`), // basket basketPanelResizable: oModel.config.basketPanelResizable, basketPanelWidthInPercent: oModel.config.basketPanelWidthInPercent, basketPanelContent: oModel.config.basketLocation === "SidePanelRight" && cc.oSearchBaskets.length > 0 ? cc.oSearchBaskets[cc.oSearchBaskets.length - 1] : null, showFacets: { parts: [{ path: "/facetVisibility" }], formatter: (facetVisibility): boolean => { if (!facetVisibility) { return false; } return true; }, }, showBasket: { parts: [{ path: "/basketPanelVisibility" }], formatter: (basketPanelVisibility): boolean => { if (!basketPanelVisibility) { return false; } return true; }, }, }; const searchLayout = new SearchLayoutResponsive( `${cc.getId()}-searchLayout`, searchLayoutResponsiveSettings ).addStyleClass("sapUshellSearchLayout"); return searchLayout; } isSearchFieldGroupLocatedInsideSearchComposite(currentControlDomRef?: HTMLElement | ParentNode): boolean { // check if the search field group is a child of the SearchCompositeControl if (typeof currentControlDomRef === "undefined" || currentControlDomRef === null) { // null: before first rendering return false; } else if ((currentControlDomRef as HTMLElement).classList) { if ((currentControlDomRef as HTMLElement).classList.contains("sapUshellSearchInputHelpPage")) { return true; } else if (typeof currentControlDomRef.parentNode !== "undefined") { return this.isSearchFieldGroupLocatedInsideSearchComposite(currentControlDomRef.parentNode); } else { this.oLogger.error( "function: isSearchFieldGroupLocatedInsideSearchComposite: Element has no property 'parentNode' but has property 'classlist'" ); return false; } } else if (currentControlDomRef.nodeType === Node.DOCUMENT_NODE) { // document node (root) return false; } else { this.oLogger.error( "function: isSearchFieldGroupLocatedInsideSearchComposite: Element has no property 'classlist'" ); return false; } } adjustSearchbarCustomGenericButtonWidth(): void { const cc = this.compositeControl; if (cc.getDomRef() === null) { return; // SearchCompositeControl not rendered ---postponed attachment to dom or not at all (unit tests) } if (cc.oFilterButton.getVisible() && cc.oFilterButton.getDomRef() === null) { return; // not yet rendered } const searchCompositeControlSizes = window.document .getElementById(cc.getDomRef().id) .getBoundingClientRect(); // search bar title let searchBarTitleWidth = 0; if (cc.oSearchBarTitle.getVisible() && cc.oSearchBarTitle.getDomRef() !== null) { searchBarTitleWidth = cc.oSearchBarTitle.getVisible() ? window.document.getElementById(cc.oSearchBarTitle.getDomRef().id).getBoundingClientRect() .width : 0; } // filter button let filterButtonWidth = 0; if (cc.oFilterButton.getVisible() && cc.oFilterButton.getDomRef() !== null) { filterButtonWidth = cc.oFilterButton.getVisible() ? window.document.getElementById(cc.oFilterButton.getDomRef().id).getBoundingClientRect() .width : 0; } // basket button(s) let basketButtonsWidth = 0; if (cc.oBasketButtons.some((btn) => btn.getVisible() && btn.getDomRef() !== null)) { cc.oBasketButtons.forEach((btn) => { if (btn.getVisible()) { basketButtonsWidth += window.document .getElementById(btn.getDomRef().id) .getBoundingClientRect().width; } }); } let dataSourceTabBarWidthPx = 0; if (cc.oDataSourceTabBar.getVisible()) { const totalTextLength = cc.oDataSourceTabBar .getItems() .reduce((accumulator, item) => accumulator + item["getText"]().length, 0); if (totalTextLength < 20) { // tiny cc.oDataSourceTabBar.addStyleClass("searchDataSourceTabStripBarTinyItemsTextLength"); cc.oDataSourceTabBar.removeStyleClass("searchDataSourceTabStripBarMediumItemsTextLength"); } else if (totalTextLength < 45) { // medium cc.oDataSourceTabBar.addStyleClass("searchDataSourceTabStripBarMediumItemsTextLength"); cc.oDataSourceTabBar.removeStyleClass("searchDataSourceTabStripBarTinyItemsTextLength"); } else { // default cc.oDataSourceTabBar.removeStyleClass("searchDataSourceTabStripBarTinyItemsTextLength"); cc.oDataSourceTabBar.removeStyleClass("searchDataSourceTabStripBarMediumItemsTextLength"); } const dataSourceTabBarDomRef = cc.oDataSourceTabBar.getDomRef(); if (dataSourceTabBarDomRef?.id) { dataSourceTabBarWidthPx = window.document .getElementById(dataSourceTabBarDomRef.id) .getBoundingClientRect().width; } else { // data source tab bar not rendered any longer because of i.e. window/container width less than 500px (see media query) // nothing to do, default value dataSourceTabBarWidthPx = 0 is OK } } const genericToolbarWidthPx = searchCompositeControlSizes.width - filterButtonWidth - // ToDo: check if filter button is visible and where it is located (begin or end) basketButtonsWidth - // ToDo: check if basket button is visible and where it is located (begin or end) searchBarTitleWidth - // ToDo: check if search bar title is visible dataSourceTabBarWidthPx; const genericToolbarWidthRem = convertPixelToRem(genericToolbarWidthPx); if (dataSourceTabBarWidthPx === 0) { if (genericToolbarWidthRem < 30) { cc.oGenericItemsToolbar.setWidth(`${genericToolbarWidthRem - 1.1}rem`); } else { cc.oGenericItemsToolbar.setWidth(`${genericToolbarWidthRem - 3.0}rem`); } } else { cc.oGenericItemsToolbar.setWidth(`${genericToolbarWidthRem - 2.75}rem`); } } resizeHandler() { // search toolbar ('overflow' of buttons at the right) setTimeout(() => this.adjustSearchbarCustomGenericButtonWidth(), 0); } createFooter(idPrefix: string): OverflowToolbar { const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; // create error message popover cc.oErrorPopover = new MessagePopover(`${idPrefix}-${messagePopoverId++}--searchMessagePopover`, { placement: PlacementType.Top, }); cc.oErrorPopover.setModel(oModel); oModel.setProperty("/messagePopoverControlId", cc.oErrorPopover.getId()); cc.oErrorPopover.bindAggregation("items", { path: "/errors", factory: () => { const item = new MessageItem("", { type: "{type}", title: "{title}", description: "{description}", }); return item; }, }); // create error message popover button const oErrorButton = new Button(cc.getId() + "-searchErrorButton", { icon: IconPool.getIconURI("alert"), text: { parts: [{ path: "/errors/length" }], formatter: function (length) { return length; }, }, visible: { parts: [{ path: "/errors/length" }], formatter: (length) => { return length > 0; }, mode: BindingMode.OneWay, }, type: ButtonType.Emphasized, tooltip: i18n.getText("errorBtn"), press: () => { if (cc.oErrorPopover.isOpen()) { cc.oErrorPopover.close(); } else { cc.oErrorPopover.setVisible(true); cc.oErrorPopover.openBy(oErrorButton); } }, }); oErrorButton.addStyleClass("sapUiSmallMarginBegin"); oErrorButton["addDelegate"]({ onAfterRendering: () => { const oModel = cc.getModelInternal() as SearchModel; if (!oModel.getProperty("/isErrorPopovered")) { // automatically open the error popup (only after first rendering of button) oErrorButton.firePress(); oModel.setProperty("/isErrorPopovered", true); } }, }); oErrorButton.setLayoutData( new OverflowToolbarLayoutData("", { priority: OverflowToolbarPriority.NeverOverflow, }) ); // create footer bar const footer = new OverflowToolbar(cc.getId() + "-searchFooter", { content: [oErrorButton], }); footer.addStyleClass("sapUiTinyMarginTop"); return footer; } chooseNoResultScreen(): void { // update "no result screen" const cc = this.compositeControl; const oModel = cc.getModelInternal() as SearchModel; let noResultScreen; if (typeof oModel?.config?.getCustomNoResultScreen === "function") { const customNoResultScreen = callConfigExit( "getCustomNoResultScreen", oModel.config.applicationComponent, () => oModel.config.getCustomNoResultScreen(oModel.getDataSource(), oModel), cc.errorHandler ); if (customNoResultScreen === undefined) { // callConfigExit returns undefined when it catches an error and reports via errorHandler noResultScreen = cc.oSearchResultContainer.getNoResultScreen(); } else if (customNoResultScreen && customNoResultScreen.isDestroyed()) { const oError = new errors.ConfigurationExitError( "getCustomNoResultScreen", oModel.config.applicationComponent, new Error( `The 'no results screen' instance provided by exit 'getCustomNoResultScreen' is already destroyed.\nWe have switched to standard 'no results screen'.\nFor stability reasons, we are destroying old instances with each roundtrip unless custom 'no results screen' instance stays the same.\nThis is needed to keep a cleaned-up dom with no ghost instances (i.e. still described to events).` ) ); cc.errorHandler.onError(oError); // do not throw oError, use standard 'no results screen' noResultScreen = cc.oSearchResultContainer.getNoResultScreen(); } else { // if needed, destroy 'no results screen' instance const currentNoResultsPage = cc.oSearchResultContainer.getNoResultScreen(); if ( currentNoResultsPage !== cc.noResultScreenFolder && currentNoResultsPage !== cc.noResultScreen && currentNoResultsPage !== customNoResultScreen ) { if (currentNoResultsPage instanceof Control) { currentNoResultsPage.destroy(); } } noResultScreen = customNoResultScreen; } } if (!noResultScreen) { if (oModel.getProperty("/isFolderMode")) { if (!cc.noResultScreenFolder) { cc.noResultScreenFolder = cc.createNoResultScreen(cc.getId(), "folder"); } noResultScreen = cc.noResultScreenFolder; } else { noResultScreen = cc.noResultScreen; } } cc.oSearchResultContainer.setNoResultScreen(noResultScreen); } openSortDialog(sortDialog: ViewSettingsDialog) { // issue: selection information is lost by clicking cancel, multiple reset selection in UI5 // workaround: rebind sort items by opening dialog sortDialog.unbindAggregation("sortItems", false); sortDialog.bindAggregation("sortItems", { path: "/sortableAttributes", factory: (): ViewSettingsItem => { return new ViewSettingsItem("", { key: { path: "key" }, text: { path: "name" }, selected: { path: "selected" }, }); }, }); sortDialog.open(); } openShowMoreDialog(dimension?: string) { const cc = this.compositeControl; openShowMoreDialog({ searchModel: cc.getModelInternal() as SearchModel, dimension: dimension, selectedTabBarIndex: 0, tabBarItems: undefined, sourceControl: cc, }); } createAndRegisterBasket( idPostfix: string, type: "sapMList" | "sapMTable" | "sapUiTable" ): SearchBasketSapUiTable | SearchBasketSapMTable | SearchBasketSapMList { const cc = this.compositeControl; let oBasket; const oSearchModel = cc.getModelInternal() as SearchModel; // create basket of requested type if (type === "sapMList") { const basketConfig: $SearchBasketSapMListSettings = { basketConfigurator: oSearchModel.config.basketBeforeAddItem ? { adaptColumns: (columns) => columns, adaptRow: oSearchModel.config.basketBeforeAddItem, } : null, showDeleteAction: true, selectionMode: "None", }; oBasket = new SearchBasketSapMList( `${cc.getId()}-SearchBasket-sapmlist-${idPostfix}`, basketConfig ); } else if (type === "sapMTable") { const basketConfig: $SearchBasketSapMTableSettings = { basketConfigurator: oSearchModel.config.basketBeforeAddItem ? { adaptColumns: (columns) => columns, adaptRow: oSearchModel.config.basketBeforeAddItem, } : null, showDeleteAction: true, selectionMode: "None", }; oBasket = new SearchBasketSapMTable( `${cc.getId()}-SearchBasket-sapmtable-${idPostfix}`, basketConfig ); } else if (type === "sapUiTable") { const basketConfig: $SearchBasketSapUiTableSettings = { basketConfigurator: oSearchModel.config.basketBeforeAddItem ? { adaptColumns: (columns) => columns, adaptRow: oSearchModel.config.basketBeforeAddItem, } : null, showDeleteAction: true, selectionMode: "None", }; oBasket = new SearchBasketSapUiTable( `${cc.getId()}-SearchBasket-sapuitable-${idPostfix}`, basketConfig ); } else { throw new ProgramError( null, `SearchCompositeControl.createAndRegisterBasket\n -> Unsupported basket type: ${type}` ); } const basketModel = oBasket.getBasketModel(); basketModel.setProperty("/internalSearchModel", oSearchModel); // it is not expected to have multiple baskets registered // fallback -> destroy existing basket instance(s) if (cc.oSearchBaskets.length > 0) { const oError = new Error( `SearchCompositeControl.createAndRegisterBasket\n -> Multiple baskets registered.\nCurrently only one basket instance can be created. We have destroyed all previous instances.` ); cc.errorHandler.onError(oError); // destroy existing basket instance(s) for (const basket of cc.oSearchBaskets) { (basket as Control).destroy(); } cc.oSearchBaskets = []; } // register basket for interaction with result view cc.oSearchBaskets.push(oBasket); oSearchModel.eventLogger?.logTechnicalEvent({ type: TechnicalEventType.BASKET_CREATE, basketType: type, }); return oBasket; } }