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

org.dominokit.domino.ui.dropdown.DropDownMenu Maven / Gradle / Ivy

There is a newer version: 1.0.139
Show newest version
package org.dominokit.domino.ui.dropdown;

import elemental2.dom.*;
import jsinterop.base.Js;
import org.dominokit.domino.ui.grid.flex.FlexItem;
import org.dominokit.domino.ui.grid.flex.FlexLayout;
import org.dominokit.domino.ui.icons.Icons;
import org.dominokit.domino.ui.keyboard.KeyboardEvents;
import org.dominokit.domino.ui.modals.ModalBackDrop;
import org.dominokit.domino.ui.style.Color;
import org.dominokit.domino.ui.utils.BaseDominoElement;
import org.dominokit.domino.ui.utils.DominoElement;
import org.dominokit.domino.ui.utils.HasBackground;
import org.jboss.elemento.EventType;
import org.jboss.elemento.IsElement;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static elemental2.dom.DomGlobal.document;
import static java.util.Objects.nonNull;

import org.dominokit.domino.ui.icons.MdiIcon;

import static org.jboss.elemento.Elements.*;

public class DropDownMenu extends BaseDominoElement implements HasBackground {

    private MenuNavigation> menuNavigation;
    private DominoElement element = DominoElement.of(div().css(DropDownStyles.DROPDOWN));
    private DominoElement menuElement = DominoElement.of(ul().css(DropDownStyles.DROPDOWN_MENU));
    private HTMLElement targetElement;
    private DropDownPosition position = DropDownPosition.BOTTOM;
    private DominoElement titleContainer = DominoElement.of(div()).addCss(DropDownStyles.DROPDOWN_TITLE_CONTAINER);
    private DominoElement searchContainer = DominoElement.of(div().css(DropDownStyles.DROPDOWN_SEARCH_CONTAINER));
    private DominoElement searchBox = DominoElement.of(input("text")
            .css(DropDownStyles.DROPDOWN_SEARCH_BOX));
    private DominoElement noSearchResultsElement;
    private MdiIcon createIcon = Icons.ALL.plus_mdi().clickable();
    private String noMatchSearchResultText = "No results matched";

    private List> actions = new ArrayList<>();
    private static boolean touchMoved;
    private List closeHandlers = new ArrayList<>();
    private List openHandlers = new ArrayList<>();
    private boolean closeOnEscape;
    private boolean searchable;
    private boolean creatable;
    private boolean caseSensitiveSearch = false;
    private List> groups = new ArrayList<>();
    private Color background;
    private HTMLElement appendTarget = document.body;
    private AppendStrategy appendStrategy = AppendStrategy.LAST;
    private SearchFilter searchFilter = (searchText, dropdownAction, caseSensitive) -> {
        if (caseSensitive) {
            return dropdownAction.getContent().textContent.contains(searchText);
        } else {
            return dropdownAction.getContent().textContent.toLowerCase().contains(searchText.toLowerCase());
        }
    };
    private OnAdd addListener = (String search) -> {
    };

    static {
        document.addEventListener(EventType.click.getName(), evt -> DropDownMenu.closeAllMenus());
        document.addEventListener(EventType.touchmove.getName(), evt -> DropDownMenu.touchMoved = true);
        document.addEventListener(EventType.touchend.getName(), evt -> {
            if (!DropDownMenu.touchMoved) {
                closeAllMenus();
            }
            DropDownMenu.touchMoved = false;
        });
    }

    public DropDownMenu(HTMLElement targetElement) {
        this.targetElement = targetElement;

        init(this);

        menuElement.setAttribute("role", "listbox");

        element.addEventListener(EventType.touchend, Event::stopPropagation);
        element.addEventListener(EventType.touchmove, Event::stopPropagation);
        element.addEventListener(EventType.touchstart, Event::stopPropagation);

        addMenuNavigationListener();
        searchContainer.addClickListener(evt -> {
            evt.preventDefault();
            evt.stopPropagation();
        });
        searchContainer.appendChild(FlexLayout.create()
                .appendChild(FlexItem.create()
                        .appendChild(Icons.ALL.magnify_mdi().clickable())
                )
                .appendChild(FlexItem.create()
                        .setFlexGrow(1)
                        .appendChild(searchBox)
                ).appendChild(FlexItem.create()
                        .appendChild(createIcon
                                .setAttribute("tabindex", "0")
                                .setAttribute("aria-expanded", "true")
                                .setAttribute("href", "#")
                                .addClickListener(evt -> addListener.onAdd(searchBox.element().value))
                        )
                )
        );

        element
                .appendChild(searchContainer)
                .appendChild(menuElement);

        setSearchable(false);
        setCreatable(false);

        KeyboardEvents.listenOn(createIcon)
                .setDefaultOptions(KeyboardEvents.KeyboardEventOptions.create()
                        .setPreventDefault(true)
                        .setStopPropagation(true))
                .onEnter(evt -> addListener.onAdd(searchBox.element().value));

        KeyboardEvents.listenOn(searchBox)
                .setDefaultOptions(KeyboardEvents.KeyboardEventOptions.create()
                        .setPreventDefault(true)
                        .setStopPropagation(true))
                .onArrowUp(evt -> menuNavigation.focusAt(lastVisibleActionIndex()))
                .onArrowDown(evt -> menuNavigation.focusAt(firstVisibleActionIndex()))
                .onEscape(evt -> close())
                .onEnter(evt -> selectFirstSearchResult());
        searchBox.addEventListener("input", evt -> {
            if (searchable) {
                doSearch();
            }
        });

        setNoSearchResultsElement(DominoElement.of(li().css(DropDownStyles.NO_RESULTS))
                .hide()
                .element());
        menuElement.appendChild(noSearchResultsElement);

        titleContainer.addClickListener(Event::stopPropagation);
    }

    private void selectFirstSearchResult() {
        List> filteredAction = getFilteredAction();
        if (!filteredAction.isEmpty()) {
            selectAt(actions.indexOf(filteredAction.get(0)));
            filteredAction.get(0)
                    .select();
        }
    }

    private int firstVisibleActionIndex() {
        for (int i = 0; i < actions.size(); i++) {
            if (!actions.get(i).isHidden()) {
                return i;
            }
        }
        return 0;
    }

    private int lastVisibleActionIndex() {
        for (int i = actions.size() - 1; i >= 0; i--) {
            if (!actions.get(i).isHidden()) {
                return i;
            }
        }
        return 0;
    }

    private void doSearch() {
        String searchValue = searchBox.element().value;
        boolean thereIsValues = false;
        for (DropdownAction action : actions) {

            action.setFilteredOut(false);
            boolean contains = searchFilter.filter(searchValue, action, caseSensitiveSearch);
            contains = contains && !action.isExcludeFromSearchResults();

            if (!contains) {
                action.filter();
            } else {
                thereIsValues = true;
                action.deFilter();
            }
        }

        if (!searchValue.isEmpty() && creatable) {
            createIcon.active();
        } else {
            createIcon.inactive();
        }

        if (thereIsValues) {
            noSearchResultsElement.hide();
        } else {
            noSearchResultsElement.show();
            noSearchResultsElement.setTextContent(noMatchSearchResultText + " \"" + searchValue + "\"");
        }
        groups.forEach(DropdownActionsGroup::changeVisibility);
    }

    public List> getFilteredAction() {
        return actions
                .stream()
                .filter(dropdownAction -> !dropdownAction.isFilteredOut())
                .collect(Collectors.toList());
    }

    private void addMenuNavigationListener() {
        menuNavigation = MenuNavigation.create(actions)
                .onSelect(DropdownAction::select)
                .focusCondition(item -> !item.isHidden())
                .onFocus(item -> {
                    if (isOpened()) {
                        item.focus();
                    }
                })
                .onEscape(this::close);

        element.addEventListener("keydown", menuNavigation);
    }

    public static void closeAllMenus() {
        NodeList elementsByName = document.body.querySelectorAll(".dropdown");
        for (int i = 0; i < elementsByName.length; i++) {
            HTMLElement item = Js.uncheckedCast(elementsByName.item(i));
            close(item);
        }
    }

    private static void close(HTMLElement item) {
        item.remove();
    }

    public static DropDownMenu create(HTMLElement targetElement) {
        return new DropDownMenu(targetElement);
    }

    public static DropDownMenu create(IsElement targetElement) {
        return new DropDownMenu(targetElement.element());
    }

    public DropDownMenu insertFirst(DropdownAction action) {
        action.addSelectionHandler(value -> {
            if (action.isAutoClose()) {
                close();
            }
        });
        actions.add(0, action);
        menuElement.insertFirst(action.element());
        return this;
    }

    public DropDownMenu appendChild(DropdownAction action) {
        action.addSelectionHandler(value -> {
            if (action.isAutoClose()) {
                close();
            }
        });
        actions.add(action);
        menuElement.appendChild(action.element());
        action.setBackground(this.background);
        return this;
    }

    public DropDownMenu addAction(DropdownAction action) {
        return appendChild(action);
    }

    public DropDownMenu separator() {
        menuElement.appendChild(li().attr("role", "separator")
                .css(DropDownStyles.DIVIDER));
        return this;
    }

    public DropDownMenu appendChild(Node child) {
        element.appendChild(child);
        return this;
    }

    public void close() {
        element.remove();
        closeHandlers.forEach(CloseHandler::onClose);
    }

    public void open() {
        open(true);
    }

    public void open(boolean focus) {
        if (hasActions() || creatable) {
            onAttached(mutationRecord -> {
                position.position(element.element(), targetElement);
                if (searchable) {
                    searchBox.element().focus();
                    clearSearch();
                } else if (focus) {
                    focus();
                }

                element.style().setProperty("z-index", ModalBackDrop.getNextZIndex() + 10 + "");
                openHandlers.forEach(OpenHandler::onOpen);

                DominoElement.of(targetElement)
                        .onDetached(targetDetach -> close());

                onDetached(detachRecord -> {
                    closeHandlers.forEach(CloseHandler::onClose);
                });
            });

            if (!appendTarget.contains(element.element())) {
                appendStrategy.onAppend(appendTarget, element.element());
            }
        }
    }

    public void clearSearch() {
        searchBox.element().value = "";
        noSearchResultsElement.hide();
        createIcon.inactive();
        actions.forEach(DropdownAction::show);
    }

    public boolean isOpened() {
        return element.isAttached();
    }

    public DropDownMenu setPosition(DropDownPosition position) {
        this.position = position;
        return this;
    }

    @Override
    public HTMLDivElement element() {
        return element.element();
    }

    public DropDownMenu clearActions() {
        menuElement.clearElement();
        actions.clear();
        groups.clear();
        menuElement.appendChild(noSearchResultsElement);
        return this;
    }

    public boolean hasActions() {
        return !actions.isEmpty();
    }

    public DropDownMenu selectAt(int index) {
        if (index >= 0 && index < actions.size()) {
            menuNavigation.focusAt(index);
        }
        return this;
    }

    public DropDownMenu addCloseHandler(CloseHandler closeHandler) {
        closeHandlers.add(closeHandler);
        return this;
    }

    public DropDownMenu removeCloseHandler(CloseHandler closeHandler) {
        closeHandlers.remove(closeHandler);
        return this;
    }

    public DropDownMenu addOpenHandler(OpenHandler openHandler) {
        openHandlers.add(openHandler);
        return this;
    }

    public DropDownMenu removeOpenHandler(OpenHandler openHandler) {
        openHandlers.remove(openHandler);
        return this;
    }

    public List> getActions() {
        return actions;
    }

    public DropDownMenu setCloseOnEscape(boolean closeOnEscape) {
        this.closeOnEscape = closeOnEscape;
        return this;
    }

    public DropDownMenu setSearchable(boolean searchable) {
        this.searchable = searchable;
        if (searchable) {
            searchContainer.show();
        } else {
            searchContainer.hide();
        }
        return this;
    }

    public DropDownMenu setCreatable(boolean creatable) {
        this.creatable = creatable;
        if (creatable) {
            createIcon.show();
        } else {
            createIcon.hide();
        }
        return this;
    }
    
    /**
     * Set the text which is displayed in case nothing is found at a searchable {@link DropDownMenu}
     * Default is "No results matched".
     * @param text
     * @return 
     */
    public DropDownMenu setNoMatchSearchResultText(String text) {
        this.noMatchSearchResultText = text;
        return this;
    }

    public DropDownMenu setOnAddListener(OnAdd onAdd) {
        this.addListener = onAdd;
        return this;
    }

    public DropDownMenu addGroup(DropdownActionsGroup group) {
        groups.add(group);
        menuElement.appendChild(group.element());
        group.bindTo(this);
        return this;
    }

    public DropDownMenu setTitle(String title) {
        if (!element.contains(titleContainer)) {
            element.insertFirst(titleContainer.appendChild(h(5).textContent(title)));
        }
        return this;
    }

    public DropDownMenu setAppendTarget(HTMLElement appendTarget) {
        if (nonNull(appendTarget)) {
            this.appendTarget = appendTarget;
        }
        return this;
    }

    public HTMLElement getAppendTarget() {
        return this.appendTarget;
    }

    public DropDownMenu setAppendStrategy(AppendStrategy appendStrategy) {
        if (nonNull(appendStrategy)) {
            this.appendStrategy = appendStrategy;
        }
        return this;
    }

    public AppendStrategy getAppendStrategy() {
        return this.appendStrategy;
    }

    public DominoElement getNoSearchResultsElement() {
        return noSearchResultsElement;
    }

    public void setNoSearchResultsElement(HTMLElement noSearchResultsElement) {
        this.noSearchResultsElement = DominoElement.of(noSearchResultsElement);
    }

    public boolean isCaseSensitiveSearch() {
        return caseSensitiveSearch;
    }

    public void setCaseSensitiveSearch(boolean caseSensitiveSearch) {
        this.caseSensitiveSearch = caseSensitiveSearch;
    }

    public DominoElement getMenuElement() {
        return menuElement;
    }

    @Override
    public DropDownMenu setBackground(Color background) {
        if (nonNull(this.background)) {
            getMenuElement().removeCss(this.background.getBackground());
        }
        getMenuElement().addCss(background.getBackground());
        this.background = background;
        actions.forEach(dropdownAction -> dropdownAction.setBackground(background));
        return this;
    }

    public DominoElement getSearchContainer() {
        return searchContainer;
    }

    public void focus() {
        menuNavigation.focusAt(0);
    }

    public SearchFilter getSearchFilter() {
        return searchFilter;
    }

    public DropDownMenu setSearchFilter(SearchFilter searchFilter) {
        if (nonNull(searchFilter)) {
            this.searchFilter = searchFilter;
        }
        return this;
    }

    @FunctionalInterface
    public interface CloseHandler {
        void onClose();
    }

    @FunctionalInterface
    public interface OpenHandler {
        void onOpen();
    }

    @FunctionalInterface
    public interface AppendStrategy {
        void onAppend(HTMLElement target, HTMLElement menu);

        AppendStrategy FIRST = (target, menu) -> DominoElement.of(target).insertFirst(menu);
        AppendStrategy LAST = (target, menu) -> DominoElement.of(target).appendChild(menu);
    }

    @FunctionalInterface
    public interface SearchFilter {
        boolean filter(String searchText, DropdownAction dropdownAction, boolean caseSensitive);
    }

    @FunctionalInterface
    public interface OnAdd {
        void onAdd(String input);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy