// 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.ui.popup;
import com.google.collide.client.util.Elements;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiTemplate;
import com.google.gwt.user.client.Timer;
import javax.annotation.Nullable;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.KeyboardEvent.KeyCode;
import elemental.js.events.JsKeyboardEvent;
import elemental.js.html.JsElement;
/**
* A popup that automatically centers its content, even if the dimensions of the content change. The
* centering is done in CSS, so performance is very good. A semi-transparent "glass" panel appears
* behind the popup. The glass is not optional due to the way {@link CenterPanel} is implemented.
*
* <p>
* {@link CenterPanel} animates into and out of view using the shrink in/expand out animation.
* </p>
*/
public class CenterPanel extends UiComponent<CenterPanel.View> {
/**
* Constructs a new {@link CenterPanel} that contains the specified content
* element.
*
* @param resources the resources to apply to the popup
* @param content the content to display in the center of the popup
* @return a new {@link CenterPanel} instance
*/
public static CenterPanel create(Resources resources, elemental.html.Element content) {
View view = new View(resources, content);
return new CenterPanel(view);
}
/**
* The resources used by this UI component.
*/
public interface Resources extends ClientBundle {
@Source({"com/google/collide/client/common/constants.css", "CenterPanel.css"})
Css centerPanelCss();
}
/**
* The Css Style names used by this panel.
*/
public interface Css extends CssResource {
/**
* Returns duration of the popup animation in milliseconds.
*/
int animationDuration();
String content();
String contentVisible();
String glass();
String glassVisible();
String popup();
String positioner();
}
/**
* The events sources by the View.
*/
private interface ViewEvents {
void onEscapeKey();
}
/**
* The view that renders the {@link CenterPanel}. The View consists of a glass
* panel that fades out the background, and a DOM structure that positions the
* contents in the exact center of the screen.
*/
public static class View extends CompositeView<ViewEvents> {
@UiTemplate("CenterPanel.ui.xml")
interface MyBinder extends UiBinder<Element, View> {
}
private static MyBinder uiBinder = GWT.create(MyBinder.class);
final Resources res;
@UiField(provided = true)
final Css css;
@UiField
DivElement contentContainer;
@UiField
DivElement glass;
@UiField
DivElement popup;
View(Resources res, elemental.html.Element content) {
this.res = res;
this.css = res.centerPanelCss();
setElement(Elements.asJsElement(uiBinder.createAndBindUi(this)));
Elements.asJsElement(contentContainer).appendChild(content);
handleEvents();
}
/**
* Returns the duration of the popup animation in milliseconds. The return
* value should equal the value of {@link Css#animationDuration()}.
*/
protected int getAnimationDuration() {
return css.animationDuration();
}
/**
* Updates the View to reflect the showing state of the popup.
*
* @param showing true if showing, false if not.
*/
protected void setShowing(boolean showing) {
if (showing) {
glass.addClassName(css.glassVisible());
contentContainer.addClassName(css.contentVisible());
} else {
glass.removeClassName(css.glassVisible());
contentContainer.removeClassName(css.contentVisible());
}
}
private void handleEvents() {
getElement().addEventListener(Event.KEYDOWN, new EventListener() {
@Override
public void handleEvent(Event evt) {
JsKeyboardEvent keyEvt = (JsKeyboardEvent) evt;
int keyCode = keyEvt.getKeyCode();
if (KeyCode.ESC == keyCode) {
if (getDelegate() != null) {
getDelegate().onEscapeKey();
}
}
}
}, true);
}
}
private boolean hideOnEscapeEnabled = false;
private boolean isShowing;
CenterPanel(View view) {
super(view);
handleViewEvents();
}
/**
* Hides the {@link CenterPanel} popup. The popup will animate out of view.
*/
public void hide() {
if (!isShowing) {
return;
}
isShowing = false;
// Animate the popup out of existance.
getView().setShowing(false);
// Remove the popup when the animation completes.
new Timer() {
@Override
public void run() {
// The popup may have been shown before this timer executes.
if (!isShowing) {
Elements.asJsElement(getView().popup).removeFromParent();
}
}
}.schedule(getView().getAnimationDuration());
}
/**
* Checks if the {@link CenterPanel} is showing or animating into view.
*
* @return true if showing, false if hidden
*/
public boolean isShowing() {
return isShowing;
}
/**
* Sets whether or not the popup should hide when escape is pressed. The
* default behavior is to ignore the escape key.
*
* @param isEnabled true to close on escape, false not to
*/
// TODO: This only works if the popup has focus. We need to capture events.
// TODO: Consider making escaping the default.
public void setHideOnEscapeEnabled(boolean isEnabled) {
this.hideOnEscapeEnabled = isEnabled;
}
/**
* See {@link #show(InputElement)}.
*/
public void show() {
show(null);
}
/**
* Displays the {@link CenterPanel} popup. The popup will animate into view.
*
* @param selectAndFocusElement an {@link InputElement} to select and focus on when the panel is
* shown. If null, no element will be given focus
*/
public void show(@Nullable final InputElement selectAndFocusElement) {
if (isShowing) {
return;
}
isShowing = true;
// Attach the popup to the body.
final JsElement popup = getView().popup.cast();
if (popup.getParentElement() == null) {
// Hide the popup so it can enter its initial state without flickering.
popup.getStyle().setVisibility("hidden");
Elements.getBody().appendChild(popup);
}
// Start the animation after the element is attached.
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
// The popup may have been hidden before this timer executes.
if (isShowing) {
popup.getStyle().removeProperty("visibility");
getView().setShowing(true);
if (selectAndFocusElement != null) {
selectAndFocusElement.select();
selectAndFocusElement.focus();
}
}
}
});
}
private void handleViewEvents() {
getView().setDelegate(new ViewEvents() {
@Override
public void onEscapeKey() {
if (hideOnEscapeEnabled) {
hide();
}
}
});
}
}