| /* |
| * 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. |
| * |
| * @param panel the panel |
| */ |
| public void onDetach() { |
| isAttached = false; |
| } |
| |
| /** |
| * Handle a resize event. |
| * |
| * @param panel the panel |
| */ |
| 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); |
| } |
| } |
| } |