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}