aboutsummaryrefslogtreecommitdiffstats
path: root/client/src
diff options
context:
space:
mode:
authorHenrik Paul <henrik@vaadin.com>2013-09-24 15:03:56 +0300
committerHenrik Paul <henrik@vaadin.com>2013-10-09 14:26:05 +0300
commitb29caad20c1d14a655f546493f6c5507a0a6f856 (patch)
tree08c8e09bc2948f3d9fbbc5f2029c7ec0db5d7c37 /client/src
parent0f7bcffdb9f753148d1027ff380c9520bb78bfd8 (diff)
downloadvaadin-framework-b29caad20c1d14a655f546493f6c5507a0a6f856.tar.gz
vaadin-framework-b29caad20c1d14a655f546493f6c5507a0a6f856.zip
Initial escalator commit (#12645)
Change-Id: Ibd0ac2896e12b99ddebdc26674a2dfced486c49a
Diffstat (limited to 'client/src')
-rw-r--r--client/src/com/vaadin/client/ui/grid/Cell.java56
-rw-r--r--client/src/com/vaadin/client/ui/grid/CellRenderer.java43
-rw-r--r--client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java81
-rw-r--r--client/src/com/vaadin/client/ui/grid/Escalator.java751
-rw-r--r--client/src/com/vaadin/client/ui/grid/PositionFunction.java88
-rw-r--r--client/src/com/vaadin/client/ui/grid/RowContainer.java126
6 files changed, 1145 insertions, 0 deletions
diff --git a/client/src/com/vaadin/client/ui/grid/Cell.java b/client/src/com/vaadin/client/ui/grid/Cell.java
new file mode 100644
index 0000000000..08f3eeb9dc
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Cell.java
@@ -0,0 +1,56 @@
+/*
+ * 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.user.client.Element;
+
+/**
+ * A representation of a single cell.
+ * <p>
+ * A Cell instance will be provided to the {@link CellRenderer} responsible for
+ * rendering the cells in a certain {@link RowContainer}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see CellRenderer#renderCell(Cell)
+ */
+public interface Cell {
+ /**
+ * Returns the index of the row this cell is in.
+ *
+ * @return the index of the row this cell is in
+ */
+ public int getRow();
+
+ /**
+ * Returns the index of the column this cell is in.
+ *
+ * @return the index of the column this cell is in
+ */
+ public int getColumn();
+
+ /**
+ * Returns the root element for this cell. The {@link CellRenderer} may
+ * update the class names of the element, add inline styles and freely
+ * modify the contents.
+ * <p>
+ * Avoid modifying the dimensions or positioning of the cell element.
+ *
+ * @return The root element for this cell. Never <code>null</code>.
+ */
+ public Element getElement();
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/CellRenderer.java b/client/src/com/vaadin/client/ui/grid/CellRenderer.java
new file mode 100644
index 0000000000..636a512e81
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/CellRenderer.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * An interface that defines how the cells in a {@link RowContainer} should look
+ * like.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see RowContainer#setCellRenderer(CellRenderer)
+ */
+public interface CellRenderer {
+ /** A {@link CellRenderer} that doesn't render anything. */
+ public static final CellRenderer NULL_RENDERER = new CellRenderer() {
+ @Override
+ public void renderCell(final Cell cell) {
+ }
+ };
+
+ /**
+ * Renders a cell contained in a row container.
+ *
+ * @param cell
+ * the cell that can be manipulated to modify the contents of the
+ * cell being rendered. Never <code>null</code>.
+ */
+ public void renderCell(Cell cell);
+}
diff --git a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
new file mode 100644
index 0000000000..be60b1a49b
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+/**
+ * A representation of the columns in an instance of {@link Escalator}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator#getColumnConfiguration()
+ */
+public interface ColumnConfiguration {
+
+ /**
+ * Removes columns at a certain offset.
+ *
+ * @param offset
+ * the index of the first column to be removed
+ * @param numberOfColumns
+ * the number of rows to remove, starting from the offset
+ * @throws IndexOutOfBoundsException
+ * if any integer in the range
+ * <code>[offset..(offset+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if <code>numberOfColumns</code> is less than 1.
+ */
+ public void removeColumns(int offset, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds columns at a certain offset.
+ * <p>
+ * The new columns will be inserted between the column at the offset, and
+ * the column before (an offset of 0 means that the columns are inserted at
+ * the beginning). Therefore, the columns at the offset and afterwards will
+ * be moved to the right.
+ * <p>
+ * The contents of the inserted columns will be queried from the respective
+ * cell renderers in the header, body and footer.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted columns will be
+ * rendered. If inserting new columns affects the contents of existing
+ * columns, {@link RowContainer#refreshRows(int, int)} needs to be called as
+ * appropriate.
+ *
+ * @param offset
+ * the index of the column before which new columns are inserted,
+ * or {@link #getColumnCount()} to add new columns at the end
+ * @param numberOfColumns
+ * the number of columns to insert after the <code>offset</code>
+ * @throws IndexOutOfBoundsException
+ * if <code>offset</code> is not an integer in the range
+ * <code>[0..{@link #getColumnCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfColumns} is less than 1.
+ */
+ public void insertColumns(int offset, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Returns the number of columns in the escalator.
+ *
+ * @return the number of columns in the escalator
+ */
+ public int getColumnCount();
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java
new file mode 100644
index 0000000000..73986503d6
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Escalator.java
@@ -0,0 +1,751 @@
+/*
+ * 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 java.util.logging.Logger;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.logical.shared.AttachEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+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;
+
+/**
+ * A low-level table-like widget that features a scrolling virtual viewport and
+ * lazily generated rows.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class Escalator extends Widget {
+
+ // todo comments legend
+ /*
+ * [[optimize]]: There's an opportunity to rewrite the code in such a way
+ * that it _might_ perform better (rememeber to measure, implement,
+ * re-measure)
+ */
+ /*
+ * [[escalator]]: This needs to be re-inspected once the escalator pattern
+ * is actually implemented.
+ */
+ /*
+ * [[rowwidth]] [[colwidth]]: This needs to be re-inspected once hard-coded
+ * values are removed, and cell dimensions are actually being calculated.
+ * NOTE: these bits can most often also be identified by searching for code
+ * reading the ROW_HEIGHT_PX and COL_WIDTH_PX constans.
+ */
+ /*
+ * [[API]]: Implementing this suggestion would require a change in the
+ * public API. These suggestions usually don't come lightly.
+ */
+
+ private static final int ROW_HEIGHT_PX = 20;
+ private static final int COLUMN_WIDTH_PX = 100;
+
+ private static class CellImpl implements Cell {
+ private final Element cellElem;
+ private final int row;
+ private final int column;
+
+ public CellImpl(final Element cellElem, final int row, final int column) {
+ this.cellElem = cellElem;
+ this.row = row;
+ this.column = column;
+ }
+
+ @Override
+ public int getRow() {
+ return row;
+ }
+
+ @Override
+ public int getColumn() {
+ return column;
+ }
+
+ @Override
+ public Element getElement() {
+ return cellElem;
+ }
+
+ }
+
+ private static final String CLASS_NAME = "v-escalator";
+
+ private class RowContainerImpl implements RowContainer {
+ private CellRenderer renderer = CellRenderer.NULL_RENDERER;
+
+ private int rows;
+
+ /**
+ * The table section element ({@code <thead>}, {@code <tbody>} or
+ * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
+ */
+ private final Element root;
+
+ /**
+ * What cell type to contain in this {@link RowContainer}. Usually
+ * either a {@code <th>} or {@code <td>}.
+ */
+ private final String cellElementTag;
+
+ public RowContainerImpl(final Element rowContainerElement,
+ final String cellElementTag) {
+ root = rowContainerElement;
+ this.cellElementTag = cellElementTag;
+ }
+
+ /**
+ * Informs the row container that the height of its respective table
+ * section has changed.
+ * <p>
+ * These calculations might affect some layouting logic, such as the
+ * body is being offset by the footer, the footer needs to be readjusted
+ * according to its height, and so on.
+ * <p>
+ * A table section is either header, body or footer.
+ *
+ * @param newPxHeight
+ * The new pixel height
+ */
+ protected void sectionHeightCalculated(final double newPxHeight) {
+ // override if implementation is needed
+ };
+
+ private Element createCellElement() {
+ return DOM.createElement(cellElementTag);
+ }
+
+ @Override
+ public CellRenderer getCellRenderer() {
+ return renderer;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows or columns
+ * when this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void setCellRenderer(final CellRenderer cellRenderer) {
+ if (cellRenderer == null) {
+ throw new IllegalArgumentException(
+ "cell renderer cannot be null");
+ }
+
+ renderer = cellRenderer;
+
+ if (hasColumnAndRowData() && getRowCount() > 0) {
+ refreshRows(0, getRowCount());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeRows(final int offset, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(offset, numberOfRows);
+
+ rows -= numberOfRows;
+
+ if (hasSomethingInDom()) {
+ for (int i = 0; i < numberOfRows; i++) {
+ root.getChild(offset).removeFromParent();
+ }
+ }
+ refreshRowPositions(offset, getRowCount());
+ recalculateSectionHeight();
+ }
+
+ private void assertArgumentsAreValidAndWithinRange(final int offset,
+ final int numberOfRows) throws IllegalArgumentException,
+ IndexOutOfBoundsException {
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ if (offset < 0 || offset + numberOfRows > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "row range (" + offset + ".."
+ + (offset + numberOfRows)
+ + ") was outside of the current number of rows ("
+ + getRowCount() + ")");
+ }
+ }
+
+ @Override
+ public int getRowCount() {
+ return rows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertRows(final int offset, final int numberOfRows) {
+ if (offset < 0 || offset > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given offset ("
+ + offset
+ + ") was outside of the current number of rows (0.."
+ + getRowCount() + ")");
+ }
+
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ rows += numberOfRows;
+
+ /*
+ * TODO [[escalator]]: modify offset and numberOfRows so that they
+ * suit the current viewport. If a partial dataset is shown,update
+ * only the part that is visible. If the viewport doesn't show any
+ * of the modifications, this method does nothing.
+ */
+
+ /*
+ * TODO [[escalator]]: assert that escalatorChildIndex is a number
+ * equal or less than the number of escalator rows
+ */
+
+ Node referenceNode;
+ if (root.getChildCount() != 0 && offset != 0) {
+ // get the row node we're inserting stuff after
+ referenceNode = root.getChild(offset - 1);
+ } else {
+ // there are now rows, so just append.
+ referenceNode = null;
+ }
+
+ for (int row = offset; row < offset + numberOfRows; row++) {
+ final Element tr = DOM.createTR();
+
+ for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
+ final Element cellElem = createCellElement();
+ paintCell(cellElem, row, col);
+ tr.appendChild(cellElem);
+ }
+
+ /*
+ * TODO [[optimize]] [[rowwidth]]: When this method is updated
+ * to measure things instead of using hardcoded values, it would
+ * be better to do everything at once after all rows have been
+ * updated to reduce the number of reflows.
+ */
+ recalculateRowWidth(tr);
+ tr.addClassName(CLASS_NAME + "-row");
+
+ position.set(tr, 0, row * ROW_HEIGHT_PX);
+
+ if (referenceNode != null) {
+ root.insertAfter(tr, referenceNode);
+ } else {
+ /*
+ * referencenode being null means we have offset 0, i.e.
+ * make it the first row
+ */
+ /*
+ * TODO [[optimize]]: Is insertFirst or append faster for an
+ * empty root?
+ */
+ root.insertFirst(tr);
+ }
+
+ /*
+ * to get the rows to appear one after another in a logical
+ * order, update the reference
+ */
+ referenceNode = tr;
+ }
+
+ /*
+ * we need to update the positions of all rows beneath the ones
+ * added right now.
+ */
+ refreshRowPositions(offset + numberOfRows, getRowCount());
+
+ /*
+ * TODO [[optimize]]: maybe the height doesn't always change?
+ */
+ recalculateSectionHeight();
+ }
+
+ /**
+ * Re-evaluates the positional coordinates for the rows in the given
+ * range. The given range is truncated to suit the given viewport.
+ *
+ * @param offset
+ * starting row index
+ * @param numberOfRows
+ * the number of rows after {@code offset} to refresh the
+ * positions of
+ */
+ private void refreshRowPositions(final int offset,
+ final int numberOfRows) {
+ final int startRow = Math.max(0, offset);
+ final int endRow = Math.min(getRowCount(), offset + numberOfRows);
+
+ for (int row = startRow; row < endRow; row++) {
+ Element tr = (Element) root.getChild(row);
+ position.set(tr, 0, row * ROW_HEIGHT_PX);
+ }
+ }
+
+ private void recalculateSectionHeight() {
+ /* TODO [[optimize]]: only do this if the height has changed */
+ sectionHeightCalculated(root.getChildCount() * ROW_HEIGHT_PX);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void refreshRows(final int offset, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(offset, numberOfRows);
+
+ /*
+ * TODO [[escalator]]: modify offset and numberOfRows to fit in the
+ * current viewport. If they don't fall into the current viewport,
+ * NOOP
+ */
+
+ if (hasColumnAndRowData()) {
+ /*
+ * TODO [[rowheight]]: nudge rows down with
+ * refreshRowPositions() as needed
+ */
+ /*
+ * TODO [[colwidth]]: reapply column and colspan widths as
+ * needed
+ */
+
+ for (int row = offset; row < offset + numberOfRows; row++) {
+ Node tr = root.getChild(row);
+ for (int col = 0; col < tr.getChildCount(); col++) {
+ paintCell((Element) tr.getChild(col), row, col);
+ }
+ }
+ }
+ }
+
+ private void paintCell(final Element cellElem, final int row,
+ final int col) {
+ /*
+ * TODO [[optimize]]: Only do this for new cells or when a row
+ * height or column width actually changes. Or is it a NOOP when
+ * re-setting a property to its current value?
+ */
+ cellElem.getStyle().setHeight(ROW_HEIGHT_PX, Unit.PX);
+ cellElem.getStyle().setWidth(COLUMN_WIDTH_PX, Unit.PX);
+
+ /*
+ * TODO [[optimize]]: Don't create a new instance every time a cell
+ * is rendered
+ */
+ final CellImpl cell = new CellImpl(cellElem, row, col);
+ /*
+ * TODO [[optimize]] [[API]]: Let the renderer know whether the cell
+ * is new so that it can use a quicker route if it can deduct that
+ * the elements that it has put there in a previous rendering is
+ * still there and the contents only need to be updated.
+ */
+ renderer.renderCell(cell);
+
+ /*
+ * TODO [[optimize]]: Only do this for cells that have not already
+ * been rendered.
+ */
+ cellElem.addClassName(CLASS_NAME + "-cell");
+ }
+ }
+
+ private class ColumnConfigurationImpl implements ColumnConfiguration {
+ private int columns = 0;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeColumns(final int offset, final int numberOfColumns) {
+ assertArgumentsAreValidAndWithinRange(offset, numberOfColumns);
+
+ columns--;
+
+ if (hasSomethingInDom()) {
+ for (RowContainerImpl rowContainer : rowContainers) {
+ for (int row = 0; row < rowContainer.getRowCount(); row++) {
+ Node tr = rowContainer.root.getChild(row);
+ for (int col = 0; col < numberOfColumns; col++) {
+ tr.getChild(offset).removeFromParent();
+ }
+ }
+ }
+ }
+ }
+
+ private void assertArgumentsAreValidAndWithinRange(final int offset,
+ final int numberOfColumns) {
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns can't be less than 1 (was "
+ + numberOfColumns + ")");
+ }
+
+ if (offset < 0 || offset + numberOfColumns > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "column range (" + offset + ".."
+ + (offset + numberOfColumns)
+ + ") was outside of the current "
+ + "number of columns (" + getColumnCount() + ")");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows when this
+ * method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertColumns(final int offset, final int numberOfColumns) {
+ if (offset < 0 || offset > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given offset("
+ + offset
+ + ") was outside of the current number of columns (0.."
+ + getColumnCount() + ")");
+ }
+
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns must be 1 or greater (was "
+ + numberOfColumns);
+ }
+
+ columns += numberOfColumns;
+ if (!hasColumnAndRowData()) {
+ return;
+ }
+
+ for (final RowContainerImpl rowContainer : rowContainers) {
+ final Element element = rowContainer.root;
+
+ for (int row = 0; row < element.getChildCount(); row++) {
+ final Element tr = (Element) element.getChild(row);
+
+ Node referenceElement;
+ if (offset != 0) {
+ referenceElement = tr.getChild(offset - 1);
+ } else {
+ referenceElement = null;
+ }
+
+ for (int col = offset; col < offset + numberOfColumns; col++) {
+ final Element cellElem = rowContainer
+ .createCellElement();
+ rowContainer.paintCell(cellElem, row, col);
+
+ if (referenceElement != null) {
+ tr.insertAfter(cellElem, referenceElement);
+ } else {
+ /*
+ * referenceElement being null means we have offset
+ * 0, make it the first cell.
+ */
+ /*
+ * TODO [[optimize]]: Is insertFirst or append
+ * faster for an empty tr?
+ */
+ tr.insertFirst(cellElem);
+ }
+
+ /*
+ * update reference to insert cells in logical order,
+ * the latter after the former
+ */
+ referenceElement = cellElem;
+ }
+
+ /*
+ * TODO [[optimize]] [[colwidth]]: When this method is
+ * updated to measure things instead of using hardcoded
+ * values, it would be better to do everything at once after
+ * all rows have been updated to reduce the number of
+ * reflows.
+ */
+ recalculateRowWidth(tr);
+ }
+ }
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columns;
+ }
+ }
+
+ private final Element headElem = DOM.createTHead();
+ private final Element bodyElem = DOM.createTBody();
+ private final Element footElem = DOM.createTFoot();
+ private final Element scroller;
+ private final Element innerScroller;
+
+ private final RowContainerImpl header = new RowContainerImpl(headElem, "th") {
+ @Override
+ protected void sectionHeightCalculated(final double newPxHeight) {
+ bodyElem.getStyle().setTop(newPxHeight, Unit.PX);
+ };
+ };
+
+ private final RowContainerImpl body = new RowContainerImpl(bodyElem, "td");
+
+ private final RowContainerImpl footer = new RowContainerImpl(footElem, "td") {
+ @Override
+ protected void sectionHeightCalculated(final double newPxHeight) {
+ footElem.getStyle().setBottom(newPxHeight, Unit.PX);
+ }
+ };
+
+ private final RowContainerImpl[] rowContainers = new RowContainerImpl[] {
+ header, body, footer };
+
+ private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
+ private final Element tableWrapper;
+
+ private PositionFunction position;
+
+ /**
+ * Creates a new Escalator widget instance.
+ */
+ public Escalator() {
+
+ detectAndApplyPositionFunction();
+
+ final Element root = DOM.createDiv();
+ setElement(root);
+ setStyleName(CLASS_NAME);
+
+ scroller = DOM.createDiv();
+ scroller.setClassName(CLASS_NAME + "-scroller");
+ root.appendChild(scroller);
+
+ innerScroller = DOM.createDiv();
+ scroller.appendChild(innerScroller);
+
+ tableWrapper = DOM.createDiv();
+ tableWrapper.setClassName(CLASS_NAME + "-tablewrapper");
+ root.appendChild(tableWrapper);
+
+ final Element table = DOM.createTable();
+ tableWrapper.appendChild(table);
+
+ headElem.setClassName(CLASS_NAME + "-header");
+ table.appendChild(headElem);
+
+ bodyElem.setClassName(CLASS_NAME + "-body");
+ table.appendChild(bodyElem);
+
+ footElem.setClassName(CLASS_NAME + "-footer");
+ table.appendChild(footElem);
+
+ /*
+ * Size calculations work only after the Escalator has been attached to
+ * the DOM. It doesn't matter if the table is populated or not by this
+ * point, there's a lot of other stuff to calculate also. All sizes
+ * start working once the first sizes have been initialized.
+ */
+ addAttachHandler(new AttachEvent.Handler() {
+ @Override
+ public void onAttachOrDetach(final AttachEvent event) {
+ if (event.isAttached()) {
+ recalculateElementSizes();
+ }
+ }
+ });
+ }
+
+ private void detectAndApplyPositionFunction() {
+ final Style docStyle = Document.get().getBody().getStyle();
+ if (hasProperty(docStyle, "transform")) {
+ if (hasProperty(docStyle, "transformStyle")) {
+ position = new Translate3DPosition();
+ } else {
+ position = new TranslatePosition();
+ }
+ } else if (hasProperty(docStyle, "webkitTransform")) {
+ position = new WebkitTranslate3DPosition();
+ } else {
+ position = new AbsolutePosition();
+ }
+
+ getLogger().info(
+ "Using " + position.getClass().getSimpleName()
+ + " for position");
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ private static native boolean hasProperty(Style style, String name)
+ /*-{
+ return style[name] !== undefined;
+ }-*/;
+
+ /**
+ * Check whether there are both columns and any row data (for either
+ * headers, body or footer).
+ *
+ * @return <code>true</code> iff header, body or footer has rows && there
+ * are columns
+ */
+ private boolean hasColumnAndRowData() {
+ return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
+ .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
+ }
+
+ /**
+ * Check whether there are any cells in the DOM.
+ *
+ * @return <code>true</code> iff header, body or footer has any child
+ * elements
+ */
+ private boolean hasSomethingInDom() {
+ return headElem.hasChildNodes() || bodyElem.hasChildNodes()
+ || footElem.hasChildNodes();
+ }
+
+ /**
+ * Returns the representation of this Escalator header.
+ *
+ * @return the header. Never <code>null</code>
+ */
+ public RowContainer getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the representation of this Escalator body.
+ *
+ * @return the body. Never <code>null</code>
+ */
+ public RowContainer getBody() {
+ return body;
+ }
+
+ /**
+ * Returns the representation of this Escalator footer.
+ *
+ * @return the footer. Never <code>null</code>
+ */
+ public RowContainer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Returns the configuration object for the columns in this Escalator.
+ *
+ * @return the configuration object for the columns in this Escalator. Never
+ * <code>null</code>
+ */
+ public ColumnConfiguration getColumnConfiguration() {
+ return columnConfiguration;
+ }
+
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
+ */
+ @Override
+ public void setWidth(final String width) {
+ super.setWidth(width);
+ recalculateElementSizes();
+ }
+
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
+ */
+ @Override
+ public void setHeight(final String height) {
+ super.setHeight(height);
+ recalculateElementSizes();
+ }
+
+ private void recalculateElementSizes() {
+ for (final RowContainerImpl rowContainer : rowContainers) {
+ rowContainer.recalculateSectionHeight();
+ }
+
+ /*
+ * TODO [[escalator]]: take scrollbar size into account only if there is
+ * something to scroll, and only for the dimension it applies to.
+ */
+ // recalculate required space for scroll underlay
+ tableWrapper.getStyle().setHeight(getElement().getOffsetHeight(),
+ Unit.PX);
+ tableWrapper.getStyle()
+ .setWidth(getElement().getOffsetWidth(), Unit.PX);
+ }
+
+ private static void recalculateRowWidth(Element tr) {
+ // TODO [[colwidth]]: adjust for variable column widths
+ tr.getStyle().setWidth(tr.getChildCount() * COLUMN_WIDTH_PX, Unit.PX);
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/PositionFunction.java b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
new file mode 100644
index 0000000000..24e119b6a4
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
@@ -0,0 +1,88 @@
+/*
+ * 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.Unit;
+import com.google.gwt.user.client.Element;
+
+/**
+ * A functional interface that can be used for positioning elements in the DOM.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+interface PositionFunction {
+ /**
+ * A position function using "transform: translate3d(x,y,z)" to position
+ * elements in the DOM.
+ */
+ public static class Translate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate3d(" + x + "px, " + y + "px, 0)");
+ }
+ }
+
+ /**
+ * A position function using "transform: translate(x,y)" to position
+ * elements in the DOM.
+ */
+ public static class TranslatePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate(" + x + "px," + y + "px)");
+ }
+ }
+
+ /**
+ * A position function using "-webkit-transform: translate3d(x,y,z)" to
+ * position elements in the DOM.
+ */
+ public static class WebkitTranslate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("webkitTransform",
+ "translate3d(" + x + "px," + y + "px,0");
+ }
+ }
+
+ /**
+ * A position function using "left: x" and "top: y" to position elements in
+ * the DOM.
+ */
+ public static class AbsolutePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setLeft(x, Unit.PX);
+ e.getStyle().setTop(y, Unit.PX);
+ }
+ }
+
+ /**
+ * Position an element in an (x,y) coordinate system in the DOM.
+ *
+ * @param e
+ * the element to position. Never <code>null</code>.
+ * @param x
+ * the x coordinate, in pixels
+ * @param y
+ * the y coordinate, in pixels
+ */
+ void set(Element e, double x, double y);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/RowContainer.java b/client/src/com/vaadin/client/ui/grid/RowContainer.java
new file mode 100644
index 0000000000..1a1a20073d
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowContainer.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+/**
+ * A representation of the rows in each of the sections (header, body and
+ * footer) in an {@link Escalator}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+public interface RowContainer {
+ /**
+ * Returns the current {@link CellRenderer} used to render cells.
+ *
+ * @return the current cell renderer
+ */
+ public CellRenderer getCellRenderer();
+
+ /**
+ * Sets the {@link CellRenderer} to use when displaying data in the
+ * escalator.
+ *
+ * @param cellRenderer
+ * the cell renderer to use to render cells. May not be
+ * <code>null</code>
+ * @throws IllegalArgumentException
+ * if {@code cellRenderer} is <code>null</code>
+ * @see CellRenderer#NULL_RENDERER
+ */
+ public void setCellRenderer(CellRenderer cellRenderer)
+ throws IllegalArgumentException;
+
+ /**
+ * Removes rows at a certain offset in the current row container.
+ *
+ * @param offset
+ * the index of the first row to be removed
+ * @param numberOfRows
+ * the number of rows to remove, starting from the offset
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[offset..(offset+numberOfRows)]</code> is not an
+ * existing row index
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void removeRows(int offset, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds rows at a certain offset in this row container.
+ * <p>
+ * The new rows will be inserted between the row at the offset, and the row
+ * before (an offset of 0 means that the rows are inserted at the
+ * beginning). Therefore, the rows currently at the offset and afterwards
+ * will be moved downwards.
+ * <p>
+ * The contents of the inserted rows will subsequently be queried from the
+ * cell renderer.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted rows will be rendered.
+ * If inserting new rows affects the contents of existing rows,
+ * {@link #refreshRows(int, int)} needs to be called for those rows
+ * separately.
+ *
+ * @param offset
+ * the index of the row before which new rows are inserted, or
+ * {@link #getRowCount()} to add rows at the end
+ * @param numberOfRows
+ * the number of rows to insert after the <code>offset</code>
+ * @see #setCellRenderer(CellRenderer)
+ * @throws IndexOutOfBoundsException
+ * if <code>offset</code> is not an integer in the range
+ * <code>[0..{@link #getRowCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void insertRows(int offset, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Refreshes a range of rows in the current row container.
+ * <p>
+ * The data for the refreshed rows are queried from the current cell
+ * renderer.
+ *
+ * @param offset
+ * the index of the first row that will be updated
+ * @param numberOfRows
+ * the number of rows to update, starting from the offset
+ * @see #setCellRenderer(CellRenderer)
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[offset..(offset+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void refreshRows(int offset, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Gets the number of rows in the current row container.
+ *
+ * @return the number of rows in the current row container
+ */
+ public int getRowCount();
+} \ No newline at end of file