/* * 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); 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); 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); } } }