/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import i18n from "./i18n"; import SearchFieldGroup from "./controls/searchfieldgroup/SearchFieldGroup"; import * as SearchHelper from "sap/esh/search/ui/SearchHelper"; import Event from "sap/ui/base/Event"; import SearchModel from "sap/esh/search/ui/SearchModel"; import SearchSelect from "./controls/searchfieldgroup/SearchSelect"; import SearchFieldStateManager from "./controls/searchfieldgroup/SearchFieldStateManager"; import SearchShellHelperHorizonTheme from "./SearchShellHelperHorizonTheme"; import UIEvents from "./UIEvents"; import Element from "sap/ui/core/Element"; import EventBus from "sap/ui/core/EventBus"; import ResourceModel from "sap/ui/model/resource/ResourceModel"; import { PeriodicRetry } from "./SearchUtil"; import WebCompSearchFieldStateManager from "./controls/webcompsearchfieldgroup/WebCompSearchFieldStateManager"; import Control from "sap/ui/core/Control"; import { createWebCompSearchFieldGroupBindings } from "./controls/webcompsearchfieldgroup/WebCompSearchFieldGroupBindings"; import type ShellBarSearch from "sap/f/gen/ui5/webcomponents_fiori/dist/ShellBarSearch"; import IconPool from "sap/ui/core/IconPool"; import Container from "sap/ushell/Container"; import Config from "sap/ushell/Config"; import jQuery from "sap/ui/thirdparty/jquery"; export interface SearchShellHelperInitProperties { searchField?: ShellBarSearch; ushell_FF_useWebComponentsSearchInput?: boolean; } export default abstract class SearchShellHelper { static isInitialized: boolean; static sSearchOverlayCSS: "sapUshellShellShowSearchOverlay"; static oModel: SearchModel; static oShellHeader: any; static oSearchFieldGroup: SearchFieldGroup; static focusInputFieldTimeout: number | undefined; static isFocusHandlerActive: boolean; static searchFieldStateManager: SearchFieldStateManager | WebCompSearchFieldStateManager; static periodicRetryFocus: PeriodicRetry; // ======================================================================= // generic methods: used for webcomponents and not webcomponents use case // ======================================================================= constructor() { throw new Error("Cannot instantiate static class 'SearchShellHelper'"); } public static async init(properties?: SearchShellHelperInitProperties): Promise { // check for new SearchShellLoader const searchShellLoaderActive = this.isSearchShellLoaderActive(); if (searchShellLoaderActive) { if (!properties) { return; // ignore legacy initialization calls } } else { // set defaults for classical none webcomponents use case properties = { searchField: undefined, ushell_FF_useWebComponentsSearchInput: false }; } // check already initialized if (this.isInitialized) { return; } this.isInitialized = true; // set autoexpand hint in case search field provided by ushell is expanded let autoExpandHint = false; if (properties?.searchField) { autoExpandHint = (properties.searchField as any).getParent().getShowSearchField(); } // get search model and shell header this.oModel = SearchModel.getModelSingleton({}, "flp") as SearchModel; this.oShellHeader = Element.getElementById("shell-header") as any; // pass ushell webcomponent configuration this.oModel.config.setUseWebComponentsSearchInput( searchShellLoaderActive && properties.ushell_FF_useWebComponentsSearchInput && !!document.querySelector("#shell-header .sapUshellShellBar") ); // pre-fetch all app tiles Container.getServiceAsync("Search").then((service) => { (service as { prefetch() }).prefetch(); }); // initialize search field group if (this.oModel.config.isWebCompSearchFieldGroupEnabled()) { await this.initWebCompSearchFieldGroup(properties.searchField); } else { await this.initSearchFieldGroup(autoExpandHint); } // after navigation -> set focus to first element of results list (Element.getElementById("viewPortContainer") as any).attachAfterNavigate( this.onAfterNavigate.bind(this), {} ); // event ?? -> set focus to first element of results list EventBus.getInstance().subscribe("sap.ushell", "appComponentLoaded", () => { if (this?.oModel?.focusHandler && SearchHelper.isSearchAppActive()) { this.oModel.focusHandler.setFocus(); } }); } public static isSearchShellLoaderActive(): boolean { let searchModulePath: string; try { searchModulePath = Config.last("/core/search/modulePath"); } catch { return false; } return searchModulePath === "sap/esh/search/ui/SearchShellLoader"; } public static resetModel(): void { this.oModel.resetQuery(); } public static onAfterNavigate(oEvent: any): void { // navigation tries to restore the focus -> but application knows better how to set the focus // -> after navigation call focus setter of search application if ( oEvent.getParameter("toId") !== "shellPage-Action-search" && oEvent.getParameter("toId") !== "applicationShellPage-Action-search" && oEvent.getParameter("toId") !== "application-Action-search" ) { return; } this.oModel.focusHandler.setFocus(); this.oModel.notifySubscribers(UIEvents.ESHSearchLayoutChanged, null); } // obsolete for webcomponents use case but coding needs to be removed from ushell public static onSearchComponentLoaded(): void { // triggered by shell after search component is loaded // (search field is created in search component) if (!SearchHelper.isSearchAppActive()) { return; } this.expandSearch(); } // obsolete for webcomponents use case but coding needs to be removed from ushell public static expandSearch(focusSearchField?: boolean): void { this.searchFieldStateManager?.expandSearch(focusSearchField); } // obsolete for webcomponents use case but coding needs to be removed from ushell public static collapseSearch(): void { this.searchFieldStateManager?.collapseSearch(); } // ======================================================================= // webcomponents methods // ======================================================================= private static async initWebCompSearchFieldGroup(searchField?: ShellBarSearch) { await this.createWebCompSearchFieldGroup(searchField); } private static async createWebCompSearchFieldGroup(searchFieldGroup: ShellBarSearch) { await createWebCompSearchFieldGroupBindings(searchFieldGroup); (searchFieldGroup as Control).setModel(this.oModel); (searchFieldGroup as Control).setModel(new ResourceModel({ bundle: i18n }), "i18n"); this.oSearchFieldGroup = searchFieldGroup as any; this.oModel.initAsync().then(() => { try { (searchFieldGroup as Control).setVisible(true); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // ignore } }); // } // ======================================================================= // classical (not webcomponent) methods // ======================================================================= private static async initSearchFieldGroup(autoExpandHint: boolean) { const oSearchConfig = { id: "sf", tooltip: "{i18n>openSearchBtn}", text: "{i18n>searchBtn}", ariaLabel: "{i18n>openSearchBtn}", icon: IconPool.getIconURI("search"), visible: true, press: () => { if (!this.oSearchFieldGroup) { this.createSearchFieldGroup(); } this.onShellSearchButtonPressed(); }, }; const frameBoundExtension = await Container.getServiceAsync("FrameBoundExtension"); const searchButton = await (frameBoundExtension as any).createHeaderItem(oSearchConfig); searchButton.showOnHome(); searchButton.showForAllApps(); if ( SearchShellHelperHorizonTheme.isSearchFieldExpandedByDefault() || SearchHelper.isSearchAppActive() || autoExpandHint ) { this.createSearchFieldGroup(); this.onShellSearchButtonPressed(false); } } private static createSearchFieldGroup() { // create search field group control const oSearchFieldGroup = new SearchFieldGroup("searchFieldInShell"); oSearchFieldGroup.setModel(this.oModel); oSearchFieldGroup.setModel(new ResourceModel({ bundle: i18n }), "i18n"); // initialize search input const oSearchInput = oSearchFieldGroup.input; oSearchInput.setMaxSuggestionWidth("30rem"); // initialize search select const oSearchSelect = oSearchFieldGroup.select as SearchSelect; oSearchSelect.setTooltip(i18n.getText("searchInTooltip")); oSearchSelect.addEventDelegate( { onAfterRendering: () => { jQuery('[id$="searchFieldInShell-select-icon"]').attr("title", i18n.getText("searchIn")); }, }, oSearchSelect ); oSearchSelect.setTooltip(i18n.getText("searchIn")); oSearchSelect.attachChange(() => { (this.searchFieldStateManager as SearchFieldStateManager).focusSearchInput({ selectContent: true, }); }); // initialize search button const oSearchButton = oSearchFieldGroup.button; oSearchButton.attachPress(() => { this.handleClickSearchButton(); }); // initialize cancel button const oSearchCancelButton = oSearchFieldGroup.cancelButton; oSearchCancelButton.attachPress(() => { this.collapseSearch(); }); oSearchFieldGroup.setCancelButtonActive(false); // esc key handler jQuery(document).on("keydown", this.fnEscCallBack.bind(this)); // header size changed -> adapt visibility cancel button, dropdown button size, visibility search button this.oShellHeader.attachSearchSizeChanged(this.sizeSearchFieldChanged.bind(this)); // create search field state manager const searchFieldStateManager = new SearchFieldStateManager({ shellHeader: this.oShellHeader, model: this.oModel, searchFieldGroup: oSearchFieldGroup, }); // set search field in shell bar this.oShellHeader.setSearch(oSearchFieldGroup); // globalize this.oSearchFieldGroup = oSearchFieldGroup; this.searchFieldStateManager = searchFieldStateManager; } public static fnEscCallBack(oEvent: any): void { // check that search field group is available if (!this.oSearchFieldGroup) { return; } // check for ESC if (oEvent.keyCode !== 27) { return; } // check that search field is focused if (!this.searchFieldStateManager.isSearchInputFocused()) { return; } // check that search app is active if (SearchHelper.isSearchAppActive()) { return; } oEvent.preventDefault(); // browser would delete value if ((this.oSearchFieldGroup as SearchFieldGroup).input.getValue() === "") { this.collapseSearch(); } else if ((this.oSearchFieldGroup as SearchFieldGroup).input.getValue() === " ") { (this.oSearchFieldGroup as SearchFieldGroup).input.setValue(""); // ?? } } public static sizeSearchFieldChanged(event: Event): void { // check that search field group is available if (!this.oSearchFieldGroup) { return; } const size = event.getParameters()["remSize"]; // display mode of connector dropdown let limit = 24; if (size <= limit) { (this.oSearchFieldGroup as SearchFieldGroup).select.setDisplayMode("icon"); } else { (this.oSearchFieldGroup as SearchFieldGroup).select.setDisplayMode("default"); } // visibility of search button limit = 9; if (size < limit) { (this.oSearchFieldGroup as SearchFieldGroup).button.setVisible(false); } else { (this.oSearchFieldGroup as SearchFieldGroup).button.setVisible(true); } // cancel button if ((event as any).getParameter("isFullWidth")) { (this.oSearchFieldGroup as SearchFieldGroup).setCancelButtonActive(true); (this.oSearchFieldGroup as SearchFieldGroup).addStyleClass("sapUshellSearchInputFullWidth"); } else { (this.oSearchFieldGroup as SearchFieldGroup).setCancelButtonActive(false); (this.oSearchFieldGroup as SearchFieldGroup).removeStyleClass("sapUshellSearchInputFullWidth"); } } public static async onShellSearchButtonPressed(focus = true): Promise { await SearchShellHelper.init(); if (!this.oSearchFieldGroup) { this.createSearchFieldGroup(); } this.expandSearch(focus); } public static handleClickSearchButton(): void { if ( (this.searchFieldStateManager as SearchFieldStateManager).getSearchBoxValue() === "" && this.oModel.getDataSource() === this.oModel.getDefaultDataSource() ) { if (SearchShellHelperHorizonTheme.isSearchFieldExpandedByDefault()) { // screen size XL: focus input field (this.searchFieldStateManager as SearchFieldStateManager).focusSearchInput({ selectContent: false, }); } else { // small screen size: collapse input field + focus shell magnifier this.collapseSearch(); } } } }