// 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.search.awesomebox.host; import com.google.collide.client.search.awesomebox.host.AwesomeBoxComponent.HiddenBehavior; import com.google.collide.client.search.awesomebox.host.AwesomeBoxComponent.HideMode; import com.google.collide.client.search.awesomebox.host.AwesomeBoxComponent.ShowReason; import com.google.collide.client.util.Elements; import com.google.collide.mvp.CompositeView; import com.google.collide.mvp.UiComponent; import com.google.collide.shared.util.ListenerManager; import com.google.collide.shared.util.ListenerRegistrar; import com.google.collide.shared.util.ListenerManager.Dispatcher; import com.google.common.base.Preconditions; import com.google.gwt.resources.client.CssResource; import elemental.dom.Node; import elemental.events.Event; import elemental.events.EventListener; import elemental.events.EventRemover; import elemental.events.KeyboardEvent; import elemental.events.KeyboardEvent.KeyCode; import elemental.html.DivElement; import elemental.html.Element; /** * The host control of the {@link AwesomeBoxComponent}s and related components. * It performs very little other than management of {@link AwesomeBoxComponent}s * and focus/cancel actions. * */ public class AwesomeBoxComponentHost extends UiComponent<AwesomeBoxComponentHost.View> { public interface Css extends CssResource { String container(); String base(); } /** * A small class which wraps the currently visible * {@link AwesomeBoxComponent}. */ public class HostedComponent implements ComponentHost { public final AwesomeBoxComponent component; public HostedComponent(AwesomeBoxComponent component) { this.component = component; } @Override public void requestHide() { if (current != this) { // This is stale, the component is already hidden. return; } hideImpl(AwesomeBoxComponentHiddenListener.Reason.OTHER); } } /** * Allows an object that is not a section to listen in when the AwesomeBox is * hiding/showing. */ public interface AwesomeBoxComponentHiddenListener { public enum Reason { /** * An event occurred which canceled the user's interaction with the * AwesomeBox such as pressing the ESC button. */ CANCEL_EVENT, /** * An external click occurred triggering an autohide. */ EXTERNAL_CLICK, /** * The component was hidden programatically, or by the component. */ OTHER } public void onHidden(Reason reason); } public interface ViewEvents { public void onExternalClick(); public void onClick(); public void onEscapePressed(); } public static class View extends CompositeView<ViewEvents> { private final EventListener bodyListener = new EventListener() { @Override public void handleEvent(Event evt) { if (getDelegate() != null && !getElement().contains((Node) evt.getTarget())) { getDelegate().onExternalClick(); } } }; private final DivElement baseElement; private EventRemover bodyRemover; public View(Element container, Css css) { super(container); container.addClassName(css.container()); baseElement = Elements.createDivElement(css.base()); baseElement.setTextContent("Actions"); container.appendChild(baseElement); attachEvents(); } void attachEvents() { baseElement.addEventListener(Event.CLICK, new EventListener() { @Override public void handleEvent(Event evt) { if (getDelegate() != null) { getDelegate().onClick(); } } }, false); getElement().addEventListener(Event.KEYUP, new EventListener() { @Override public void handleEvent(Event evt) { KeyboardEvent event = (KeyboardEvent) evt; if (event.getKeyCode() == KeyCode.ESC && getDelegate() != null) { getDelegate().onEscapePressed(); } } }, false); } public void setBaseActive(boolean active) { Preconditions.checkState(!isBaseActive() == active, "Invalid base element state!"); if (!active) { baseElement.removeFromParent(); } else { getElement().appendChild(baseElement); } } public boolean isBaseActive() { return baseElement.getParentElement() != null; } public void attachComponentElement(Element component) { Preconditions.checkState(!isBaseActive(), "Base cannot be attached"); getElement().appendChild(component); } public void setBodyListenerAttached(boolean shouldAttach) { boolean isListenerAttached = bodyRemover != null; Preconditions.checkState( isListenerAttached != shouldAttach, "Invalid listener attachment state"); if (shouldAttach) { bodyRemover = Elements.getBody().addEventListener(Event.MOUSEDOWN, bodyListener, false); } else { bodyRemover.remove(); bodyRemover = null; } } } public class ViewEventsImpl implements ViewEvents { @Override public void onClick() { // only do a show if we're not showing anything already if (getView().isBaseActive()) { showImpl(ShowReason.CLICK); } } @Override public void onExternalClick() { if (model.getActiveComponent().getHideMode() == HideMode.AUTOHIDE) { hideImpl(AwesomeBoxComponentHiddenListener.Reason.EXTERNAL_CLICK); } } @Override public void onEscapePressed() { hideImpl(AwesomeBoxComponentHiddenListener.Reason.CANCEL_EVENT); } } private final HostedComponent NONE_SHOWING = new HostedComponent(null); private final ListenerManager<AwesomeBoxComponentHiddenListener> componentHiddenListener = ListenerManager.create(); private final AwesomeBoxComponentHostModel model; private HostedComponent current = NONE_SHOWING; public AwesomeBoxComponentHost(View view, AwesomeBoxComponentHostModel model) { super(view); this.model = model; view.setDelegate(new ViewEventsImpl()); } /** * Returns a {@link ListenerRegistrar} which can be used to listen for the * active component being hidden. */ public ListenerRegistrar<AwesomeBoxComponentHiddenListener> getComponentHiddenListener() { return componentHiddenListener; } /** * Hides the currently active component. */ public void hide() { if (isComponentActive()) { hideImpl(AwesomeBoxComponentHiddenListener.Reason.OTHER); } } /** * Displays the current component set in the * {@link AwesomeBoxComponentHostModel}. Any currently displayed component * will be removed from the DOM. */ public void show() { showImpl(ShowReason.OTHER); } private void showImpl(ShowReason reason) { if (current.component == model.getActiveComponent()) { current.component.focus(); return; } else if (isComponentActive()) { hide(); } current = new HostedComponent(model.getActiveComponent()); Preconditions.checkState(current.component != null, "There is no active component to host"); getView().setBaseActive(false); getView().setBodyListenerAttached(true); getView().attachComponentElement(current.component.getElement()); current.component.onShow(current, reason); current.component.focus(); } /** * @return true if a component is currently active. */ public boolean isComponentActive() { return current != NONE_SHOWING; } private void hideImpl(AwesomeBoxComponentHiddenListener.Reason reason) { Preconditions.checkNotNull( current != NONE_SHOWING, "There must be an active component to hide."); // Extract the current component and mark us as none showing AwesomeBoxComponent component = current.component; current = NONE_SHOWING; // remove the current component and reattach our base /* * NOTE: Removing parent seems to cause any queued DOM events for this * element to freak out, make sure current is already NONE_SHOWING to block * other hide attempts. If you don't you'll probably see things like * NOT_FOUND_ERR. This is especially true of the blur event used by the * AwesomeBox to hide. */ component.getElement().removeFromParent(); getView().setBaseActive(true); getView().setBodyListenerAttached(false); // Hide component, potentially revert, then dispatch the hidden listener component.onHide(); maybeRevertToDefaultComponent(component); dispatchHiddenListener(reason); } private void dispatchHiddenListener(final AwesomeBoxComponentHiddenListener.Reason reason) { componentHiddenListener.dispatch(new Dispatcher<AwesomeBoxComponentHiddenListener>() { @Override public void dispatch(AwesomeBoxComponentHiddenListener listener) { listener.onHidden(reason); } }); } private void maybeRevertToDefaultComponent(AwesomeBoxComponent component) { boolean isComponentTheModelsActiveComponent = component == model.getActiveComponent(); boolean isHiddenBehaviorRevert = component.getHiddenBehavior() == HiddenBehavior.REVERT_TO_DEFAULT; if (isComponentTheModelsActiveComponent && isHiddenBehaviorRevert) { model.revertToDefaultComponent(); } } }