UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,535 lines (1,306 loc) 81.2 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.FacetFilter. sap.ui.define([ './NavContainer', './library', 'sap/ui/core/Control', 'sap/ui/core/CustomData', 'sap/ui/core/Element', 'sap/ui/core/IconPool', "sap/ui/core/Lib", 'sap/ui/core/delegate/ItemNavigation', 'sap/ui/core/InvisibleText', 'sap/ui/core/IntervalTrigger', 'sap/ui/Device', 'sap/ui/core/Icon', 'sap/ui/model/Filter', 'sap/ui/model/FilterOperator', 'sap/ui/model/json/JSONModel', './FacetFilterRenderer', "sap/ui/events/KeyCodes", "sap/base/assert", "sap/base/Log", "sap/ui/events/jquery/EventSimulation", "sap/ui/thirdparty/jquery", "sap/m/Button", "sap/m/ToolbarSpacer", "sap/m/OverflowToolbar", "sap/m/Text", "sap/m/Toolbar", "sap/m/Popover", "sap/m/SearchField", "sap/m/Bar", "sap/m/Dialog", "sap/m/List", "sap/m/StandardListItem", "sap/m/CheckBox", "sap/m/Page", "sap/ui/core/library", 'sap/ui/core/date/UI5Date', // jQuery Plugin "scrollRightRTL" "sap/ui/dom/jquery/scrollRightRTL", // jQuery Plugin "scrollLeftRTL" "sap/ui/dom/jquery/scrollLeftRTL", // jQuery custom selectors ":sapTabbable" "sap/ui/dom/jquery/Selectors" ], function( NavContainer, library, Control, CustomData, Element, IconPool, Library, ItemNavigation, InvisibleText, IntervalTrigger, Device, Icon, Filter, FilterOperator, JSONModel, FacetFilterRenderer, KeyCodes, assert, Log, EventSimulation, jQuery, Button, ToolbarSpacer, OverflowToolbar, Text, Toolbar, Popover, SearchField, Bar, Dialog, List, StandardListItem, CheckBox, Page, coreLibrary, UI5Date ) { "use strict"; // shortcut for sap.m.ToolbarDesign var ToolbarDesign = library.ToolbarDesign; // shortcut for sap.m.ListType var ListType = library.ListType; // shortcut for sap.m.ListMode var ListMode = library.ListMode; // shortcut for sap.m.FacetFilterListDataType var FacetFilterListDataType = library.FacetFilterListDataType; // shortcut for sap.m.ButtonType var ButtonType = library.ButtonType; // shortcut for sap.m.PlacementType var PlacementType = library.PlacementType; // shortcut for sap.m.FacetFilterType var FacetFilterType = library.FacetFilterType; // shortcut for sap.ui.core.TitleLevel var TitleLevel = coreLibrary.TitleLevel; var SCROLL_DURATION = 500; /** * Constructor for a new <code>FacetFilter</code>. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * Provides filtering functionality with multiple parameters. * * <h3>Overview</h3> * * The <code>FacetFilter</code> control is used to provide filtering functionality * with multiple parameters and supports the users in finding the information they * need from potentially very large data sets. * * Your app can have dependencies between facets where selection of filter items in * one facet list limits the list of valid filters in another facet list. * * The <code>FacetFilter</code> uses {@link sap.m.FacetFilterList FacetFilterList} and * {@link sap.m.FacetFilterItem FacetFilterItem} to model facets and their associated * filters. * * <b>Note: </b>{@link sap.m.FacetFilterList FacetFilterList} is a subclass of * {@link sap.m.List} and supports growing enablement feature via the property * <code>growing</code>. When you use this feature, be aware that it only works with * one-way data binding. * Having growing feature enabled when the <code>items</code> aggregation is bound to * a model with two-way data binding, may lead to unexpected and/or inconsistent * behavior across browsers, such as unexpected closing of the list. * * <h3>Usage</h3> * * Use the <code>FacetFilter</code> if your app displays a large list of * items that can be grouped by multiple parameters, for example products by category * and supplier. With the <code>FacetFilter</code>, you allow the users * to dynamically filter the list so it only displays products from the categories and * suppliers they want to see. * * While the {@link sap.m.FacetFilterList} popup is opened (when the user selects a button * corresponding to the list's name), any other activities leading to focus change will * close it. For example, when the popup is opened and the app developer loads a * {@link sap.m.BusyDialog} or any other dialog that obtains the focus, the popup will * be closed. * * <h3>Responsive behavior</h3> * * The <code>FacetFilter</code> supports the following two types, which * can be configured using the control's <code>type</code> property: * * <ul><li>Simple type (default) - only available for desktop and tablet screen sizes. * The active facets are displayed as individually selectable buttons on the toolbar.</li> * <li>Light type - automatically enabled on smart phone sized devices, but also * available for desktop and tablets. The active facets and selected filter items are * displayed in the summary bar. When the user selects the summary bar, a navigable * dialog list of all facets is displayed. When the user selects a facet, the dialog * scrolls to show the list of filters that are available for the selected facet.</li></ul> * * <h3>Additional Information</h3> * * For more information, go to <b>Developer Guide</b> section in the Demo Kit and navigate to * <b>More About Controls</b> &gt; <b>sap.m</b> &gt; <b>Facet Filter</b> * * @extends sap.ui.core.Control * @implements sap.ui.core.IShrinkable * @version 1.146.0 * * @constructor * @public * @alias sap.m.FacetFilter * @see {@link topic:c6c38217a4a64001a22ad76cdfa97fae Facet Filter} */ var FacetFilter = Control.extend("sap.m.FacetFilter", /** @lends sap.m.FacetFilter.prototype */ { metadata : { interfaces : [ "sap.ui.core.IShrinkable" ], library : "sap.m", properties : { /** * If set to <code>true</code> and the FacetFilter type is <code>Simple</code>, then the Add Facet icon will be displayed and each facet button will also have a Facet Remove icon displayed beside it, allowing the user to deactivate the facet. * * <b>Note:</b> Always set this property to <code>true</code> when your facet lists are not active, so that the user is able to select them and interact with them. */ showPersonalization : {type : "boolean", group : "Appearance", defaultValue : false}, /** * Defines the default appearance of the FacetFilter on the device. Possible values are <code>Simple</code> (default) and <code>Light</code>. */ type : {type : "sap.m.FacetFilterType", group : "Appearance", defaultValue : FacetFilterType.Simple}, /** * Enables/disables live search in the search field of all <code>sap.m.FacetFilterList</code> instances. */ liveSearch : {type : "boolean", group : "Behavior", defaultValue : true}, /** * Shows the summary bar instead of the FacetFilter buttons bar when set to <code>true</code>. */ showSummaryBar : {type : "boolean", group : "Behavior", defaultValue : false}, /** * Shows/hides the FacetFilter Reset button. */ showReset : {type : "boolean", group : "Behavior", defaultValue : true}, /** * If set to <code>true</code>, an OK button is displayed for every FacetFilterList popover. This button allows the user to close the popover from within the popover instead of having to click outside of it. */ showPopoverOKButton : {type : "boolean", group : "Appearance", defaultValue : false} }, defaultAggregation : "lists", aggregations : { /** * Collection of FacetFilterList controls. */ lists : {type : "sap.m.FacetFilterList", multiple : true, singularName : "list"}, /** * Hidden aggregation of buttons that open each FacetFilterList popover. These buttons are displayed only when the FacetFilter is of type <code>Simple</code>. */ buttons : {type : "sap.m.Button", multiple : true, singularName : "button", visibility : "hidden"}, /** * Hidden aggregation of icons for setting FacetFilterLists to inactive, thereby, removing the FacetFilter button from the display. The icon is displayed only if personalization is enabled. */ removeFacetIcons : {type : "sap.ui.core.Icon", multiple : true, singularName : "removeFacetIcon", visibility : "hidden"}, /** * Hidden aggregation for the FacetFilterLists popover. */ popover : {type : "sap.m.Popover", multiple : false, visibility : "hidden"}, /** * Hidden aggregation for the Add Facet button. This button allows the user to open the facet dialog and add or configure facets. This is displayed only if personalization is enabled and the FacetFilter is of type <code>Simple</code>. */ addFacetButton : {type : "sap.m.Button", multiple : false, visibility : "hidden"}, /** * Hidden aggregation for the dialog that displays the facet and filter items pages. */ dialog : {type : "sap.m.Dialog", multiple : false, visibility : "hidden"}, /** * Hidden aggregation for the summary bar. */ summaryBar : {type : "sap.m.Toolbar", multiple : false, visibility : "hidden"}, /** * Hidden aggregation for the Reset button displayed for FacetFilter of type <code>Simple</code>. */ resetButton : {type : "sap.m.Button", multiple : false, visibility : "hidden"}, /** * Hidden aggregation for the arrow that scrolls the facets to the left when the FacetFilter is set to type <code>Simple</code>. */ arrowLeft : {type : "sap.ui.core.Icon", multiple : false, visibility : "hidden"}, /** * Hidden aggregation for the arrow that scrolls the facets to the right when the FacetFilter is set to type <code>Simple</code>. */ arrowRight : {type : "sap.ui.core.Icon", multiple : false, visibility : "hidden"} }, events : { /** * Fired when the Reset button is pressed to inform that all FacetFilterLists need to be reset. * * The default filtering behavior of the sap.m.FacetFilterList can be prevented by calling <code>sap.ui.base.Event.prototype.preventDefault</code> function * in the <code>search</code> event handler function. If the default filtering behavior is prevented then filtering behavior has to be defined at application level * inside the <code>search</code> and <code>reset</code> event handler functions. */ reset : {}, /** * Fired when the user confirms filter selection. */ confirm: {} } }, renderer: FacetFilterRenderer }); // How many pixels to scroll with every overflow arrow click FacetFilter.SCROLL_STEP = 264; /* * Loads the appropriate type of FacetFilter according to device. * @param {object} oType Type of FacetFilter to render depending on device * @returns {this} this for chaining */ FacetFilter.prototype.setType = function(oType) { var oSummaryBar, bActive; // Force light type if running on a phone if (Device.system.phone) { this.setProperty("type", FacetFilterType.Light); bActive = true; } else { this.setProperty("type", oType); bActive = (oType === FacetFilterType.Light); } oSummaryBar = this._getSummaryBar(); oSummaryBar.setActive(bActive); if (oType === FacetFilterType.Light) { if (this.getShowReset()) { this._addResetToSummary(oSummaryBar); } else { this._removeResetFromSummary(oSummaryBar); } } return this; }; /* * Sets whether or not to display Reset button to reset values. * @param {boolean} bVal Boolean to set Reset button to true or false * @returns {this} this for chaining */ FacetFilter.prototype.setShowReset = function(bVal) { this.setProperty("showReset", bVal); var oSummaryBar = this._getSummaryBar(); if (bVal) { if (this.getShowSummaryBar() || this.getType() === FacetFilterType.Light) { this._addResetToSummary(oSummaryBar); } } else { if (this.getShowSummaryBar() || this.getType() === FacetFilterType.Light) { this._removeResetFromSummary(oSummaryBar); } } return this; }; /* * Sets whether or not to display summary bar. * @param {boolean} bVal Boolean to set summary bar to <code>true</code> or <code>false</code> * @returns {this} this for chaining */ FacetFilter.prototype.setShowSummaryBar = function(bVal) { this.setProperty("showSummaryBar", bVal); if (bVal) { var oSummaryBar = this._getSummaryBar(); if (this.getShowReset()) { this._addResetToSummary(oSummaryBar); } else { this._removeResetFromSummary(oSummaryBar); } oSummaryBar.setActive(this.getType() === FacetFilterType.Light); } return this; }; /* * Sets whether or not to display live search bar. * @param {boolean} bVal Boolean to set live search bar to <code>true</code> or <code>false</code> * @returns {this} <code>this</code> to allow method chaining */ FacetFilter.prototype.setLiveSearch = function(bVal) { // Allow app to change live search while the search field is displayed. this.setProperty("liveSearch", bVal); if (this._displayedList) { var oList = this._displayedList; var oSearchField = Element.getElementById(oList.getAssociation("search")); // Always detach the handler at first regardless of bVal, otherwise multiple calls of this method will add // a separate change handler to the search field. oSearchField.detachLiveChange(oList._handleSearchEvent, oList); if (bVal) { oSearchField.attachLiveChange(oList._handleSearchEvent, oList); } } return this; }; /* * Gets the FacetFilterLists necessary to load. * @returns {sap.m.FacetFilterList} List that is specified. */ FacetFilter.prototype.getLists = function() { // Override to make sure we also return a list if it is currently displayed // in a display container (like the Popover or Dialog). When a list is displayed it is removed from the lists aggregation // and placed into the display container, so it will no longer be part of the lists aggregation. var aLists = this.getAggregation("lists"); if (!aLists) { aLists = []; } if (this._displayedList) { aLists.splice(this._listAggrIndex, 0, this._displayedList); } aLists.forEach(function(oList) { oList.setBusyIndicatorDelay(0); if (!oList.hasListeners("listItemsChange")) { oList.attachEvent("listItemsChange", _listItemsChangeHandler.bind(this)); } }.bind(this)); return aLists; }; function _listItemsChangeHandler(oEvent) { var oList = oEvent.getSource(); if (this._oAllCheckBoxBar) { this._oAllCheckBoxBar.setVisible(Boolean(oList.getItems(true).length)); } } /** * Removes the aggregation from the FacetFilterList. * @returns {sap.m.FacetFilterList} oList that is to be removed */ FacetFilter.prototype.removeAggregation = function() { var oList = Control.prototype.removeAggregation.apply(this, arguments); if (arguments[0] === "lists") { this._removeList(oList); } return oList; }; // API doc provided in the meta-data /** * Opens the FacetFilter dialog. * * @returns {this} this pointer for chaining * @public */ FacetFilter.prototype.openFilterDialog = function() { var oDialog = this._getFacetDialog(); var oNavContainer = this._getFacetDialogNavContainer(); oDialog.removeAllAriaLabelledBy(); oDialog.addAriaLabelledBy(InvisibleText.getStaticId("sap.m", "FACETFILTER_AVAILABLE_FILTER_NAMES")); oDialog.addContent(oNavContainer); this.getLists().forEach(function (oList) { if (oList.getMode() === ListMode.MultiSelect) { oList._preserveOriginalActiveState(); } }); //keyboard acc - focus on 1st item of 1st page oDialog.setInitialFocus(oNavContainer.getPages()[0].getContent()[0].getItems()[0]); oDialog.open(); return this; }; /** * @private */ FacetFilter.prototype.init = function() { this._pageSize = 5; this._invalidateFlag = false; this._lastCategoryFocusIndex = 0; this._aDomRefs = null; this._previousTarget = null; this._addTarget = null; this._aRows = null; //save item level div this._bundle = Library.getResourceBundleFor("sap.m"); this.data("sap-ui-fastnavgroup", "true", true); // Define group for F6 handling // Button map used to quickly get a button for a given list. This avoids having to iterate through the button aggregation // to find a button for a list. this._buttons = {}; this._aOwnedLabels = []; // Remove icon map used to quickly get the remove icon for a given list. This avoids having to iterate through the removeIcon aggregation // to find an icon for a list. this._removeFacetIcons = {}; // The index of a list in the "lists" aggregation, used to restore the list back to the aggregation when it is no longer displayed this._listAggrIndex = -1; // Reference to the currently displayed FacetFilterList. This is set after the list is moved from the lists aggregation // to the display container. this._displayedList = null; // Last state of scrolling - using during rendering this._lastScrolling = false; // Remember the facet button overflow state this._bPreviousScrollForward = false; this._bPreviousScrollBack = false; this._popoverClosing = false; this._getAddFacetButton(); // This is the reset button shown for Simple type (not the same as the button created for the summary bar) this.setAggregation("resetButton", this._createResetButton()); // Attach event delegate for click handling this._oFacetPopoverCloseDelegate = { onclick: function(oEvent) { // Only handle clicks for Simple type if (this.getType() !== FacetFilterType.Simple) { return; } var oPopover = this.getAggregation("popover"); var oPopoverDom = oPopover && oPopover.getDomRef(); if (oPopoverDom && !oPopoverDom.contains(oEvent.target)) { this._closePopover(); } } }; this.addEventDelegate(this._oFacetPopoverCloseDelegate, this); // Enable touch support for the carousel if (EventSimulation.touchEventMode === "ON" && !Device.system.phone) { this._enableTouchSupport(); } if (Device.system.phone) { this.setType(FacetFilterType.Light); } }; /** * @private */ FacetFilter.prototype.exit = function() { var oCtrl; IntervalTrigger.removeListener(this._checkOverflow, this); if (this._oFacetPopoverCloseDelegate) { this.removeEventDelegate(this._oFacetPopoverCloseDelegate); } if (this.oItemNavigation) { this.removeDelegate(this.oItemNavigation); this.oItemNavigation.destroy(); } if (this._aOwnedLabels) { this._aOwnedLabels.forEach(function (sId) { oCtrl = Element.getElementById(sId); if (oCtrl) { oCtrl.destroy(); } }); this._aOwnedLabels = null; } if (this._oAllCheckBoxBar) { this._oAllCheckBoxBar = undefined; } if (this._oInvisibleTitleElement) { this._oInvisibleTitleElement.destroy(); this._oInvisibleTitleElement = null; } }; /** * @private */ FacetFilter.prototype.onBeforeRendering = function() { if (this.getShowSummaryBar() || this.getType() === FacetFilterType.Light) { var oSummaryBar = this._getSummaryBar(); var oText = oSummaryBar.getContent()[0]; oText.setText(this._getSummaryText()); } // Detach the interval timer attached in onAfterRendering IntervalTrigger.removeListener(this._checkOverflow, this); }; /** * @private */ FacetFilter.prototype.onAfterRendering = function() { var bShowSummaryBar = this.getShowSummaryBar(), sType = this.getType(), oSummaryBar = this._getSummaryBar().$(); if (sType !== FacetFilterType.Light && !Device.system.phone) { // Attach an interval timer that periodically checks overflow of the "head" div in the event that the window is resized or the device orientation is changed. This is ultimately to // see if carousel arrows should be displayed. IntervalTrigger.addListener(this._checkOverflow, this); } if (sType !== FacetFilterType.Light) { this._startItemNavigation(); this.addDelegate(this.oItemNavigation); } if (sType === FacetFilterType.Light) { oSummaryBar.attr("aria-roledescription", this._bundle.getText("FACETFILTER_ACTIVE_TITLE")); oSummaryBar.attr("role", "group"); } else if (bShowSummaryBar) { oSummaryBar.attr("aria-roledescription", this._bundle.getText("FACETFILTER_TITLE")); } }; /* Keyboard Handling */ /** * Sets the start of navigation with keyboard. * @private */ FacetFilter.prototype._startItemNavigation = function() { //Collect the dom references of the items var oFocusRef = this.getDomRef(), aRows = oFocusRef.getElementsByClassName("sapMFFHead"), aDomRefs = []; if (aRows.length > 0) { for (var i = 0; i < aRows[0].childNodes.length; i++) { if (aRows[0].childNodes[i].id.indexOf("ff") < 0 && aRows[0].childNodes[i].id.indexOf("icon") < 0 && aRows[0].childNodes[i].id.indexOf("add") < 0) { aDomRefs.push(aRows[0].childNodes[i]); } if (aRows[0].childNodes[i].id.indexOf("add") >= 0) { aDomRefs.push(aRows[0].childNodes[i]); } } } if (aDomRefs != "") { this._aDomRefs = aDomRefs; } //initialize the delegate add apply it to the control (only once) if (!this.oItemNavigation) { this.oItemNavigation = new ItemNavigation(); } this._aRows = aRows; for (var i = 0; i < this.$().find(":sapTabbable").length; i++) { if (this.$().find(":sapTabbable")[i].id.indexOf("add") >= 0) { this._addTarget = this.$().find(":sapTabbable")[i]; break; } } // After each rendering the delegate needs to be initialized as well. this.oItemNavigation.setRootDomRef(oFocusRef); if (this._invalidateFlag == true) { this.oItemNavigation.setFocusedIndex(-1); this.focus(); this._invalidateFlag = false; } //set the array of dom nodes representing the items. this.oItemNavigation.setItemDomRefs(aDomRefs); //turn off the cycling this.oItemNavigation.setCycling(false); //set the selected index this.oItemNavigation.setPageSize(this._pageSize); this.oItemNavigation.setDisabledModifiers({ sapnext: ["alt", "meta"], sapprevious: ["alt", "meta"], saphome : ["alt", "meta"], sapend : ["meta"] }); }; /** * Deletes list category. * @param {object} oEvent Fired when the Delete key is pressed */ FacetFilter.prototype.onsapdelete = function(oEvent) { var oButton, oList; // no special handling is needed in case of Light mode if (this.getType() === FacetFilterType.Light) { return; } // no deletion - showpersonalization set to false" if (!this.getShowPersonalization()) { return; } oButton = Element.getElementById(oEvent.target.id); if (!oButton) {//not a UI5 object return; } oList = Element.getElementById(oButton.getAssociation("list")); // no deletion on button 'Add', "Reset" if (!oList) {//We allow only buttons with attached list. return; } // no deletion - showRemoveFacetIcon set to false if (!oList.getShowRemoveFacetIcon()) { return; } oList.removeSelections(true); oList.setSelectedKeys(); oList.setProperty("active", false, true); this.invalidate(); var $Tabbables = this.$().find(":sapTabbable"); jQuery($Tabbables[$Tabbables.length - 1]).trigger("focus"); var nextFocusIndex = this.oItemNavigation.getFocusedIndex(); jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextFocusIndex + 1); this.focus(); if (this.oItemNavigation.getFocusedIndex() == 0) { for ( var k = 0; k < this.$().find(":sapTabbable").length - 1; k++) { if ($Tabbables[k].id.indexOf("add") >= 0) { jQuery($Tabbables[k]).trigger("focus"); } } } }; //[TAB] /** * Handles the navigation when using the TAB key. * @param {object} oEvent Fired when the TAB key is pressed */ FacetFilter.prototype.onsaptabnext = function(oEvent) { // no special handling for TAB is needed in case of Light mode if (this.getType() === FacetFilterType.Light) { return; } this._previousTarget = oEvent.target; if (oEvent.target.parentNode.className == "sapMFFHead" ) { //if focus on category, and then press tab, then focus on reset for ( var i = 0; i < this.$().find(":sapTabbable").length; i++) { if (this.$().find(":sapTabbable")[i].parentNode.className == "sapMFFResetDiv") { jQuery(this.$().find(":sapTabbable")[i]).trigger("focus"); this._invalidateFlag = false; oEvent.preventDefault(); oEvent.setMarked(); return; } } } this._lastCategoryFocusIndex = this.oItemNavigation.getFocusedIndex(); if (this._invalidateFlag == true) { this.oItemNavigation.setFocusedIndex(-1); this.focus(); this._invalidateFlag = false; } }; /** * Navigates back with SHIFT + TAB to focus on the previous item. * @param {object} oEvent Fired when SHIFT + TAB keys are pressed */ //[SHIFT]+[TAB] FacetFilter.prototype.onsaptabprevious = function(oEvent) { // no special handling for SHIFT + TAB is needed in case of Light mode if (this.getType() === FacetFilterType.Light) { return; } // without tabnext, and keep entering shift+tab, focus move to the 1st facetfilter list Button if (oEvent.target.parentNode.className == "sapMFFResetDiv" && this._previousTarget == null) { jQuery(this.$().find(":sapTabbable")[0]).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); return; } if (oEvent.target.parentNode.className == "sapMFFResetDiv" && this._previousTarget != null && this._previousTarget.id != oEvent.target.id) { jQuery(this._previousTarget).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); return; } if (oEvent.target.id.indexOf("add") >= 0 || oEvent.target.parentNode.className == "sapMFFHead") { this._previousTarget = oEvent.target; jQuery(this.$().find(":sapTabbable")[0]).trigger("focus"); } }; /** * Moves the focus to the last icon in the category when the END key is pressed. * @param {object} oEvent Fired when END key is pressed */ FacetFilter.prototype.onsapend = function(oEvent) { // no special handling in Light mode if (this.getType() === FacetFilterType.Light) { return; } if (this._addTarget != null) { jQuery(this._addTarget).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); } else { jQuery(this._aRows[this._aRows.length - 1]).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); } this._previousTarget = oEvent.target; }; /** * Moves the focus to the first icon in the category when the HOME key is pressed. * @param {object} oEvent Fired when HOME key is pressed */ FacetFilter.prototype.onsaphome = function(oEvent) { // no special handling for HOME is needed in case of Light mode if (this.getType() === FacetFilterType.Light) { return; } jQuery(this._aRows[0]).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); this._previousTarget = oEvent.target; }; /** * Moves the focus to an appropriate area (upwards) when PAGEUP key is pressed. * @param {object} oEvent Fired when PAGEUP key is pressed */ FacetFilter.prototype.onsappageup = function(oEvent) { this._previousTarget = oEvent.target; }; /** * Moves the focus to an appropriate area (downwards) when PAGEDOWN key is pressed. * @param {object} oEvent Fired when PAGEDOWN key is pressed */ FacetFilter.prototype.onsappagedown = function(oEvent) { this._previousTarget = oEvent.target; }; /** * Imitates Page Down event. * @param {object} oEvent Fired when CTRL + RIGHT keys are pressed */ FacetFilter.prototype.onsapincreasemodifiers = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } // [CTRL]+[RIGHT] - keycode 39 - page down if (oEvent.which == KeyCodes.ARROW_RIGHT) { this._previousTarget = oEvent.target; var currentFocusIndex = this.oItemNavigation.getFocusedIndex() - 1; var nextFocusIndex = currentFocusIndex + this._pageSize; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextFocusIndex); this.focus(); } }; /** * Imitates Page Up event. * @param {object} oEvent Fired when CTRL + LEFT keys are pressed */ FacetFilter.prototype.onsapdecreasemodifiers = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } // [CTRL]+[LEFT] - keycode 37 - page up var currentFocusIndex = 0; if (oEvent.which == KeyCodes.ARROW_LEFT) { this._previousTarget = oEvent.target; currentFocusIndex = this.oItemNavigation.getFocusedIndex() + 1; var nextFocusIndex = currentFocusIndex - this._pageSize; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextFocusIndex); this.focus(); } }; /** * Imitates Page Down event. * @param {object} oEvent Fired when CTRL + DOWN keys are pressed */ FacetFilter.prototype.onsapdownmodifiers = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } // [CTRL]+[DOWN] - page down this._previousTarget = oEvent.target; var currentFocusIndex = 0; currentFocusIndex = this.oItemNavigation.getFocusedIndex() - 1; var nextFocusIndex = currentFocusIndex + this._pageSize; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextFocusIndex); this.focus(); }; /** * Imitates Page Up event. * @param {object} oEvent Fired when CTRL + UP keys are pressed */ FacetFilter.prototype.onsapupmodifiers = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } // [CTRL]+[UP] - page up this._previousTarget = oEvent.target; var currentFocusIndex = 0; currentFocusIndex = this.oItemNavigation.getFocusedIndex(); if (currentFocusIndex != 0) { currentFocusIndex = currentFocusIndex + 1; } var nextFocusIndex = currentFocusIndex - this._pageSize; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextFocusIndex); this.focus(); }; /** * Moves the focus to the next category (if the focus is on a category). * Scroll accordingly if needed. * @param {object} oEvent Fired when RIGHT or DOWN key is pressed */ FacetFilter.prototype.onsapexpand = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } // [+] = right/down - keycode 107 this._previousTarget = oEvent.target; var nextDocusIndex = this.oItemNavigation.getFocusedIndex() + 1; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextDocusIndex); this.focus(); }; /** * Moves the focus to the previous category (if the focus is on a category). * Scroll accordingly if needed. The Add Filter button is considered a category. * @param {object} oEvent The event fired when LEFT or UP ARROW key is pressed */ FacetFilter.prototype.onsapcollapse = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } // [-] = left/up - keycode 109 this._previousTarget = oEvent.target; var nextDocusIndex = this.oItemNavigation.getFocusedIndex() - 1; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextDocusIndex); this.focus(); }; /** * Moves the focus to the next category (if the focus is on a category). * Scroll accordingly if needed. * @param {object} oEvent Fired when DOWN ARROW key is pressed */ FacetFilter.prototype.onsapdown = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } this._previousTarget = oEvent.target; if (oEvent.target.parentNode.className == "sapMFFResetDiv") { jQuery(oEvent.target).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); return; } }; /** * Moves the focus to the previous category (if the focus is on a category). * Scroll accordingly if needed. The Add Filter button is considered a category. * @param {object} oEvent Fired when UP ARROW key is pressed */ FacetFilter.prototype.onsapup = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } this._previousTarget = oEvent.target; if (oEvent.target.parentNode.className == "sapMFFResetDiv") { jQuery(oEvent.target).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); } }; /** * Moves the focus to the previous category (if the focus is on a category). * Scroll accordingly if needed. The Add Filter button is considered a category. * @param {object} oEvent Fired when LEFT ARROW key is pressed */ FacetFilter.prototype.onsapleft = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } this._previousTarget = oEvent.target; if (oEvent.target.parentNode.className == "sapMFFResetDiv") { jQuery(oEvent.target).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); } var oItems = this.oItemNavigation.aItemDomRefs, iCurrentFocusIndex = this.oItemNavigation.getFocusedIndex(), iNexFucusIndex = iCurrentFocusIndex - 1 >= 0 ? iCurrentFocusIndex - 1 : iCurrentFocusIndex, oNextTarget = Element.closestTo(oItems[iNexFucusIndex]), iScrollOffset = this._calculateScrollIntoView(oNextTarget); this._scroll(iScrollOffset, SCROLL_DURATION); }; /** * Moves the focus to the next category (if the focus is on a category). * Scroll accordingly if needed. * @param {object} oEvent Fired when RIGHT ARROW key is pressed */ FacetFilter.prototype.onsapright = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } this._previousTarget = oEvent.target; if (oEvent.target.parentNode.className == "sapMFFResetDiv") { jQuery(oEvent.target).trigger("focus"); oEvent.preventDefault(); oEvent.setMarked(); } var oItems = this.oItemNavigation.aItemDomRefs, iCurrentFocusIndex = this.oItemNavigation.getFocusedIndex(), iNexFucusIndex = oItems.length > iCurrentFocusIndex + 1 ? iCurrentFocusIndex + 1 : iCurrentFocusIndex, oNextTarget = Element.closestTo(oItems[iNexFucusIndex]), iScrollToPosition = this._calculateScrollIntoView(oNextTarget); this._scroll(iScrollToPosition, SCROLL_DURATION); }; /** * Sets the focus back to the Category (if the focus is on a category, which had the focus at the time when the categories' list got the focus). * The Add Filter button is considered a category. * @param {object} oEvent Fired when ESCAPE key is pressed */ FacetFilter.prototype.onsapescape = function(oEvent) { if (this.getType() === FacetFilterType.Light) { return; } if (oEvent.target.parentNode.className == "sapMFFResetDiv") { return; } var nextFocusIndex = this._lastCategoryFocusIndex; jQuery(oEvent.target).trigger("blur"); this.oItemNavigation.setFocusedIndex(nextFocusIndex); this.focus(); }; /** * Displays the facet popover when the user presses the facet button (Simple type only). * The popover is created if it does not exist and is available through the popover aggregation. This aggregation is destroyed when the popover is closed. * * @returns {sap.m.Popover} Multiple calls return the same popover instance * @private */ FacetFilter.prototype._getPopover = function() { var oPopover = this.getAggregation("popover"); if (!oPopover) { var that = this; // Popover allowing the user to view, select, and search filter items oPopover = new Popover({ placement: PlacementType.Bottom, beforeOpen: function(oEvent) { if (that._displayedList) { that._displayedList._bSearchEventDefaultBehavior && that._displayedList._setSearchValue(""); } this.setCustomHeader(that._createFilterItemsSearchFieldBar(that._displayedList)); var subHeaderBar = this.getSubHeader(); if (!subHeaderBar) { this.setSubHeader(that._createSelectAllCheckboxBar(that._displayedList)); } clearDeleteFacetIconTouchStartFlag(that._displayedList); }, beforeClose: function() { that._popoverClosing = true; }, afterClose: function(oEvent) { this._popoverClosing = false; that._handlePopoverAfterClose(); }, horizontalScrolling: false }); // Suppress invalidate so that FacetFilter is not rerendered when the popover is opened (causing it to immediately close) this.setAggregation("popover", oPopover, true); oPopover.setContentWidth("30%"); // Set the minimum width of the popover to insure that it is not too small to display it's content properly. // This is not the same as setting Popover.contentWidth, which sets a fixed width size. We want the popover // to grow in width if any of its content is wider than the min width. oPopover.addStyleClass("sapMFFPop"); var clearDeleteFacetIconTouchStartFlag = function(oList) { if (!oList) { return; } var oIcon = that._getFacetRemoveIcon(oList); if (oIcon) { oIcon._bTouchStarted = false; } }; } if (this.getShowPopoverOKButton()) { this._addOKButtonToPopover(oPopover); } else { oPopover.destroyAggregation("footer"); } return oPopover; }; /** * Handles closing of the popover with given filter list. * * We have 2 options for calling this method: * 1. Popover.afterClose handler. * 2. Delete Icon.touchend(2.1) & Icon.press handler(2.2) - fnProcessRemoveFacetAction * * When the user clicks on the delete facet icon, the following event flows are possible: * a) quick click - icon touchstart, icon touchend, icon press, popover afterClose * b) click, hold & release delete icon - icon touchstart, popover afterClose, icon touchend, icon press * c) click, hold delete icon, but release elsewhere - icon touchstart, popover afterClose, icon touchend * * Having in mind the above, the following corresponding actions are taken: * a) current method is called due to option#1 where "listClose" & "confirm" events are fired. * b) method call due to option #1 is skipped, the real work is posponed to the next call (due to option #2.2) * c) method call due to option #1 is skipped and as there is no press event, next the underlying method is called * due to option #2.1 * * If popover is destroyed any further calls of this method results in nothing as the work has already been done. * * (Re) opening popover restarts this functionality. * @private */ FacetFilter.prototype._handlePopoverAfterClose = function () { var oPopover = this.getAggregation("popover"), oList = this._displayedList; if (!oPopover) { // make sure we skip redundant work return; } var oIcon = this._getFacetRemoveIcon(oList); if (oIcon && oIcon._bTouchStarted) { //do not react on popover close if the "remove facet" button was touched, but not released (i.e. no 'press' event) return; } this._restoreListFromDisplayContainer(oPopover); this._displayRemoveIcon(false, oList); oList._fireListCloseEvent(); this._fireConfirmEvent(); // Destroy the popover aggregation, otherwise if the list is then moved to the dialog filter items page, it will still think it's DOM element parent // is the popover causing facet filer item checkbox selection to not display the check mark when the item is selected. this.destroyAggregation("popover"); if (this._oOpenPopoverDeferred) { setTimeout(function () { this._oOpenPopoverDeferred.resolve(); this._oOpenPopoverDeferred = undefined; }.bind(this), 0); } }; /** * Fires the <code>confirm</code> event. * @private */ FacetFilter.prototype._fireConfirmEvent = function () { this.fireEvent('confirm'); }; /** * * @param {sap.m.Popover} oPopover the Popover to be opened * @param {sap.ui.core.Control} oControl The control the popover will be opened "by" * @returns {this} <code>this</code> to allow method chaining * @private */ FacetFilter.prototype._openPopover = function(oPopover, oControl) { var bIsListOpenDefaultPrevented; // Don't open if already open, otherwise the popover will display empty. if (!oPopover.isOpen()) { var oList = Element.getElementById(oControl.getAssociation("list")); assert(oList, "The facet filter button should be associated with a list."); bIsListOpenDefaultPrevented = !oList.fireListOpen({}); oList.attachUpdateFinished(_listItemsChangeHandler.bind(this)); this._moveListToDisplayContainer(oList, oPopover); oPopover.openBy(oControl); //Display remove facet icon only if ShowRemoveFacetIcon property is set to true if (oList.getShowRemoveFacetIcon()) { this._displayRemoveIcon(true, oList); } if (oList.getWordWrap()) { oPopover.setContentWidth("30%"); } if (!bIsListOpenDefaultPrevented) { oList._applySearch(); } } return this; }; /** * @returns {sap.m.Button} the "add facet" button * @private */ FacetFilter.prototype._getAddFacetButton = function() { var oButton = this.getAggregation("addFacetButton"); if (!oButton) { oButton = new Button(this.getId() + "-add", { icon: IconPool.getIconURI("add-filter"), type: ButtonType.Transparent, tooltip:this._bundle.getText("FACETFILTER_ADDFACET"), press: function(oEvent) { this.openFilterDialog(); }.bind(this) }); this.setAggregation("addFacetButton", oButton, true); } return oButton; }; /** * Gets the facet button for the given list (it is created if it doesn't exist yet). * The button text is set with the given list title. * * @param {sap.m.FacetFilterList} oList The list displayed when the button is pressed * @returns {sap.m.Button} The button for the list * @private */ FacetFilter.prototype._getButtonForList = function(oList) { if (this._buttons[oList.getId()]) { this._setButtonText(oList); return this._buttons[oList.getId()]; } var that = this; var oButton = new Button({ type : ButtonType.Transparent, press : function(oEvent) { /*eslint-disable consistent-this */ var oThisButton = this; /*eslint-enable consistent-this */ var fnOpenPopover = function() { var oPopover = that._getPopover(); that._openPopover(oPopover, oThisButton); }; if (oList.getMode() === ListMode.MultiSelect) { oList._preserveOriginalActiveState(); } var oPopover = that._getPopover(); if (oPopover.isOpen()) { // create a deferred that will be triggered after the popover is closed setTimeout(function() { if (oPopover.isOpen()) { return; } that._oOpenPopoverDeferred = jQuery.Deferred(); that._oOpenPopoverDeferred.promise().done(fnOpenPopover); }, 100); } else { setTimeout(fnOpenPopover.bind(this), 100); } } }); this._buttons[oList.getId()] = oButton; this.addAggregation("buttons", oButton); // Insures that the button text is updated if FacetFilterList.setTitle() is called oButton.setAssociation("list", oList.getId(), true); this._setButtonText(oList); return oButton; }; /** * Updates the facet button text based on selections in the given list. * * @param {sap.m.FacetFilterList} oList The FacetFilterList * @private */ FacetFilter.prototype._setButtonText = function(oList) { var oButton = this._buttons[oList.getId()]; //store the full count of list items initially and when there's items if (oList._iAllItemsCount === undefined && oList.getMaxItemsCount() || !oList._bSearchEventDefaultBehavior) { oList._iAllItemsCount = oList.getMaxItemsCount(); } if (oButton) { // Button may not be created yet if FFL.setTitle() is called before the button is rendered the first time var sText = ""; var aSelectedKeyNames = Object.getOwnPropertyNames(oList._oSelectedKeys); var iLength = aSelectedKeyNames.length; if (iLength === 1) { // Use selected item value for button label if only one selected var sSelectedItemText = oList._oSelectedKeys[aSelectedKeyNames[0]]; sText = this._bundle.getText("FACETFILTER_ITEM_SELECTION", [oList.getTitle(), sSelectedItemText]); } else if (iLength > 0 && iLength === (oList._iAllItemsCount ? oList._iAllItemsCount : 0) ) { //if iAllItemsCount is undefined we must be sure that the check is between integers sText = this._bundle.getText("FACETFILTER_ALL_SELECTED", [oList.getTitle()]); } else if (iLength > 0) { sText = this._bundle.getText("FACETFILTER_ITEM_SELECTION", [oList.getTitle(), iLength]); } else { sText = oList.getTitle(); } oButton.setText(sText); } }; /** * Gets the FacetFilterList remove icon for the given list (it is created if it doesn't exist yet ). * The icon is associated with the FacetFilterList ID, which is why we only need to pass the FacetFilterList to retrieve the icon once it has been created. * @param {sap.m.FacetFilterList} oList the given list, whose icon will be returned * @private */ FacetFilter.prototype._getFacetRemoveIcon = function(oList) { var that = this, oIcon = this._removeFacetIcons[oList.getId()]; if (!oIcon) { oIcon = new Icon({ src : IconPool.getIconURI("decline"), tooltip:this._bundle.getText("FACETFILTER_REMOVE"), press: function() { oIcon._bPressed = true; } }); oIcon.addDelegate({ ontouchstart: function() { //Mark this icon as touch started oIcon._bTouchStarted = true; oIcon._bPressed = false; }, ontouchend: function() { // Not all touchend are followed by "press" event(e.g. touchstart over the icon, but the user touchend-s somewhere else. // So make sure the "remove icon" is always hidden that._displayRemoveIcon(false, oList); oIcon._bTouchStarted = false; //Schedule actual processing so eventual "press" event is caught. setTimeout(fnProcessRemoveFacetAction.bind(this), 100); } }, true); /** * Handles touch/click on "remove facet" icon depending on the received events. **/ var fnProcessRemoveFacetAction = function() { if (oIcon._bPressed) {// touchstart, touchend, press oList.removeSelections(true); oList.setSelectedKeys(); oList.setProperty("active", false, true); } //otherwise - touchstart, touchend, because the user released the mouse/touchend-ed outside the icon. //In both cases popover closes and needs to be handled that._handlePopoverAfterClose(); }; oIcon.setAssociation("list", oList.getId(), true); oIcon.addStyleClass("sapMFFLRemoveIcon"); this._removeFacetIcons[oList.getId()] = oIcon; this.addAggregation("removeFacetIcons", oIcon); this._displayRemoveIcon(false, oList); } return oIcon; }; /** * Shows/hides the FacetFilterList remove icon for the given list. * @param {boolean} bDisplay if the icon will be displayed * @param {sap.m.FacetFilterList} oList - the list where the icon is * @private */ FacetFilter.prototype._displayRemoveIcon = function(bDisplay, oList) { if (this.getShowPersonalization()) { var oIcon = this._removeFacetIcons[oList.getId()]; if (bDisplay) { oIcon.removeStyleClass("sapMFFLHiddenRemoveIcon"); oIcon.addStyleClass("sapMFFLVisibleRemoveIcon"); } else { oIcon.removeStyleClass("sapMFFLVisibleRemoveIcon"); oIcon.addStyleClass("sapMFFLHiddenRemoveIcon"); } } }; /** * Creates the navigation container displayed in the FacetFilter dialog. * The container is created with an initial page for the list of facets and a second page for displaying a list of items associated with the facet selected on the initial page. * * @private */ FacetFilter.prototype._getFacetDialogNavContainer = function() { // set autoFocus of the NavContainer to false because otherwise on touch devices // the keyboard pops out due to the focus being automatically set on an input field var oNavContainer = new NavContainer({ autoFocus: false }); var oFacetPage = this._createFacetPage(); oNavContainer.addPage(oFacetPage); oNavContainer.setInitialPage(oFacetPage); oNavContainer.attachNavigate(function(oEvent) { var oToPage = oEvent.getParameters()["to"], oDialog = this.getAggregation("dialog"), oInvisibleTitleElement = this._getInvisibleTitleElement(); if (oToPage !== oFacetPage) { oDialog.addAriaLabelledBy(oInvisibleTitleElement.getId()); oInvisibleTitleElement.setText(oToPage.getTitle()); } else { oDialog.removeAriaLabelledBy(oInvisibleTitleElement.getId()); oInvisibleTitleElement.setText(""); } oDialog.setInitialFocus(oToPage); }, this); oNavContainer.attachAfterNavigate(function(oEvent) { // Clean up transient filter items page controls. This must be done here instead of navFromFacetFilterList // so that controls are not removed before the transition to the facet page is completed. Otherwise you notice // a slight visual change in the filter items page just prior to navigation. var oToPage = oEvent.getParameters()["to"]; var oFromPage = oEvent.getParameters()['from']; //keyboard acc if (oFromPage === oFacetPage) { // in SingleSelectMaster focus on the 1st content item 1st item // in MultiSelect mode focus on 1st item of 2nd content item, since the first content item is the Bar with "Select All" checkbox var oFirstItem = (this._displayedList.getMode() === ListMode.MultiSelect) ? oToPage.getContent(0)[1].getItems()[0] : oToPage.getContent(0)[0].getItems()[0]; if (oFirstItem) { oFirstItem.focus(); } else if (oToPage.getContent()[1]) { oToPage.getContent()[1].focus(); } } if (oToPage === oFacetPage) { // Destroy the search field bar oFromPage.destroySubHeader(); assert(this._displayedList === null, "Filter items list should have been placed back in the FacetFilter aggregation before page content is destroyed."); oFromPage.destroyContent(); // Destroy the select all checkbox bar // TODO: Find out why the counter is not updated without forcing rendering of the facet list item