/**
* Copyright 2010 Google Inc.
*
* 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.livingstories.client.ui;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.event.logical.shared.HasOpenHandlers;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiConstructor;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasAnimation;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.livingstories.client.lsp.event.BlockToggledEvent;
import com.google.livingstories.client.lsp.event.EventBus;
import com.google.livingstories.client.util.Constants;
/**
* A widget that consists of a header and a content panel. The content panel
* contains one or two widgets, one representing the 'closed' state and an optional
* one representing the 'open' state. If the open state is available,
* this widget toggles between the states each time the header is clicked.
* The panel will also animate, expanding and collapsing to accomodate these elements.
*
* This class uses the same style rules as the standard gwt DisclosurePanel.
* .gwt-DisclosurePanel { the panel's primary style }
* .gwt-DisclosurePanel-open { dependent style set when panel is open }
* .gwt-DisclosurePanel-closed { dependent style set when panel is closed }
* .gwt-DisclosurePanel .header { style for the header }
* .gwt-DisclosurePanel .content { style for the content }
*
* Quite a bit of this code comes directly from the standard DisclosurePanel itself;
* however, extending that class didn't seem practical for our use case, so we rewrite
* some functionality here.
*/
public final class ToggleDisclosurePanel extends Composite implements HasAnimation,
HasOpenHandlers<ToggleDisclosurePanel>, HasCloseHandlers<ToggleDisclosurePanel>,
HasClickHandlers {
/**
* Used to wrap widgets in the header to provide click support. Effectively
* wraps the widget in an <code>anchor</code> to get automatic keyboard
* access.
*/
private final class ClickableHeader extends SimplePanel implements HasClickHandlers {
private ClickableHeader() {
// Anchor is used to allow keyboard access.
super(DOM.createAnchor());
Element elem = getElement();
DOM.setElementProperty(elem, "href", "javascript:void(0);");
// Avoids layout problems from having blocks in inlines.
DOM.setStyleAttribute(elem, "display", "block");
sinkEvents(Event.ONCLICK);
setStyleName(STYLENAME_HEADER);
}
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addHandler(handler, ClickEvent.getType());
}
@Override
public void onBrowserEvent(Event event) {
// no need to call super.
switch (DOM.eventGetType(event)) {
case Event.ONCLICK:
// Prevent link default action.
DOM.eventPreventDefault(event);
ClickEvent.fireNativeEvent(event, this);
boolean opened = !isOpen;
// Need to set this open first here instead of relying on the event
// in case the user didn't specify a content item id for this widget.
setOpen(opened, true);
if (contentItemId != null) {
EventBus.INSTANCE.fireEvent(new BlockToggledEvent(opened, contentItemId));
}
}
}
}
// Stylename constants.
private static final String STYLENAME_DEFAULT = "gwt-DisclosurePanel";
private static final String STYLENAME_SUFFIX_OPEN = "open";
private static final String STYLENAME_SUFFIX_CLOSED = "closed";
private static final String STYLENAME_HEADER = "gwt-header";
private static final String STYLENAME_CONTENT = "gwt-content";
private final VerticalPanel mainPanel = new VerticalPanel();
private final ClickableHeader header = new ClickableHeader();
private final SimplePanel contentWrapper = new SimplePanel();
private boolean isAnimationEnabled = true;
private boolean isOpen = false;
private Widget closedWidget;
private Widget openedWidget;
private Command onAnimationCompletion;
private HandlerRegistration toggleEventHandler;
private Long contentItemId;
@UiConstructor
public ToggleDisclosurePanel(boolean headerOnTop) {
init(headerOnTop);
}
public ToggleDisclosurePanel(Widget panelHeader, boolean headerOnTop) {
this(headerOnTop);
setHeader(panelHeader);
}
@Override
protected void onUnload() {
super.onUnload();
if (toggleEventHandler != null) {
toggleEventHandler.removeHandler();
toggleEventHandler = null;
}
}
public HandlerRegistration addCloseHandler(
CloseHandler<ToggleDisclosurePanel> handler) {
return addHandler(handler, CloseEvent.getType());
}
public HandlerRegistration addOpenHandler(
OpenHandler<ToggleDisclosurePanel> handler) {
return addHandler(handler, OpenEvent.getType());
}
public HandlerRegistration addClickHandler(ClickHandler handler) {
return header.addClickHandler(handler);
}
public void setAnimationCompletionCommand(Command onAnimationCompletion) {
this.onAnimationCompletion = onAnimationCompletion;
}
public void clear() {
setContent(null, null);
}
public Widget getContent() {
return contentWrapper.getWidget();
}
public Widget getHeader() {
return header.getWidget();
}
public HasText getHeaderTextAccessor() {
Widget widget = header.getWidget();
return (widget instanceof HasText) ? (HasText) widget : null;
}
public boolean isAnimationEnabled() {
return isAnimationEnabled;
}
public boolean isOpen() {
return isOpen;
}
public void setAnimationEnabled(boolean enable) {
isAnimationEnabled = enable;
}
/**
* Sets the content widget which can be opened and closed by this panel. If
* there is a preexisting content widget, it will be detached.
*
* @param closedContent the widget to show in the closed state
* @param openedContent the widget to show in the opened state
*/
public void setContent(Widget closedContent, Widget openedContent) {
final Widget currentContent = getContent();
// Remove existing content widget.
if (currentContent != null) {
contentWrapper.setWidget(null);
closedWidget = null;
openedWidget = null;
}
// Add new content widget if != null.
if (closedContent != null) {
closedWidget = closedContent;
openedWidget = openedContent;
if (openedContent == null) {
isOpen = false;
} else {
openedContent.addStyleName(STYLENAME_CONTENT);
}
closedContent.addStyleName(STYLENAME_CONTENT);
contentWrapper.setWidget(isOpen ? openedContent : closedContent);
}
}
/**
* Make this panel listen for events being fired for the specified contentItem id.
*/
public void handleContentItemEvents(Long currentContentItemId) {
this.contentItemId = currentContentItemId;
toggleEventHandler = EventBus.INSTANCE.addHandler(BlockToggledEvent.TYPE,
new BlockToggledEvent.Handler() {
@Override
public void onToggle(BlockToggledEvent e) {
if (contentItemId.equals(e.getContentItemId())) {
setOpen(e.isOpened(), e.shouldAnimate());
e.finish();
}
}
});
}
/**
* Sets the widget used as the header for the panel.
*
* @param headerWidget the widget to be used as the header
*/
public void setHeader(Widget headerWidget) {
header.setWidget(headerWidget);
}
/**
* Changes the visible state of this <code>DisclosurePanel</code>.
*
* @param isOpen <code>true</code> to open the panel, <code>false</code> to
* close
*/
public void setOpen(boolean isOpen, boolean animate) {
if (this.isOpen != isOpen && openedWidget != null) {
this.isOpen = isOpen;
setContentDisplay(animate);
fireEvent();
}
}
public void toggle() {
setOpen(!isOpen, true);
}
/**
* <b>Affected Elements:</b>
* <ul>
* <li>-header = the clickable header.</li>
* </ul>
*
* @see UIObject#onEnsureDebugId(String)
*/
@Override
protected void onEnsureDebugId(String baseID) {
super.onEnsureDebugId(baseID);
header.ensureDebugId(baseID + "-header");
}
private void fireEvent() {
if (isOpen) {
OpenEvent.fire(this, this);
} else {
CloseEvent.fire(this, this);
}
}
private void init(boolean headerOnTop) {
initWidget(mainPanel);
if (headerOnTop) {
mainPanel.add(header);
mainPanel.add(contentWrapper);
} else {
mainPanel.add(contentWrapper);
mainPanel.add(header);
}
DOM.setStyleAttribute(contentWrapper.getElement(), "padding", "0px");
DOM.setStyleAttribute(contentWrapper.getElement(), "overflow", "hidden");
setStyleName(STYLENAME_DEFAULT);
addStyleDependentName(STYLENAME_SUFFIX_CLOSED);
}
private void setContentDisplay(boolean animate) {
if (isOpen) {
removeStyleDependentName(STYLENAME_SUFFIX_CLOSED);
addStyleDependentName(STYLENAME_SUFFIX_OPEN);
} else {
removeStyleDependentName(STYLENAME_SUFFIX_OPEN);
addStyleDependentName(STYLENAME_SUFFIX_CLOSED);
}
if (getContent() != null) {
int oldHeight = getContent().getElement().getClientHeight();
DOM.setStyleAttribute(contentWrapper.getElement(), "height", oldHeight + "px");
contentWrapper.setWidget(isOpen ? openedWidget : closedWidget);
int newHeight = getContent().getElement().getClientHeight();
if (animate) {
ExpandEffect animation = new ExpandEffect(contentWrapper, newHeight);
animation.run(Constants.ANIMATION_DURATION);
} else {
DOM.setStyleAttribute(contentWrapper.getElement(), "height", "auto");
runCompletionCode();
}
}
}
private class ExpandEffect extends StyleEffect {
public ExpandEffect(Widget widget, int newValue) {
super(widget, "height", newValue);
}
@Override
public void onComplete() {
DOM.setStyleAttribute(widget.getElement(), "height", "auto");
runCompletionCode();
}
}
private void runCompletionCode() {
// for IE friendliness, we always run the completion code via the DeferredCommand
// mechanism.
DeferredCommand.addCommand(new Command() {
@Override
public void execute() {
if (onAnimationCompletion != null) {
onAnimationCompletion.execute();
}
}
});
}
}