001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.gwt.client.ui; 029 030import org.opencms.gwt.client.I_CmsDescendantResizeHandler; 031import org.opencms.gwt.client.js.ResizeObserver; 032import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle; 033import org.opencms.gwt.client.util.CmsDomUtil; 034import org.opencms.gwt.client.util.CmsDomUtil.Style; 035import org.opencms.gwt.client.util.CmsFadeAnimation; 036 037import java.util.ArrayList; 038import java.util.Iterator; 039import java.util.List; 040 041import com.google.gwt.core.client.Scheduler; 042import com.google.gwt.core.client.Scheduler.ScheduledCommand; 043import com.google.gwt.dom.client.Element; 044import com.google.gwt.dom.client.Style.Display; 045import com.google.gwt.dom.client.Style.Overflow; 046import com.google.gwt.dom.client.Style.Unit; 047import com.google.gwt.event.dom.client.MouseOutEvent; 048import com.google.gwt.event.dom.client.MouseOutHandler; 049import com.google.gwt.event.dom.client.MouseOverEvent; 050import com.google.gwt.event.dom.client.MouseOverHandler; 051import com.google.gwt.event.logical.shared.ValueChangeEvent; 052import com.google.gwt.event.logical.shared.ValueChangeHandler; 053import com.google.gwt.event.shared.HandlerRegistration; 054import com.google.gwt.user.client.Command; 055import com.google.gwt.user.client.DOM; 056import com.google.gwt.user.client.Event; 057import com.google.gwt.user.client.Timer; 058import com.google.gwt.user.client.ui.AbstractNativeScrollbar; 059import com.google.gwt.user.client.ui.VerticalScrollbar; 060import com.google.gwt.user.client.ui.Widget; 061 062import jsinterop.base.Js; 063 064/** 065 * Scroll panel implementation with custom scroll bars. Works in all browsers but IE7.<p> 066 */ 067public class CmsScrollPanelImpl extends CmsScrollPanel { 068 069 /** 070 * Handler to show and hide the scroll bar on hover.<p> 071 */ 072 private class HoverHandler implements MouseOutHandler, MouseOverHandler { 073 074 /** The owner element. */ 075 Element m_owner; 076 077 /** The element to fade in and out. */ 078 private Element m_fadeElement; 079 080 /** The currently running hide animation. */ 081 private CmsFadeAnimation m_hideAnimation; 082 083 /** The timer to hide the scroll bar with a delay. */ 084 private Timer m_removeTimer; 085 086 /** 087 * Constructor.<p> 088 * 089 * @param owner the owner element 090 * @param fadeElement the element to fade in and out on hover 091 */ 092 HoverHandler(Element owner, Element fadeElement) { 093 094 m_owner = owner; 095 m_fadeElement = fadeElement; 096 } 097 098 /** 099 * @see com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google.gwt.event.dom.client.MouseOutEvent) 100 */ 101 public void onMouseOut(MouseOutEvent event) { 102 103 m_removeTimer = new Timer() { 104 105 /** 106 * @see com.google.gwt.user.client.Timer#run() 107 */ 108 @Override 109 public void run() { 110 111 clearShowing(); 112 } 113 }; 114 m_removeTimer.schedule(1000); 115 } 116 117 /** 118 * @see com.google.gwt.event.dom.client.MouseOverHandler#onMouseOver(com.google.gwt.event.dom.client.MouseOverEvent) 119 */ 120 public void onMouseOver(MouseOverEvent event) { 121 122 if ((m_hideAnimation != null) 123 || !CmsDomUtil.hasClass(I_CmsLayoutBundle.INSTANCE.scrollBarCss().showBars(), m_owner)) { 124 if (m_hideAnimation != null) { 125 m_hideAnimation.cancel(); 126 m_hideAnimation = null; 127 } else { 128 CmsFadeAnimation.fadeIn(m_fadeElement, null, 100); 129 } 130 m_owner.addClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().showBars()); 131 } 132 if (m_removeTimer != null) { 133 m_removeTimer.cancel(); 134 m_removeTimer = null; 135 } 136 } 137 138 /** 139 * Hides the scroll bar.<p> 140 */ 141 void clearShowing() { 142 143 m_hideAnimation = CmsFadeAnimation.fadeOut(m_fadeElement, new Command() { 144 145 public void execute() { 146 147 m_owner.removeClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().showBars()); 148 149 } 150 }, 200); 151 152 m_removeTimer = null; 153 } 154 } 155 156 /** Hidden element to measure the appropriate size of the container element. */ 157 private Element m_hiddenSize; 158 159 /** The measured width of the native scroll bars. */ 160 private int m_nativeScrollbarWidth; 161 162 /** The resize observer. */ 163 private ResizeObserver m_resizeObserver; 164 165 /** The vertical scroll bar. */ 166 private VerticalScrollbar m_scrollbar; 167 168 /** The scroll layer. */ 169 private Element m_scrollLayer; 170 171 /** The scroll bar change handler registration. */ 172 private HandlerRegistration m_verticalScrollbarHandlerRegistration; 173 174 /** The scroll bar width. */ 175 private int m_verticalScrollbarWidth; 176 177 /** 178 * Constructor.<p> 179 */ 180 public CmsScrollPanelImpl() { 181 182 super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv()); 183 setStyleName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollPanel()); 184 Element scrollable = getScrollableElement(); 185 scrollable.getStyle().clearPosition(); 186 scrollable.getStyle().setOverflowX(Overflow.HIDDEN); 187 scrollable.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollable()); 188 getElement().appendChild(scrollable); 189 Element container = getContainerElement(); 190 container.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollContainer()); 191 scrollable.appendChild(container); 192 m_scrollLayer = DOM.createDiv(); 193 getElement().appendChild(m_scrollLayer); 194 m_scrollLayer.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollbarLayer()); 195 CmsScrollBar scrollbar = new CmsScrollBar(scrollable, container); 196 setVerticalScrollbar(scrollbar, 8); 197 m_hiddenSize = DOM.createDiv(); 198 m_hiddenSize.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().hiddenSize()); 199 200 /* 201 * Listen for scroll events from the root element and the scrollable element 202 * so we can align the scrollbars with the content. Scroll events usually 203 * come from the scrollable element, but they can also come from the root 204 * element if the user clicks and drags the content, which reveals the 205 * hidden scrollbars. 206 */ 207 Event.sinkEvents(getElement(), Event.ONSCROLL); 208 Event.sinkEvents(scrollable, Event.ONSCROLL); 209 initHoverHandler(); 210 } 211 212 /** 213 * @see com.google.gwt.user.client.ui.SimplePanel#iterator() 214 */ 215 @Override 216 public Iterator<Widget> iterator() { 217 218 // Return a simple iterator that enumerates the 0 or 1 elements in this 219 // panel. 220 List<Widget> widgets = new ArrayList<Widget>(); 221 if (getWidget() != null) { 222 widgets.add(getWidget()); 223 } 224 if (getVerticalScrollBar() != null) { 225 widgets.add(getVerticalScrollBar().asWidget()); 226 } 227 final Iterator<Widget> internalIterator = widgets.iterator(); 228 return new Iterator<Widget>() { 229 230 public boolean hasNext() { 231 232 return internalIterator.hasNext(); 233 } 234 235 public Widget next() { 236 237 return internalIterator.next(); 238 } 239 240 @Override 241 public void remove() { 242 243 throw new UnsupportedOperationException(); 244 } 245 }; 246 } 247 248 /** 249 * @see com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user.client.Event) 250 */ 251 @Override 252 public void onBrowserEvent(Event event) { 253 254 // Align the scrollbars with the content. 255 if (Event.ONSCROLL == event.getTypeInt()) { 256 maybeUpdateScrollbarPositions(); 257 } 258 super.onBrowserEvent(event); 259 } 260 261 /** 262 * @see org.opencms.gwt.client.ui.CmsScrollPanel#onResizeDescendant() 263 */ 264 @Override 265 public void onResizeDescendant() { 266 267 int maxHeight = CmsDomUtil.getCurrentStyleInt(getElement(), Style.maxHeight); 268 if (maxHeight > 0) { 269 getScrollableElement().getStyle().setPropertyPx("maxHeight", maxHeight); 270 } 271 // appending div to measure panel width, doing it every time anew to avoid rendering bugs in Chrome 272 getElement().appendChild(m_hiddenSize); 273 int width = m_hiddenSize.getClientWidth(); 274 m_hiddenSize.removeFromParent(); 275 if (width > 0) { 276 getContainerElement().getStyle().setWidth(width, Unit.PX); 277 maybeUpdateScrollbars(); 278 } 279 } 280 281 /** 282 * @see org.opencms.gwt.client.ui.CmsScrollPanel#setResizable(boolean) 283 */ 284 @Override 285 public void setResizable(boolean resize) { 286 287 super.setResizable(resize); 288 if (resize) { 289 m_scrollbar.asWidget().getElement().getStyle().setMarginBottom(7, Unit.PX); 290 } else { 291 m_scrollbar.asWidget().getElement().getStyle().setMarginBottom(0, Unit.PX); 292 } 293 } 294 295 /** 296 * Returns the vertical scroll bar.<p> 297 * 298 * @return the vertical scroll bar 299 */ 300 protected VerticalScrollbar getVerticalScrollBar() { 301 302 return m_scrollbar; 303 } 304 305 /** 306 * @see com.google.gwt.user.client.ui.ScrollPanel#onAttach() 307 */ 308 @Override 309 protected void onAttach() { 310 311 super.onAttach(); 312 hideNativeScrollbars(); 313 onResizeDescendant(); 314 } 315 316 /** 317 * @see com.google.gwt.user.client.ui.Widget#onLoad() 318 */ 319 @Override 320 protected void onLoad() { 321 322 super.onLoad(); 323 hideNativeScrollbars(); 324 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 325 326 public void execute() { 327 328 onResizeDescendant(); 329 } 330 }); 331 initResizeObserver(); 332 } 333 334 /** 335 * @see com.google.gwt.user.client.ui.Widget#onUnload() 336 */ 337 @Override 338 protected void onUnload() { 339 340 super.onUnload(); 341 disconnectResizeObserver(); 342 } 343 344 /** 345 * Disconnects and removes the resize observer. 346 */ 347 private void disconnectResizeObserver() { 348 349 if (m_resizeObserver != null) { 350 m_resizeObserver.disconnect(); 351 m_resizeObserver = null; 352 } 353 } 354 355 /** 356 * Hide the native scrollbars. We call this after attaching to ensure that we 357 * inherit the direction (rtl or ltr). 358 */ 359 private void hideNativeScrollbars() { 360 361 m_nativeScrollbarWidth = AbstractNativeScrollbar.getNativeScrollbarWidth(); 362 getScrollableElement().getStyle().setMarginRight(-(m_nativeScrollbarWidth + 10), Unit.PX); 363 } 364 365 /** 366 * Initializes the hover handler to hide and show the scroll bar on hover.<p> 367 */ 368 private void initHoverHandler() { 369 370 HoverHandler handler = new HoverHandler(getElement(), m_scrollbar.asWidget().getElement()); 371 addDomHandler(handler, MouseOverEvent.getType()); 372 addDomHandler(handler, MouseOutEvent.getType()); 373 } 374 375 /** 376 * Initializes the resize observer. 377 * 378 * <p>This throws away any old resize observers and creates a new one 379 * to observe the element of the scroll panel itself, and the element with the scrollable content. 380 */ 381 private void initResizeObserver() { 382 383 disconnectResizeObserver(); 384 m_resizeObserver = new ResizeObserver(entries -> { 385 maybeUpdateScrollbars(); 386 }); 387 m_resizeObserver.observe(Js.cast(getElement())); 388 m_resizeObserver.observe(Js.cast(getContainerElement())); 389 } 390 391 /** 392 * Synchronize the scroll positions of the scrollbars with the actual scroll 393 * position of the content. 394 */ 395 private void maybeUpdateScrollbarPositions() { 396 397 if (!isAttached()) { 398 return; 399 } 400 401 if (m_scrollbar != null) { 402 int vPos = getVerticalScrollPosition(); 403 if (m_scrollbar.getVerticalScrollPosition() != vPos) { 404 m_scrollbar.setVerticalScrollPosition(vPos); 405 } 406 } 407 } 408 409 /** 410 * Update the position of the scrollbars.<p> 411 * If only the vertical scrollbar is present, it takes up the entire height of 412 * the right side. If only the horizontal scrollbar is present, it takes up 413 * the entire width of the bottom. If both scrollbars are present, the 414 * vertical scrollbar extends from the top to just above the horizontal 415 * scrollbar, and the horizontal scrollbar extends from the left to just right 416 * of the vertical scrollbar, leaving a small square in the bottom right 417 * corner.<p> 418 */ 419 private void maybeUpdateScrollbars() { 420 421 if (!isAttached()) { 422 return; 423 } 424 425 /* 426 * Measure the height and width of the content directly. Note that measuring 427 * the height and width of the container element (which should be the same) 428 * doesn't work correctly in IE. 429 */ 430 Widget w = getWidget(); 431 int contentHeight = (w == null) ? 0 : w.getOffsetHeight(); 432 433 // Determine which scrollbars to show. 434 int realScrollbarHeight = 0; 435 int realScrollbarWidth = 0; 436 if ((m_scrollbar != null) && (getElement().getClientHeight() < contentHeight)) { 437 // Vertical scrollbar is defined and required. 438 realScrollbarWidth = m_verticalScrollbarWidth; 439 } 440 441 if (realScrollbarWidth > 0) { 442 m_scrollLayer.getStyle().clearDisplay(); 443 444 m_scrollbar.setScrollHeight(Math.max(0, contentHeight - realScrollbarHeight)); 445 } else if (m_scrollLayer != null) { 446 m_scrollLayer.getStyle().setDisplay(Display.NONE); 447 } 448 if (m_scrollbar instanceof I_CmsDescendantResizeHandler) { 449 ((I_CmsDescendantResizeHandler)m_scrollbar).onResizeDescendant(); 450 } 451 maybeUpdateScrollbarPositions(); 452 } 453 454 /** 455 * Set the scrollbar used for vertical scrolling. 456 * 457 * @param scrollbar the scrollbar, or null to clear it 458 * @param width the width of the scrollbar in pixels 459 */ 460 private void setVerticalScrollbar(final CmsScrollBar scrollbar, int width) { 461 462 // Validate. 463 if ((scrollbar == m_scrollbar) || (scrollbar == null)) { 464 return; 465 } 466 // Detach new child. 467 468 scrollbar.asWidget().removeFromParent(); 469 // Remove old child. 470 if (m_scrollbar != null) { 471 if (m_verticalScrollbarHandlerRegistration != null) { 472 m_verticalScrollbarHandlerRegistration.removeHandler(); 473 m_verticalScrollbarHandlerRegistration = null; 474 } 475 remove(m_scrollbar); 476 } 477 m_scrollLayer.appendChild(scrollbar.asWidget().getElement()); 478 adopt(scrollbar.asWidget()); 479 480 // Logical attach. 481 m_scrollbar = scrollbar; 482 m_verticalScrollbarWidth = width; 483 484 // Initialize the new scrollbar. 485 m_verticalScrollbarHandlerRegistration = scrollbar.addValueChangeHandler(new ValueChangeHandler<Integer>() { 486 487 public void onValueChange(ValueChangeEvent<Integer> event) { 488 489 int vPos = scrollbar.getVerticalScrollPosition(); 490 int v = getVerticalScrollPosition(); 491 if (v != vPos) { 492 setVerticalScrollPosition(vPos); 493 } 494 495 } 496 }); 497 maybeUpdateScrollbars(); 498 } 499}