]> source.dussan.org Git - vaadin-framework.git/commitdiff
Support OSX's hiding scrollbars (#12645)
authorHenrik Paul <henrik@vaadin.com>
Thu, 21 Nov 2013 14:52:32 +0000 (16:52 +0200)
committerHenrik Paul <henrik@vaadin.com>
Thu, 21 Nov 2013 14:54:53 +0000 (16:54 +0200)
Change-Id: If5df6a7651482a33558088398330fd73a4d43645

WebContent/VAADIN/themes/base/escalator/escalator.scss
client/src/com/vaadin/client/ui/grid/Escalator.java
client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java [new file with mode: 0644]

index 436f849456c1eb8bdbb17a403d8fe00925e4d7c6..8136f23df66497bd4244b55a6b62e112e8119303 100644 (file)
@@ -11,9 +11,23 @@ $border-color: #aaa;
 .#{$primaryStyleName}-scroller {
        position: absolute;
        overflow: auto;
-       height: inherit;
+       z-index: 20;
+}
+
+.#{$primaryStyleName}-scroller-horizontal {
        left: 0; /* Left position adjusted to align with frozen columns */
        right: 0;
+       bottom: 0;
+       overflow-y: hidden;
+       -ms-overflow-y: hidden;
+}
+
+.#{$primaryStyleName}-scroller-vertical {
+       right: 0;
+       top: 0;    /* this will be overridden by code, but it's a good default behavior */
+       bottom: 0; /* this will be overridden by code, but it's a good default behavior */
+       overflow-x: hidden;
+       -ms-overflow-x: hidden;
 }
 
 .#{$primaryStyleName}-tablewrapper {
index bd9d4ba245e63d40466084e901d235354a62e36e..769a109569e09d424ceffcf94c2a1e35735e57d1 100644 (file)
@@ -46,6 +46,8 @@ import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition;
 import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition;
 import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition;
 import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition;
+import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle;
+import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle;
 
 /*-
 
@@ -226,10 +228,6 @@ public class Escalator extends Widget {
      * that it _might_ perform better (rememeber to measure, implement,
      * re-measure)
      */
-    /*
-     * [[escalator]]: This code will require alterations that are relevant for
-     * the escalator functionality.
-     */
     /*
      * [[rowwidth]] [[colwidth]]: This code will require alterations that are
      * relevant for being able to support variable row heights or column widths.
@@ -391,7 +389,7 @@ public class Escalator extends Widget {
                     }
                 }
 
-                moveScrollFromEvent(escalator.scrollerElem, -deltaX, -deltaY,
+                moveScrollFromEvent(escalator, -deltaX, -deltaY,
                         event.getNativeEvent());
             }
 
@@ -406,21 +404,16 @@ public class Escalator extends Widget {
             }
         }
 
-        public static void moveScrollFromEvent(final Element scrollerElem,
+        public static void moveScrollFromEvent(final Escalator escalator,
                 final double deltaX, final double deltaY,
                 final NativeEvent event) {
-            /*
-             * TODO [[optimize]]: instead of calling getScollLeft/Top that
-             * potentially causes a reflow, update a new scroll absolute
-             * position.
-             */
-            if (!Double.isNaN(deltaX) && deltaX != 0) {
-                scrollerElem
-                        .setScrollLeft((int) (scrollerElem.getScrollLeft() + deltaX));
+
+            if (!Double.isNaN(deltaX)) {
+                escalator.horizontalScrollbar.setScrollPosByDelta((int) deltaX);
             }
-            if (!Double.isNaN(deltaY) && deltaY != 0) {
-                scrollerElem
-                        .setScrollTop((int) (scrollerElem.getScrollTop() + deltaY));
+
+            if (!Double.isNaN(deltaY)) {
+                escalator.verticalScrollbar.setScrollPosByDelta((int) deltaY);
             }
 
             /*
@@ -514,8 +507,8 @@ public class Escalator extends Widget {
 
         private void cancelBecauseOfEdgeOrCornerMaybe(final int lastLeft,
                 final int lastTop) {
-            if (lastLeft == scrollerElem.getScrollLeft()
-                    && lastTop == scrollerElem.getScrollTop()) {
+            if (lastLeft == horizontalScrollbar.getScrollPos()
+                    && lastTop == verticalScrollbar.getScrollPos()) {
                 cancel();
             }
         }
@@ -564,8 +557,7 @@ public class Escalator extends Widget {
                     deltaY = -0.5*e.wheelDelta;
                 }
 
-                var scroller = esc.@com.vaadin.client.ui.grid.Escalator::scrollerElem;
-                @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(scroller, deltaX, deltaY, e);
+                @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
             });
         }-*/;
 
@@ -574,41 +566,35 @@ public class Escalator extends Widget {
          * that the sizes of the scroll handles appear correct in the browser
          */
         public void recalculateScrollbarsForVirtualViewport() {
-            double scrollerHeight = height - header.height;
-
-            /*
-             * It looks better this way: if we only have a vertical scrollbar,
-             * keep it between the header and footer. But if we have a
-             * horizontal scrollbar, envelop the footer also.
-             */
-            if (!needsHorizontalScrollbars()) {
-                scrollerHeight -= footer.height;
-            }
-
-            scrollerElem.getStyle().setHeight(scrollerHeight, Unit.PX);
-
-            final double scrollerTop = header.height;
-            scrollerElem.getStyle().setTop(scrollerTop, Unit.PX);
-
-            /*
-             * TODO [[freezecol]]: cut scrollerElem from the left to take freeze
-             * columns into account. innerScroller probably needs some
-             * adjustments too.
-             */
-
             // TODO [[rowheight]]: adjust for variable row heights.
-            double innerScrollerHeight = ROW_HEIGHT_PX * body.getRowCount();
+            int innerScrollerHeight = ROW_HEIGHT_PX * body.getRowCount();
+
+            double vScrollBottom = footer.height;
             if (needsHorizontalScrollbars()) {
-                innerScrollerHeight += footer.height;
+                vScrollBottom += horizontalScrollbar.getScrollbarThickness();
             }
-            innerScrollerElem.getStyle()
-                    .setHeight(innerScrollerHeight, Unit.PX);
+
+            verticalScrollbar.getElement().getStyle()
+                    .setTop(header.height, Unit.PX);
+            verticalScrollbar.getElement().getStyle()
+                    .setBottom(vScrollBottom, Unit.PX);
+            verticalScrollbar.setScrollSize(innerScrollerHeight);
 
             // TODO [[colwidth]]: adjust for variable column widths.
             int columnsToScroll = columnConfiguration.getColumnCount()
                     - columnConfiguration.getFrozenColumnCount();
-            innerScrollerElem.getStyle().setWidth(
-                    COLUMN_WIDTH_PX * columnsToScroll, Unit.PX);
+            horizontalScrollbar
+                    .setScrollSize(COLUMN_WIDTH_PX * columnsToScroll);
+
+            final Style hScrollbarStyle = horizontalScrollbar.getElement()
+                    .getStyle();
+            if (needsVerticalScrollbars()) {
+                final int hScrollbarRight = verticalScrollbar
+                        .getScrollbarThickness();
+                hScrollbarStyle.setRight(hScrollbarRight, Unit.PX);
+            } else {
+                hScrollbarStyle.clearRight();
+            }
 
             // we might've got new or got rid of old scrollbars.
             recalculateTableWrapperSize();
@@ -620,15 +606,15 @@ public class Escalator extends Widget {
          */
         public void recalculateTableWrapperSize() {
             double wrapperHeight = height;
-            if (scrollerElem.getOffsetWidth() < innerScrollerElem
-                    .getOffsetWidth()) {
-                wrapperHeight -= Util.getNativeScrollbarSize();
+            if (horizontalScrollbar.getOffsetSize() < horizontalScrollbar
+                    .getScrollSize()) {
+                wrapperHeight -= horizontalScrollbar.getScrollbarThickness();
             }
 
             double wrapperWidth = width;
-            if (scrollerElem.getOffsetHeight() - footer.height < innerScrollerElem
-                    .getOffsetHeight()) {
-                wrapperWidth -= Util.getNativeScrollbarSize();
+            if (verticalScrollbar.getOffsetSize() - footer.height < verticalScrollbar
+                    .getScrollSize()) {
+                wrapperWidth -= verticalScrollbar.getScrollbarThickness();
             }
 
             tableWrapper.getStyle().setHeight(wrapperHeight, Unit.PX);
@@ -651,8 +637,8 @@ public class Escalator extends Widget {
                 return;
             }
 
-            final int scrollLeft = scrollerElem.getScrollLeft();
-            final int scrollTop = scrollerElem.getScrollTop();
+            final int scrollLeft = horizontalScrollbar.getScrollPos();
+            final int scrollTop = verticalScrollbar.getScrollPos();
 
             if (lastScrollLeft != scrollLeft) {
                 for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
@@ -895,7 +881,7 @@ public class Escalator extends Widget {
         }
     }
 
-    private static final String CLASS_NAME = "v-escalator";
+    static final String CLASS_NAME = "v-escalator";
 
     private abstract class AbstractRowContainer implements RowContainer {
         private EscalatorUpdater updater = EscalatorUpdater.NULL;
@@ -1165,11 +1151,11 @@ public class Escalator extends Widget {
             }
 
             /*
-             * TODO [[escalator]][[rowheight]]: even if no rows are evaluated in
-             * the current viewport, the heights of some unrendered rows might
-             * change in a refresh. This would cause the scrollbar to be
-             * adjusted (in scrollHeight and/or scrollTop). Do we want to take
-             * this into account?
+             * TODO [[rowheight]]: even if no rows are evaluated in the current
+             * viewport, the heights of some unrendered rows might change in a
+             * refresh. This would cause the scrollbar to be adjusted (in
+             * scrollHeight and/or scrollTop). Do we want to take this into
+             * account?
              */
             if (hasColumnAndRowData()) {
                 /*
@@ -1251,7 +1237,7 @@ public class Escalator extends Widget {
                 final int leftByDiff = (int) (scroller.lastScrollLeft - removedColumnsPxAmount);
                 final int newScrollLeft = Math.max(firstRemovedColumnLeft,
                         leftByDiff);
-                scrollerElem.setScrollLeft(newScrollLeft);
+                horizontalScrollbar.setScrollPos(newScrollLeft);
             }
 
             // this needs to be after the scroll position adjustment above.
@@ -1304,8 +1290,8 @@ public class Escalator extends Widget {
                     * COLUMN_WIDTH_PX;
 
             if (columnsWereAddedToTheLeftOfViewport) {
-                scrollerElem
-                        .setScrollLeft((int) (scroller.lastScrollLeft + numberOfColumns
+                horizontalScrollbar
+                        .setScrollPos((int) (scroller.lastScrollLeft + numberOfColumns
                                 * COLUMN_WIDTH_PX));
             }
         }
@@ -1562,10 +1548,9 @@ public class Escalator extends Widget {
              */
             scroller.recalculateScrollbarsForVirtualViewport();
 
-            final boolean addedRowsAboveCurrentViewport = index * ROW_HEIGHT_PX < scrollerElem
-                    .getScrollTop();
-            final boolean addedRowsBelowCurrentViewport = index * ROW_HEIGHT_PX > scrollerElem
-                    .getScrollTop() + calculateHeight();
+            final boolean addedRowsAboveCurrentViewport = index * ROW_HEIGHT_PX < getScrollTop();
+            final boolean addedRowsBelowCurrentViewport = index * ROW_HEIGHT_PX > getScrollTop()
+                    + calculateHeight();
 
             if (addedRowsAboveCurrentViewport) {
                 /*
@@ -1751,7 +1736,7 @@ public class Escalator extends Widget {
             }
 
             internalScrollEventCalls++;
-            scrollerElem.setScrollTop(scrollerElem.getScrollTop() + yDelta);
+            verticalScrollbar.setScrollPosByDelta(yDelta);
 
             final int snappedYDelta = yDelta - yDelta % ROW_HEIGHT_PX;
             for (final Element tr : visualRowOrder) {
@@ -1876,8 +1861,8 @@ public class Escalator extends Widget {
                 // TODO [[rowheight]]
                 final int yDelta = removedAbove.length() * ROW_HEIGHT_PX;
                 final int firstLogicalRowHeight = ROW_HEIGHT_PX;
-                final boolean removalScrollsToShowFirstLogicalRow = scrollerElem
-                        .getScrollTop() - yDelta < firstLogicalRowHeight;
+                final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
+                        .getScrollPos() - yDelta < firstLogicalRowHeight;
 
                 if (removedVisualInside.isEmpty()
                         && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
@@ -1895,7 +1880,8 @@ public class Escalator extends Widget {
                      * current negative scrolltop, presto!), so that it isn't
                      * aligned funnily
                      */
-                    adjustScrollPosIgnoreEvents(-scrollerElem.getScrollTop());
+                    adjustScrollPosIgnoreEvents(-verticalScrollbar
+                            .getScrollPos());
                 }
             }
 
@@ -2584,9 +2570,17 @@ public class Escalator extends Widget {
                 }
             }
 
-            scrollerElem.getStyle().setLeft(frozenColumns * COLUMN_WIDTH_PX,
-                    Unit.PX);
+            /*
+             * If decreasing the amount of frozen columns, and scrolled to the
+             * right, the scroll position will reset. So we need to remember the
+             * scroll position, and re-apply it once the scrollbar size has been
+             * adjusted.
+             */
+            int scrollPos = horizontalScrollbar.getScrollPos();
+            horizontalScrollbar.getElement().getStyle()
+                    .setLeft(frozenColumns * COLUMN_WIDTH_PX, Unit.PX);
             scroller.recalculateScrollbarsForVirtualViewport();
+            horizontalScrollbar.setScrollPos(scrollPos);
         }
 
         @Override
@@ -2632,8 +2626,8 @@ public class Escalator extends Widget {
     private int tBodyScrollTop = 0;
     private int tBodyScrollLeft = 0;
 
-    private final Element scrollerElem = DOM.createDiv();
-    private final Element innerScrollerElem = DOM.createDiv();
+    private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
+    private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
 
     private final HeaderRowContainer header = new HeaderRowContainer(headElem);
     private final BodyRowContainer body = new BodyRowContainer(bodyElem);
@@ -2690,10 +2684,11 @@ public class Escalator extends Widget {
         setElement(root);
         setStyleName(CLASS_NAME);
 
-        scrollerElem.setClassName(CLASS_NAME + "-scroller");
-        root.appendChild(scrollerElem);
-
-        scrollerElem.appendChild(innerScrollerElem);
+        root.appendChild(verticalScrollbar.getElement());
+        root.appendChild(horizontalScrollbar.getElement());
+        verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize());
+        horizontalScrollbar
+                .setScrollbarThickness(Util.getNativeScrollbarSize());
 
         tableWrapper = DOM.createDiv();
         tableWrapper.setClassName(CLASS_NAME + "-tablewrapper");
@@ -2732,11 +2727,17 @@ public class Escalator extends Widget {
                     recalculateElementSizes();
                     body.paintInsertRows(0, body.getRowCount());
 
-                    scroller.attachScrollListener(scrollerElem);
+                    scroller.attachScrollListener(verticalScrollbar
+                            .getElement());
+                    scroller.attachScrollListener(horizontalScrollbar
+                            .getElement());
                     scroller.attachMousewheelListener(getElement());
                     scroller.attachTouchListeners(getElement());
                 } else {
-                    scroller.detachScrollListener(scrollerElem);
+                    scroller.detachScrollListener(verticalScrollbar
+                            .getElement());
+                    scroller.detachScrollListener(horizontalScrollbar
+                            .getElement());
                     scroller.detachMousewheelListener(getElement());
                     scroller.detachTouchListeners(getElement());
                 }
@@ -2870,7 +2871,7 @@ public class Escalator extends Widget {
      * @return the logical vertical scroll offset
      */
     public double getScrollTop() {
-        return scrollerElem.getScrollTop();
+        return verticalScrollbar.getScrollPos();
     }
 
     /**
@@ -2881,7 +2882,7 @@ public class Escalator extends Widget {
      *            the number of pixels to scroll vertically
      */
     public void setScrollTop(final double scrollTop) {
-        scrollerElem.setScrollTop((int) scrollTop);
+        verticalScrollbar.setScrollPos((int) scrollTop);
     }
 
     /**
@@ -2891,7 +2892,7 @@ public class Escalator extends Widget {
      * @return the logical horizontal scroll offset
      */
     public int getScrollLeft() {
-        return scrollerElem.getScrollLeft();
+        return horizontalScrollbar.getScrollPos();
     }
 
     /**
@@ -2902,7 +2903,7 @@ public class Escalator extends Widget {
      *            the number of pixels to scroll horizontally
      */
     public void setScrollLeft(final int scrollLeft) {
-        scrollerElem.setScrollLeft(scrollLeft);
+        horizontalScrollbar.setScrollPos(scrollLeft);
     }
 
     /**
diff --git a/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java
new file mode 100644 (file)
index 0000000..82c2c56
--- /dev/null
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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.vaadin.client.ui.grid;
+
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+
+/**
+ * An element-like bundle representing a configurable and visual scrollbar in
+ * one axis.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see VerticalScrollbarBundle
+ * @see HorizontalScrollbarBundle
+ */
+abstract class ScrollbarBundle {
+    private static final String CLASS_NAME = Escalator.CLASS_NAME + "-scroller";
+
+    /**
+     * The pixel size for OSX's invisible scrollbars.
+     * <p>
+     * Touch devices don't show a scrollbar at all, so the scrollbar size is
+     * irrelevant in their case. There doesn't seem to be any other popular
+     * platforms that has scrollbars similar to OSX. Thus, this behavior is
+     * tailored for OSX only, until additional platforms start behaving this
+     * way.
+     */
+    private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;
+
+    /**
+     * A representation of a single vertical scrollbar.
+     * 
+     * @see VerticalScrollbarBundle#getElement()
+     */
+    final static class VerticalScrollbarBundle extends ScrollbarBundle {
+        public VerticalScrollbarBundle() {
+            root.addClassName(CLASS_NAME + "-vertical");
+        }
+
+        @Override
+        public void setScrollPos(int px) {
+            root.setScrollTop(px);
+        }
+
+        @Override
+        public int getScrollPos() {
+            return root.getScrollTop();
+        }
+
+        @Override
+        protected void internalSetScrollSize(int px) {
+            scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+        }
+
+        @Override
+        public int getScrollSize() {
+            return scrollSizeElement.getOffsetHeight();
+        }
+
+        @Override
+        protected void internalSetOffsetSize(int px) {
+            root.getStyle().setHeight(px, Unit.PX);
+        }
+
+        @Override
+        public int getOffsetSize() {
+            return root.getOffsetHeight();
+        }
+
+        @Override
+        protected void internalSetScrollbarThickness(int px) {
+            root.getStyle().setWidth(px, Unit.PX);
+            scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+        }
+
+        @Override
+        protected int internalGetScrollbarThickness() {
+            return root.getOffsetWidth();
+        }
+
+        @Override
+        protected void forceScrollbar(boolean enable) {
+            if (enable) {
+                root.getStyle().setOverflowY(Overflow.SCROLL);
+            } else {
+                root.getStyle().clearOverflowY();
+            }
+        }
+    }
+
+    /**
+     * A representation of a single horizontal scrollbar.
+     * 
+     * @see HorizontalScrollbarBundle#getElement()
+     */
+    final static class HorizontalScrollbarBundle extends ScrollbarBundle {
+        protected HorizontalScrollbarBundle() {
+            root.addClassName(CLASS_NAME + "-horizontal");
+        }
+
+        @Override
+        public void setScrollPos(int px) {
+            root.setScrollLeft(px);
+        }
+
+        @Override
+        public int getScrollPos() {
+            return root.getScrollLeft();
+        }
+
+        @Override
+        protected void internalSetScrollSize(int px) {
+            scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+        }
+
+        @Override
+        public int getScrollSize() {
+            return scrollSizeElement.getOffsetWidth();
+        }
+
+        @Override
+        protected void internalSetOffsetSize(int px) {
+            root.getStyle().setWidth(px, Unit.PX);
+        }
+
+        @Override
+        public int getOffsetSize() {
+            return root.getOffsetWidth();
+        }
+
+        @Override
+        protected void internalSetScrollbarThickness(int px) {
+            root.getStyle().setHeight(px, Unit.PX);
+            scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+        }
+
+        @Override
+        protected int internalGetScrollbarThickness() {
+            return root.getOffsetHeight();
+        }
+
+        @Override
+        protected void forceScrollbar(boolean enable) {
+            if (enable) {
+                root.getStyle().setOverflowX(Overflow.SCROLL);
+            } else {
+                root.getStyle().clearOverflowX();
+            }
+        }
+    }
+
+    protected final Element root = DOM.createDiv();
+    protected final Element scrollSizeElement = DOM.createDiv();
+    protected boolean isInvisibleScrollbar = false;
+
+    private ScrollbarBundle() {
+        root.appendChild(scrollSizeElement);
+        root.setClassName(CLASS_NAME);
+    }
+
+    /**
+     * Gets the root element of this scrollbar-composition.
+     * 
+     * @return the root element
+     */
+    public Element getElement() {
+        return root;
+    }
+
+    /**
+     * Modifies the scroll position of this scrollbar by a number of pixels
+     * 
+     * @param delta
+     *            the delta in pixels to change the scroll position by
+     */
+    public void setScrollPosByDelta(int delta) {
+        if (delta != 0) {
+            /*
+             * TODO [[optimize]]: rewrite this so that we can evoid a potential
+             * reflow from getScrollPos()
+             */
+            setScrollPos(getScrollPos() + delta);
+        }
+    }
+
+    /**
+     * Modifies {@link #root root's} dimensions in the axis the scrollbar is
+     * representing.
+     * 
+     * @param px
+     *            the new size of {@link #root} in the dimension this scrollbar
+     *            is representing
+     */
+    protected abstract void internalSetOffsetSize(int px);
+
+    /**
+     * Sets the length of the scrollbar.
+     * 
+     * @param px
+     *            the length of the scrollbar in pixels
+     */
+    public void setOffsetSize(int px) {
+        internalSetOffsetSize(px);
+        forceScrollbar(needsScrollbars());
+    }
+
+    /**
+     * Force the scrollbar to be visible with CSS. In practice, this means to
+     * set either <code>overflow-x</code> or <code>overflow-y</code> to "
+     * <code>scroll</code>" in the scrollbar's direction.
+     * <p>
+     * This is an IE8 workaround, since it doesn't always show scrollbars with
+     * <code>overflow: auto</code> enabled.
+     */
+    protected abstract void forceScrollbar(boolean enable);
+
+    /**
+     * Gets the length of the scrollbar
+     * 
+     * @return the length of the scrollbar in pixels
+     */
+    public abstract int getOffsetSize();
+
+    /**
+     * Sets the scroll position of the scrollbar in the axis the scrollbar is
+     * representing.
+     * 
+     * @param px
+     *            the new scroll position in pixels
+     */
+    public abstract void setScrollPos(int px);
+
+    /**
+     * Gets the scroll position of the scrollbar in the axis the scrollbar is
+     * representing.
+     * 
+     * @return the new scroll position in pixels
+     */
+    public abstract int getScrollPos();
+
+    /**
+     * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
+     * such a way that the scrollbar is able to scroll a certain number of
+     * pixels in the axis it is representing.
+     * 
+     * @param px
+     *            the new size of {@link #scrollSizeElement} in the dimension
+     *            this scrollbar is representing
+     */
+    protected abstract void internalSetScrollSize(int px);
+
+    /**
+     * Sets the amount of pixels the scrollbar needs to be able to scroll
+     * through.
+     * 
+     * @param px
+     *            the number of pixels the scrollbar should be able to scroll
+     *            through
+     */
+    public void setScrollSize(int px) {
+        internalSetScrollSize(px);
+        forceScrollbar(needsScrollbars());
+    }
+
+    /**
+     * Gets the amount of pixels the scrollbar needs to be able to scroll
+     * through.
+     * 
+     * @return the number of pixels the scrollbar should be able to scroll
+     *         through
+     */
+    public abstract int getScrollSize();
+
+    /**
+     * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
+     * opposite axis to what the scrollbar is representing.
+     * 
+     * @param px
+     *            the dimension that {@link #scrollSizeElement} should take in
+     *            the opposite axis to what the scrollbar is representing
+     */
+    protected abstract void internalSetScrollbarThickness(int px);
+
+    /**
+     * Sets the scrollbar's thickness.
+     * <p>
+     * If the thickness is set to 0, the scrollbar will be treated as an
+     * "invisible" scrollbar. This means, the DOM structure will be given a
+     * non-zero size, but {@link #getScrollbarThickness()} will still return the
+     * value 0.
+     * 
+     * @param px
+     *            the scrollbar's thickness in pixels
+     */
+    public void setScrollbarThickness(int px) {
+        isInvisibleScrollbar = (px == 0);
+        internalSetScrollbarThickness(px != 0 ? px
+                : OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
+    }
+
+    /**
+     * Gets the scrollbar's thickness as defined in the DOM.
+     * 
+     * @return the scrollbar's thickness as defined in the DOM, in pixels
+     */
+    protected abstract int internalGetScrollbarThickness();
+
+    /**
+     * Gets the scrollbar's thickness.
+     * <p>
+     * This value will differ from the value in the DOM, if the thickness was
+     * set to 0 with {@link #setScrollbarThickness(int)}, as the scrollbar is
+     * then treated as "invisible."
+     * 
+     * @return the scrollbar's thickness in pixels
+     */
+    public int getScrollbarThickness() {
+        if (!isInvisibleScrollbar) {
+            return internalGetScrollbarThickness();
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Checks whether the scrollbar should be active and needed, i.e. if the
+     * "content" is larger than the "frame".
+     * 
+     * @return <code>true</code> iff the scrollbar should be active and needed
+     */
+    protected boolean needsScrollbars() {
+        return getOffsetSize() < getScrollSize();
+    }
+}