blob: 49f12786871e8320c1a98fca83747d3e6069c54d [file] [log] [blame]
/*
* Copyright 2011 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.gwt.user.client.ui;
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.Document;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.logical.shared.HasResizeHandlers;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.layout.client.Layout;
import com.google.gwt.layout.client.Layout.Layer;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.ResizeLayoutPanel.Impl.Delegate;
/**
* A simple panel that {@link ProvidesResize} to its one child, but does not
* {@link RequiresResize}. Use this to embed layout panels in any location
* within your application.
*/
public class ResizeLayoutPanel extends SimplePanel implements ProvidesResize,
HasResizeHandlers {
/**
* Implementation of resize event.
*/
abstract static class Impl {
/**
* Delegate event handler.
*/
abstract static interface Delegate {
/**
* Called when the element is resized.
*/
void onResize();
}
boolean isAttached;
Element parent;
private Delegate delegate;
/**
* Initialize the implementation.
*
* @param elem the element to listen for resize
* @param delegate the {@link Delegate} to inform when resize occurs
*/
public void init(Element elem, Delegate delegate) {
this.parent = elem;
this.delegate = delegate;
}
/**
* Called on attach.
*/
public void onAttach() {
isAttached = true;
}
/**
* Called on detach.
*/
public void onDetach() {
isAttached = false;
}
/**
* Handle a resize event.
*/
protected void handleResize() {
if (isAttached && delegate != null) {
delegate.onResize();
}
}
}
/**
* Implementation of resize event.
*/
static class ImplStandard extends Impl implements EventListener {
/**
* Chrome does not fire an onresize event if the dimensions are too small to
* render a scrollbar.
*/
private static final String MIN_SIZE = "20px";
private Element collapsible;
private Element collapsibleInner;
private Element expandable;
private Element expandableInner;
private int lastOffsetHeight = -1;
private int lastOffsetWidth = -1;
private boolean resettingScrollables;
@Override
public void init(Element elem, Delegate delegate) {
super.init(elem, delegate);
/*
* Set the minimum dimensions to ensure that scrollbars are rendered and
* fire onscroll events.
*/
elem.getStyle().setProperty("minWidth", MIN_SIZE);
elem.getStyle().setProperty("minHeight", MIN_SIZE);
/*
* Detect expansion. In order to detect an increase in the size of the
* widget, we create an absolutely positioned, scrollable div with
* height=width=100%. We then add an inner div that has fixed height and
* width equal to 100% (converted to pixels) and set scrollLeft/scrollTop
* to their maximum. When the outer div expands, scrollLeft/scrollTop
* automatically becomes a smaller number and trigger an onscroll event.
*/
expandable = Document.get().createDivElement().cast();
expandable.getStyle().setVisibility(Visibility.HIDDEN);
expandable.getStyle().setPosition(Position.ABSOLUTE);
expandable.getStyle().setHeight(100.0, Unit.PCT);
expandable.getStyle().setWidth(100.0, Unit.PCT);
expandable.getStyle().setOverflow(Overflow.SCROLL);
expandable.getStyle().setZIndex(-1);
elem.appendChild(expandable);
expandableInner = Document.get().createDivElement().cast();
expandable.appendChild(expandableInner);
DOM.sinkEvents(expandable, Event.ONSCROLL);
/*
* Detect collapse. In order to detect a decrease in the size of the
* widget, we create an absolutely positioned, scrollable div with
* height=width=100%. We then add an inner div that has height=width=200%
* and max out the scrollTop/scrollLeft. When the height or width
* decreases, the inner div loses 2px for every 1px that the scrollable
* div loses, so the scrollTop/scrollLeft decrease and we get an onscroll
* event.
*/
collapsible = Document.get().createDivElement().cast();
collapsible.getStyle().setVisibility(Visibility.HIDDEN);
collapsible.getStyle().setPosition(Position.ABSOLUTE);
collapsible.getStyle().setHeight(100.0, Unit.PCT);
collapsible.getStyle().setWidth(100.0, Unit.PCT);
collapsible.getStyle().setOverflow(Overflow.SCROLL);
collapsible.getStyle().setZIndex(-1);
elem.appendChild(collapsible);
collapsibleInner = Document.get().createDivElement().cast();
collapsibleInner.getStyle().setWidth(200, Unit.PCT);
collapsibleInner.getStyle().setHeight(200, Unit.PCT);
collapsible.appendChild(collapsibleInner);
DOM.sinkEvents(collapsible, Event.ONSCROLL);
}
@Override
public void onAttach() {
super.onAttach();
DOM.setEventListener(expandable, this);
DOM.setEventListener(collapsible, this);
/*
* Update the scrollables in a deferred command so the browser calculates
* the offsetHeight/Width correctly.
*/
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute() {
resetScrollables();
}
});
}
public void onBrowserEvent(Event event) {
if (!resettingScrollables && Event.ONSCROLL == event.getTypeInt()) {
EventTarget eventTarget = event.getEventTarget();
if (!Element.is(eventTarget)) {
return;
}
Element target = eventTarget.cast();
if (target == collapsible || target == expandable) {
handleResize();
}
}
}
@Override
public void onDetach() {
super.onDetach();
DOM.setEventListener(expandable, null);
DOM.setEventListener(collapsible, null);
lastOffsetHeight = -1;
lastOffsetWidth = -1;
}
@Override
protected void handleResize() {
if (resetScrollables()) {
super.handleResize();
}
}
/**
* Reset the positions of the scrollable elements.
*
* @return true if the size changed, false if not
*/
private boolean resetScrollables() {
/*
* Older versions of safari trigger a synchronous scroll event when we
* update scrollTop/scrollLeft, so we set a boolean to ignore that event.
*/
if (resettingScrollables) {
return false;
}
resettingScrollables = true;
/*
* Reset expandable element. Scrollbars are not rendered if the div is too
* small, so we need to set the dimensions of the inner div to a value
* greater than the offsetWidth/Height.
*/
int offsetHeight = parent.getOffsetHeight();
int offsetWidth = parent.getOffsetWidth();
int height = offsetHeight + 100;
int width = offsetWidth + 100;
expandableInner.getStyle().setHeight(height, Unit.PX);
expandableInner.getStyle().setWidth(width, Unit.PX);
expandable.setScrollTop(height);
expandable.setScrollLeft(width);
// Reset collapsible element.
collapsible.setScrollTop(collapsible.getScrollHeight() + 100);
collapsible.setScrollLeft(collapsible.getScrollWidth() + 100);
if (lastOffsetHeight != offsetHeight || lastOffsetWidth != offsetWidth) {
lastOffsetHeight = offsetHeight;
lastOffsetWidth = offsetWidth;
resettingScrollables = false;
return true;
}
resettingScrollables = false;
return false;
}
}
/**
* Implementation of resize event used by IE.
*/
static class ImplTrident extends Impl {
@Override
public void init(Element elem, Delegate delegate) {
super.init(elem, delegate);
initResizeEventListener(elem);
}
@Override
public void onAttach() {
super.onAttach();
setResizeEventListener(parent, this);
}
@Override
public void onDetach() {
super.onDetach();
setResizeEventListener(parent, null);
}
/**
* Initalize the onresize listener. This method doesn't create a memory leak
* because we don't set a back reference to the Impl class until we attach
* to the DOM.
*/
private native void initResizeEventListener(Element elem) /*-{
var theElem = elem;
var handleResize = $entry(function() {
if (theElem.__resizeImpl) {
theElem.__resizeImpl.@com.google.gwt.user.client.ui.ResizeLayoutPanel.Impl::handleResize()();
}
});
elem.attachEvent('onresize', handleResize);
}-*/;
/**
* Set the event listener that handles resize events.
*/
private native void setResizeEventListener(Element elem, Impl listener) /*-{
elem.__resizeImpl = listener;
}-*/;
}
/**
* Implementation of resize event used by IE6.
*/
static class ImplIE6 extends ImplTrident {
@Override
public void onAttach() {
super.onAttach();
/*
* IE6 doesn't render this panel unless you kick it after its been
* attached.
*/
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute() {
if (isAttached) {
parent.getStyle().setProperty("zoom", "1");
}
}
});
}
}
private final Impl impl = GWT.create(Impl.class);
private Layer layer;
private final Layout layout;
private final ScheduledCommand resizeCmd = new ScheduledCommand() {
public void execute() {
resizeCmdScheduled = false;
handleResize();
}
};
private boolean resizeCmdScheduled = false;
public ResizeLayoutPanel() {
layout = new Layout(getElement());
impl.init(getElement(), new Delegate() {
public void onResize() {
scheduleResize();
}
});
}
public HandlerRegistration addResizeHandler(ResizeHandler handler) {
return addHandler(handler, ResizeEvent.getType());
}
@Override
public boolean remove(Widget w) {
// Validate.
if (widget != w) {
return false;
}
// Orphan.
try {
orphan(w);
} finally {
// Physical detach.
layout.removeChild(layer);
layer = null;
// Logical detach.
widget = null;
}
return true;
}
@Override
public void setWidget(Widget w) {
// Validate
if (w == widget) {
return;
}
// Detach new child.
if (w != null) {
w.removeFromParent();
}
// Remove old child.
if (widget != null) {
remove(widget);
}
// Logical attach.
widget = w;
if (w != null) {
// Physical attach.
layer = layout.attachChild(widget.getElement(), widget);
layer.setTopHeight(0.0, Unit.PX, 100.0, Unit.PCT);
layer.setLeftWidth(0.0, Unit.PX, 100.0, Unit.PCT);
adopt(w);
// Update the layout.
layout.layout();
scheduleResize();
}
}
@Override
protected void onAttach() {
super.onAttach();
impl.onAttach();
layout.onAttach();
scheduleResize();
}
@Override
protected void onDetach() {
super.onDetach();
impl.onDetach();
layout.onDetach();
}
private void handleResize() {
if (!isAttached()) {
return;
}
// Provide resize to child.
if (widget instanceof RequiresResize) {
((RequiresResize) widget).onResize();
}
// Fire resize event.
ResizeEvent.fire(this, getOffsetWidth(), getOffsetHeight());
}
/**
* Schedule a resize handler. We schedule the event so the DOM has time to
* update the offset sizes, and to avoid duplicate resize events from both a
* height and width resize.
*/
private void scheduleResize() {
if (isAttached() && !resizeCmdScheduled) {
resizeCmdScheduled = true;
Scheduler.get().scheduleDeferred(resizeCmd);
}
}
}