/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ui.popup;
import elemental.dom.Element;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventTarget;
import elemental.events.MouseEvent;
import elemental.html.ClientRect;
import elemental.html.Window;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.user.client.Timer;
import org.eclipse.che.ide.util.dom.Elements;
import static elemental.css.CSSStyleDeclaration.Unit.PX;
/**
* Popup widow that hides itself on outside mouse down actions..
*/
public abstract class PopupWidget<T> {
private static final int MIN_WIDTH = 400;
private static final int MIN_HEIGHT = 130;
protected final Element popupBodyElement;
/** The list (ul) element for the popup. */
private final Element listElement;
private final EventListener popupListener;
/**
* The keyboard listener in the popup.
*/
private final EventListener keyboardListener;
protected final PopupResources popupResources;
/**
* The previously focused element.
*/
private Element previousFocus;
/** The main element for the popup. */
private Element popupElement;
public PopupWidget(final PopupResources popupResources, String title) {
this.popupResources = popupResources;
popupElement = Elements.createDivElement(popupResources.popupStyle().popup());
Element headerElement = Elements.createDivElement(popupResources.popupStyle().header());
headerElement.setInnerText(title);
popupElement.appendChild(headerElement);
popupBodyElement = Elements.createDivElement(popupResources.popupStyle().body());
popupElement.appendChild(popupBodyElement);
listElement = Elements.createUListElement();
popupBodyElement.appendChild(listElement);
popupListener = new EventListener() {
@Override
public void handleEvent(final Event evt) {
if (evt instanceof MouseEvent) {
final MouseEvent mouseEvent = (MouseEvent) evt;
final EventTarget target = mouseEvent.getTarget();
if (target instanceof Element) {
final Element elementTarget = (Element) target;
if (!PopupWidget.this.popupElement.contains(elementTarget)) {
hide();
evt.preventDefault();
}
}
}
// else won't happen
}
};
keyboardListener = new PopupKeyDownListener(this, this.listElement);
}
public abstract String getEmptyMessage();
/** Create an element for the given item data. */
public abstract Element createItem(final T itemModel);
/**
* Show the widget at the given document position.
* @param left the horizontal pixel position in the document
* @param top the vertical pixel position in the document
*/
public void show(final float left, final float top) {
if (!listElement.hasChildNodes()) {
Element emptyElement = Elements.createLiElement(popupResources.popupStyle().item());
emptyElement.setTextContent(getEmptyMessage());
listElement.appendChild(emptyElement);
return;
}
/* Reset popup dimensions and show. */
popupElement.getStyle().setLeft(left, PX);
popupElement.getStyle().setTop(top, PX);
popupElement.getStyle().setWidth("" + MIN_WIDTH + "px");
popupElement.getStyle().setHeight("" + MIN_HEIGHT + "px");
popupElement.getStyle().setOpacity(0);
Elements.getDocument().getBody().appendChild(popupElement);
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
popupElement.getStyle().setOpacity(1);
}
});
Elements.getDocument().addEventListener(Event.MOUSEDOWN, popupListener, false);
// does it fit inside the doc body?
// This does exactly the same thing for height/top and width/left
final Window window = Elements.getWindow();
final int winX = window.getInnerWidth();
final int winY = window.getInnerHeight();
ClientRect widgetRect = this.popupElement.getBoundingClientRect();
if (widgetRect.getBottom() > winY) {
// it doesn't fit
final float overflow = widgetRect.getBottom() - winY;
if (widgetRect.getHeight() - overflow > MIN_HEIGHT) {
// the widget can be shrunk to fit
this.popupElement.getStyle().setHeight(widgetRect.getHeight() - overflow, PX);
} else {
// we need to shrink AND move the widget up
this.popupElement.getStyle().setHeight(MIN_HEIGHT, PX);
final int newTop = Math.max(winY - MIN_HEIGHT, MIN_HEIGHT);
this.popupElement.getStyle().setTop(newTop, PX);
}
}
// bounding rect has changed
widgetRect = this.popupElement.getBoundingClientRect();
if (widgetRect.getRight() > winX) {
// it doesn't fit
final float overflow = widgetRect.getRight() - winX;
if (widgetRect.getWidth() - overflow > MIN_WIDTH) {
// the widget can be shrunk to fit
this.popupElement.getStyle().setWidth(widgetRect.getWidth() - overflow, PX);
} else {
// we need to shrink AND move the widget up
this.popupElement.getStyle().setWidth(MIN_WIDTH, PX);
final int newLeft = Math.max(winX - MIN_WIDTH, MIN_WIDTH);
this.popupElement.getStyle().setLeft(newLeft - MIN_WIDTH, PX);
}
}
if (needsFocus()) {
// save previous focus and set focus in popup
previousFocus = Elements.getDocument().getActiveElement();
listElement.getFirstElementChild().focus();
}
// add key event listener on popup
listElement.addEventListener(Event.KEYDOWN, keyboardListener, false);
}
/**
* Add an item in the popup view.
* @param itemModel the data for the item
*/
public void addItem(final T itemModel) {
if (itemModel == null) {
return;
}
Element element = createItem(itemModel);
element.setTabIndex(1);
listElement.appendChild(element);
}
/** Hide the popup. */
public void hide() {
// restore previous focus state
if (previousFocus != null) {
previousFocus.focus();
previousFocus = null;
}
popupElement.getStyle().setOpacity(0);
new Timer() {
@Override
public void run() {
if (popupElement != null) {
// detach assist popup
popupElement.getParentNode().removeChild(popupElement);
popupElement = null;
}
// remove all items from popup element
listElement.setInnerHTML("");
}
}.schedule(250);
// remove the keyboard listener
listElement.removeEventListener(Event.KEYDOWN, keyboardListener, false);
// remove the mouse listener
Elements.getDocument().removeEventListener(Event.MOUSEDOWN, popupListener);
}
/**
* Action taken when an item is validated.
* @param itemElement the validated item
*/
public void validateItem(final Element itemElement) {
// by default, only hide the popup
hide();
}
/**
* Returns the widget viewed as an element.
* @return the element
*/
public Element asElement() {
return this.popupElement;
}
/**
* Tells if the popup widget wants focus.<br/>
* Override the method to match needed value.
* @return true iff the widget needs the focus
*/
public boolean needsFocus() {
return false;
}
}