All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dominokit.domino.ui.menu.AbstractMenuItem Maven / Gradle / Ivy

/*
 * Copyright © 2019 Dominokit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.dominokit.domino.ui.menu;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.dominokit.domino.ui.utils.Domino.*;

import elemental2.core.JsDate;
import elemental2.dom.Element;
import elemental2.dom.Event;
import elemental2.dom.HTMLElement;
import elemental2.dom.HTMLLIElement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.dominokit.domino.ui.IsElement;
import org.dominokit.domino.ui.elements.AnchorElement;
import org.dominokit.domino.ui.elements.DivElement;
import org.dominokit.domino.ui.elements.LIElement;
import org.dominokit.domino.ui.events.EventType;
import org.dominokit.domino.ui.icons.lib.Icons;
import org.dominokit.domino.ui.menu.direction.BestFitSideDropDirection;
import org.dominokit.domino.ui.style.ConditionalCssClass;
import org.dominokit.domino.ui.utils.*;
import org.gwtproject.editor.client.TakesValue;

/**
 * Represents a general purpose menu item that can be used in different types of menus.
 *
 * 

Usage example: * *

 * AbstractMenuItem item = new AbstractMenuItem<>();
 * item.setKey("item1").withValue("Value1");
 * 
* * @param the type parameter defining the value of the menu item * @see BaseDominoElement */ public class AbstractMenuItem extends BaseDominoElement> implements HasSelectionHandler, AbstractMenuItem>, HasDeselectionHandler>, TakesValue, MenuStyles { protected final LIElement root; protected final AnchorElement linkElement; protected Menu parent; private final List>> selectionHandlers = new ArrayList<>(); private final List deselectionHandlers = new ArrayList<>(); private String key; private V value; private LazyChild> indicatorIcon; Menu menu; MenuItemsGroup itemGroup; protected DivElement prefixElement; protected DivElement bodyElement; protected DivElement postfixElement; protected DivElement nestedIndicatorElement; protected boolean searchable = true; protected boolean selectable = true; protected MenuSearchFilter searchFilter = (token, caseSensitive) -> false; /** Default constructor to create a menu item. */ public AbstractMenuItem() { root = li().addCss(dui_menu_item); linkElement = a("#") .setAttribute("tabindex", "0") .setAttribute("aria-expanded", "true") .addCss(dui_menu_item_anchor) .appendChild(prefixElement = div().addCss(dui_menu_item_prefix)) .appendChild(bodyElement = div().addCss(dui_menu_item_body)) .appendChild(postfixElement = div().addCss(dui_menu_item_postfix)) .appendChild(nestedIndicatorElement = div().addCss(dui_menu_item_nested_indicator)); root.appendChild(linkElement); indicatorIcon = createIndicator(Icons.menu_right()); init(this); double[] startTime = new double[] {0}; this.addEventListener( EventType.touchstart.getName(), evt -> { startTime[0] = JsDate.now(); focus(); openSubMenu(); }); this.addEventListener( EventType.touchend.getName(), evt -> { evt.stopPropagation(); double endTime = JsDate.now(); double diff = endTime - startTime[0]; if (diff < 200) { evt.preventDefault(); onSelected(evt); } }); this.addEventListener( EventType.click.getName(), evt -> { evt.stopPropagation(); evt.preventDefault(); onSelected(evt); }); this.addEventListener(EventType.mouseenter.getName(), evt -> openSubMenu()); } /** * Creates an indicator for the menu item. * *

This is used for visual indication, typically for sub-menu expansions or other visual cues. * * @param element the visual element representing the indicator * @return a lazy child representation of the indicator for later instantiation */ private LazyChild> createIndicator(IsElement element) { return LazyChild.of(elementOf(element), nestedIndicatorElement); } /** * Determines whether the menu item is selectable. * * @return true if the item is selectable, false otherwise */ public boolean isSelectable() { return selectable; } /** * Sets the selectable property of the menu item. * * @param selectable true to make the item selectable, false otherwise * @param the type of the menu item * @return the current instance of the menu item */ public > T setSelectable(boolean selectable) { this.selectable = selectable; return (T) this; } private void onSelected(Event evt) { if (parent.isMultiSelect() && isSelected()) { deselect(); } else { select(); } } /** * Gets the target element to which child elements can be appended. * * @return the body element of the menu item */ @Override public Element getAppendTarget() { return bodyElement.element(); } /** * Determines whether the menu item is searchable. * * @return true if the item is searchable, false otherwise */ public boolean isSearchable() { return searchable; } /** * Sets the searchable property of the menu item. * * @param searchable true to make the item searchable, false otherwise * @param the type of the menu item * @return the current instance of the menu item */ public > T setSearchable(boolean searchable) { this.searchable = searchable; return (T) this; } /** * Performs a search on the menu item based on the given token. * *

This method typically determines the visibility of the menu item based on the search token. * * @param token the search token or keyword * @param caseSensitive determines if the search should consider case sensitivity * @return always returns {@code false}; the reason for this should be provided based on the * method's context */ public boolean onSearch(String token, boolean caseSensitive) { if (isNull(token) || token.isEmpty()) { this.show(); } else { hide(); } return false; } /** * Selects the menu item without notifying the selection handlers. * * @return the current instance of the menu item */ public AbstractMenuItem select() { return select(false); } /** * Deselects the menu item without notifying the deselection handlers. * * @return the current instance of the menu item */ public AbstractMenuItem deselect() { return deselect(false); } /** * Selects the menu item. * *

Adds selection styling and notifies the selection handlers if not silent. * * @param silent if {@code true}, the selection handlers won't be notified * @return the current instance of the menu item */ public > T select(boolean silent) { if (!isDisabled() && isSelectable()) { addCss( ConditionalCssClass.of(dui_menu_item_selected, () -> parent.isPreserveSelectionStyles())); setAttribute("selected", true); if (!silent) { selectionHandlers.forEach(handler -> handler.onSelection(this)); } if (nonNull(parent)) { parent.onItemSelected(this, silent); } } return (T) this; } /** * Deselects the menu item. * *

Removes selection styling and notifies the deselection handlers if not silent. * * @param silent if {@code true}, the deselection handlers won't be notified * @return the current instance of the menu item */ public > T deselect(boolean silent) { if (!isDisabled() && isSelectable()) { dui_menu_item_selected.remove(this); setAttribute("selected", false); if (!silent) { deselectionHandlers.forEach(DeselectionHandler::onDeselection); } if (nonNull(parent)) { parent.onItemDeselected(this, silent); } } return (T) this; } /** * Checks if the menu item is currently selected. * * @return {@code true} if the menu item is selected, {@code false} otherwise */ public boolean isSelected() { return Optional.ofNullable(getAttribute("selected")).map(Boolean::parseBoolean).orElse(false); } /** * Adds a selection handler to the menu item. * *

The provided handler will be invoked when the menu item is selected. * * @param selectionHandler the handler to be added * @return the current instance of the menu item */ @Override public AbstractMenuItem addSelectionHandler( HasSelectionHandler.SelectionHandler> selectionHandler) { if (nonNull(selectionHandler)) { selectionHandlers.add(selectionHandler); } return this; } /** * Removes a previously added selection handler from the menu item. * * @param selectionHandler the handler to be removed * @return the current instance of the menu item */ @Override public AbstractMenuItem removeSelectionHandler( HasSelectionHandler.SelectionHandler> selectionHandler) { if (nonNull(selectionHandler)) { selectionHandlers.remove(selectionHandler); } return this; } /** * Adds a deselection handler to the menu item. * *

The provided handler will be invoked when the menu item is deselected. * * @param deselectionHandler the handler to be added * @return the current instance of the menu item */ @Override public AbstractMenuItem addDeselectionHandler(DeselectionHandler deselectionHandler) { if (nonNull(deselectionHandler)) { deselectionHandlers.add(deselectionHandler); } return this; } /** * Sets focus on the clickable element of the menu item. * * @return the current instance of the menu item */ public AbstractMenuItem focus() { getClickableElement().focus(); return this; } /** * Sets the parent menu for this menu item. * * @param menu the parent menu */ void setParent(Menu menu) { this.parent = menu; } /** * Retrieves the key associated with this menu item. * * @return the key of the menu item */ public String getKey() { return key; } /** * Sets the key for this menu item. * *

This can be useful for programmatically distinguishing menu items. * * @param key the key to set * @return the current instance of the menu item with the specified key set */ public > T setKey(String key) { this.key = key; return (T) this; } /** * Retrieves the value associated with this menu item. * * @return the value of the menu item */ public V getValue() { return value; } /** * Sets the value for this menu item. * *

This can represent any associated data or context for the item. * * @param value the value to set */ public void setValue(V value) { this.value = value; } /** * Assigns a value to the menu item and returns the item instance. * *

This is a fluid API method to allow chained calls. * * @param value the value to set * @return the current instance of the menu item with the specified value set */ public > T withValue(V value) { setValue(value); return (T) this; } /** * Retrieves the nesting indicator for this menu item. * *

The nesting indicator typically indicates sub-menu existence. * * @return the current nesting indicator of the menu item */ public LazyChild> getNestingIndicator() { return indicatorIcon; } /** * Sets a new nesting indicator for the menu item. * *

This replaces the existing nesting indicator with the provided one. * * @param nestingIndicator the new nesting indicator element * @return the current instance of the menu item with the specified nesting indicator set */ public AbstractMenuItem setNestingIndicator(IsElement nestingIndicator) { if (nonNull(nestingIndicator)) { indicatorIcon.remove(); indicatorIcon = createIndicator(elementOf(nestingIndicator)); indicatorIcon.get(); } return this; } /** * Sets a sub-menu for this menu item. * * @param menu the sub-menu to be associated with this item * @return the current instance of the menu item with the specified sub-menu set */ public AbstractMenuItem setMenu(Menu menu) { this.menu = menu; if (nonNull(this.menu)) { this.menu.setAttribute("domino-sub-menu", true); this.menu.removeAttribute("domino-ui-root-menu"); indicatorIcon.get(); this.menu.setTargetElement(this); this.menu.setDropDirection(new BestFitSideDropDirection()); this.menu.setParentItem(this); } else { this.indicatorIcon.remove(); } return this; } /** * Opens the sub-menu associated with this menu item. If there's no sub-menu, it will close the * current open menu after a delay. */ public void openSubMenu() { if (nonNull(menu)) { DelayedExecution.execute( () -> { if (nonNull(parent)) { this.menu.setParent(parent); if (!this.parent.isDropDown() || (this.parent.isDropDown() && this.parent.isOpened())) { this.parent.openSubMenu(this.menu); } } }, 200); } else { DelayedExecution.execute( () -> { if (nonNull(parent)) { parent.closeCurrentOpen(); } }, 200); } } /** Opens the associated sub-menu immediately, closing other popups. */ private void openSelfMenu() { PopupsCloser.close(); this.menu.open(); this.parent.setCurrentOpen(this.menu); } /** Callback for when the parent menu is closed. */ void onParentClosed() { closeSubMenu(); } /** * Closes the associated sub-menu of this menu item. * * @return the current instance of the menu item */ public AbstractMenuItem closeSubMenu() { if (nonNull(this.menu)) { this.menu.close(); } return this; } /** * Binds this menu item to a specific menu item group. * * @param group the menu item group to bind to * @return the current instance of the menu item bound to the specified group */ AbstractMenuItem bindToGroup(MenuItemsGroup group) { this.itemGroup = group; return this; } /** * Unbinds this menu item from any associated menu item group. * * @return the current instance of the menu item unbound from any group */ AbstractMenuItem unbindGroup() { this.itemGroup = null; return this; } /** * Checks if the menu item is associated with a menu item group. * * @return true if the menu item is part of a group, false otherwise */ public boolean isGrouped() { return Optional.ofNullable(this.itemGroup).isPresent(); } /** * Retrieves the parent menu of this menu item. * * @return the parent menu of this item */ public Menu getParent() { return this.parent; } /** * Determines if this menu item has an associated sub-menu. * * @return true if there is a sub-menu, false otherwise */ public boolean hasMenu() { return nonNull(this.menu); } /** * Retrieves the clickable element associated with this menu item. * * @return the HTML element that can be clicked to trigger this menu item */ @Override public HTMLElement getClickableElement() { return linkElement.element(); } /** * Appends a child to the postfix element. * * @param postfixAddOn the postfix add-on to append * @param the type of the menu item * @return the current instance of the menu item */ public > T appendChild(PostfixAddOn postfixAddOn) { postfixElement.appendChild(postfixAddOn); return (T) this; } /** * Appends a child to the prefix element. * * @param prefixAddOn the prefix add-on to append * @param the type of the menu item * @return the current instance of the menu item */ public > T appendChild(PrefixAddOn prefixAddOn) { prefixElement.appendChild(prefixAddOn); return (T) this; } /** * Retrieves the current {@link MenuSearchFilter} used for search operations. * * @return the current {@link MenuSearchFilter} */ public MenuSearchFilter getSearchFilter() { return searchFilter; } /** * Sets the {@link MenuSearchFilter} to be used during search operations. * * @param searchFilter the search filter to set * @return this Menu item instance for chaining */ public > T setSearchFilter(MenuSearchFilter searchFilter) { this.searchFilter = searchFilter; return (T) this; } /** * Check if the menu item text starts with a specific string * * @param character the text to check against. * @return boolean, true if the menu item starts with the text, false otherwise. */ public boolean startsWith(String character) { return false; } /** * Returns the underlying DOM element. * * @return the DOM element of the menu item */ @Override public HTMLLIElement element() { return root.element(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy