com.google.collide.client.util.HoverController.java Source code

Java tutorial

Introduction

Here is the source code for com.google.collide.client.util.HoverController.java

Source

// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.collide.client.util;

import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.user.client.Timer;

import elemental.dom.Node;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventRemover;
import elemental.events.EventTarget;
import elemental.events.MouseEvent;
import elemental.html.Element;

/**
 * Controller to manage a group of elements that are hovered and unhovered
 * together. For example, you can use this controller to link a button to its
 * submenu.
 * 
 * When the user mouses over any of the "partner" elements, the controller calls
 * {@link HoverListener#onHover()}. When the user mouses out of all partner
 * elements and does not mouse over one of the elements within a fixed delay,
 * the controller calls {@link UnhoverListener#onUnhover()}.
 * 
 * The default delay is 1300ms, but you can override this. We recommend using
 * one of the static values so that similar UI components use the same delay.
 */
public class HoverController {

    /**
     * The default unhover delay.
     */
    public static final int DEFAULT_UNHOVER_DELAY = 1300;

    /**
     * The unhover delay used for dropdown UI components, such as button menus.
     */
    public static final int DROP_DOWN_UNHOVER_DELAY = 300;

    /**
     * Listener interface to be notified of hover state changes.
     */
    public static interface HoverListener {
        /**
         * Handles the event when the user hovers one of the partner elements.
         */
        public void onHover();
    }

    /**
     * Listener interface to be notified of hover state changes.
     */
    public static interface UnhoverListener {
        /**
         * Handles the event when the user unhovers one of the partner elements, and
         * does not hover another partner element within the fixed delay.
         */
        public void onUnhover();
    }

    /**
     * A helper class to store a partner element and it's event removers.
     */
    private class PartnerHolder {
        private final Element element;
        private final EventRemover mouseOverRemover;
        private final EventRemover mouseOutRemover;

        PartnerHolder(final Element element) {
            this.element = element;
            mouseOverRemover = element.addEventListener(Event.MOUSEOVER, new EventListener() {
                @Override
                public void handleEvent(Event evt) {
                    if (relatedTargetOutsideElement((MouseEvent) evt)) {
                        hover();
                    }
                }
            }, false);
            mouseOutRemover = element.addEventListener(Event.MOUSEOUT, new EventListener() {
                @Override
                public void handleEvent(Event evt) {
                    if (relatedTargetOutsideElement((MouseEvent) evt)) {
                        unhover();
                    }
                }
            }, false);
        }

        Element getElement() {
            return element;
        }

        void teardown() {
            mouseOverRemover.remove();
            mouseOutRemover.remove();
        }

        /**
         * Checks if the related target of the MouseEvent (the "from" element for a
         * mouseover, the "to" element for a mouseout) is actually outside of the
         * partner element. If the target element contains children, we will receive
         * mouseover/mouseout events when the mouse moves over/out of the children,
         * even if the mouse is still within the partner element. These
         * intra-element events don't affect the hover state of the partner element,
         * so we want to ignore them.
         */
        private boolean relatedTargetOutsideElement(MouseEvent evt) {
            EventTarget relatedTarget = evt.getRelatedTarget();
            return relatedTarget == null || !element.contains((Node) relatedTarget);
        }
    }

    private HoverListener hoverListener;
    private UnhoverListener unhoverListener;
    private boolean isHovering = false;
    private int unhoverDelay = DEFAULT_UNHOVER_DELAY;
    private Timer unhoverTimer;
    private final JsonArray<PartnerHolder> partners = JsonCollections.createArray();

    /**
     * Adds a partner element to this controller. See class javadoc for
     * an explanation of the interaction between partner elements.
     */
    public void addPartner(Element element) {
        if (!hasPartner(element)) {
            partners.add(new PartnerHolder(element));
        }
    }

    /**
     * Removes a partner element from this controller.
     */
    public void removePartner(Element element) {
        for (int i = 0, n = partners.size(); i < n; ++i) {
            PartnerHolder holder = partners.get(i);
            if (holder.getElement() == element) {
                holder.teardown();
                partners.remove(i);
                break;
            }
        }
    }

    private boolean hasPartner(Element element) {
        for (int i = 0, n = partners.size(); i < n; ++i) {
            PartnerHolder holder = partners.get(i);
            if (holder.getElement() == element) {
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the listener that will receive events when any of the partner elements
     * is hovered.
     */
    public void setHoverListener(HoverListener listener) {
        this.hoverListener = listener;
    }

    /**
     * Sets the listener that will receive events when all of the partner elements
     * are unhovered.
     */
    public void setUnhoverListener(UnhoverListener listener) {
        this.unhoverListener = listener;
    }

    /**
     * Sets the delay between the last native mouseout event and when
     * {@link UnhoverListener#onUnhover()} is called. If the user mouses out of
     * one partner element and over another partner element within the unhover
     * delay, the unhover event is not triggered.
     * 
     * If the delay is zero, the unhover listener is called synchronously. If the
     * delay is less than zero, the unhover listener is never called.
     * 
     * @param delay the delay in milliseconds
     */
    public void setUnhoverDelay(int delay) {
        this.unhoverDelay = delay;
    }

    /**
     * Cancels the unhover timer if one is pending. This will prevent an unhover
     * listener from firing until the next time the user mouses out of a partner
     * element.
     */
    public void cancelUnhoverTimer() {
        if (unhoverTimer != null) {
            unhoverTimer.cancel();
            unhoverTimer = null;
        }
    }

    /**
     * Flushes the unhover timer if one is pending. This will reset the hover
     * controller to a state where it can fire a hover event the next time the
     * element is hovered.
     */
    public void flushUnhoverTimer() {
        if (unhoverTimer != null) {
            cancelUnhoverTimer();
            unhoverNow();
        }
    }

    /**
     * Updates the state of the controller to indicate that the user is hovering
     * over one of the partner elements.
     */
    private void hover() {
        cancelUnhoverTimer();

        // Early exit if already hovering.
        if (isHovering) {
            return;
        }

        isHovering = true;
        if (hoverListener != null) {
            hoverListener.onHover();
        }
    }

    /**
     * Starts a timer that will update the controller to the unhover state if the
     * user doesn't hover one of the partner elements within the specified unhover
     * delay.
     */
    private void unhover() {
        // Early exit if already unhovering or if the delay is negative.
        if (!isHovering || unhoverDelay < 0) {
            return;
        }

        if (unhoverDelay == 0) {
            unhoverNow();
        } else if (unhoverTimer == null) {
            // Wait a short time before unhovering so the user has a chance to move
            // the mouse from one partner to another.
            unhoverTimer = new Timer() {
                @Override
                public void run() {
                    unhoverNow();
                }
            };
            unhoverTimer.schedule(unhoverDelay);
        }
    }

    /**
     * Updates the state of the controller to indicate that the user is no longer
     * hovering any of the partner elements.
     */
    private void unhoverNow() {
        cancelUnhoverTimer();
        isHovering = false;
        if (unhoverListener != null) {
            unhoverListener.onUnhover();
        }
    }
}