View Javadoc
1   /*
2    * Copyright 2000-2018 Vaadin Ltd.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  
17  package com.vaadin.v7.client.ui;
18  
19  import info.magnolia.ui.vaadin.gwt.client.grid.VMagnoliaTable;
20  
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.LinkedList;
24  import java.util.List;
25  
26  import com.google.gwt.animation.client.Animation;
27  import com.google.gwt.core.client.Scheduler;
28  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
29  import com.google.gwt.dom.client.Document;
30  import com.google.gwt.dom.client.Element;
31  import com.google.gwt.dom.client.Style.Display;
32  import com.google.gwt.dom.client.Style.Unit;
33  import com.google.gwt.dom.client.Style.Visibility;
34  import com.google.gwt.dom.client.TableCellElement;
35  import com.google.gwt.event.dom.client.KeyCodes;
36  import com.google.gwt.user.client.DOM;
37  import com.google.gwt.user.client.Event;
38  import com.google.gwt.user.client.ui.Widget;
39  import com.vaadin.client.ComputedStyle;
40  import com.vaadin.client.UIDL;
41  import com.vaadin.client.WidgetUtil;
42  import com.vaadin.client.ui.Icon;
43  import com.vaadin.v7.client.ui.VTreeTablePatched.VTreeTableScrollBody.VTreeTableRow;
44  
45  public class VTreeTablePatched extends VMagnoliaTable {
46  
47      /** For internal use only. May be removed or replaced in the future. */
48      public static class PendingNavigationEvent {
49          public final int keycode;
50          public final boolean ctrl;
51          public final boolean shift;
52  
53          public PendingNavigationEvent(int keycode, boolean ctrl,
54                  boolean shift) {
55              this.keycode = keycode;
56              this.ctrl = ctrl;
57              this.shift = shift;
58          }
59  
60          @Override
61          public String toString() {
62              String string = "Keyboard event: " + keycode;
63              if (ctrl) {
64                  string += " + ctrl";
65              }
66              if (shift) {
67                  string += " + shift";
68              }
69              return string;
70          }
71      }
72  
73      /** For internal use only. May be removed or replaced in the future. */
74      public boolean collapseRequest;
75  
76      private boolean selectionPending;
77  
78      /** For internal use only. May be removed or replaced in the future. */
79      public int colIndexOfHierarchy;
80  
81      /** For internal use only. May be removed or replaced in the future. */
82      public String collapsedRowKey;
83  
84      /** For internal use only. May be removed or replaced in the future. */
85      public VTreeTableScrollBody scrollBody;
86  
87      /** For internal use only. May be removed or replaced in the future. */
88      public boolean animationsEnabled;
89  
90      /** For internal use only. May be removed or replaced in the future. */
91      public LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTablePatched.PendingNavigationEvent>();
92  
93      /** For internal use only. May be removed or replaced in the future. */
94      public boolean focusParentResponsePending;
95  
96      @Override
97      protected VScrollTableBody createScrollBody() {
98          scrollBody = new VTreeTableScrollBody();
99          return scrollBody;
100     }
101 
102     /*
103      * Overridden to allow animation of expands and collapses of nodes.
104      */
105     @Override
106     public void addAndRemoveRows(UIDL partialRowAdditions) {
107         if (partialRowAdditions == null) {
108             return;
109         }
110 
111         if (animationsEnabled) {
112             if (partialRowAdditions.hasAttribute("hide")) {
113                 scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
114                         partialRowAdditions.getIntAttribute("firstprowix"),
115                         partialRowAdditions.getIntAttribute("numprows"));
116             } else {
117                 scrollBody.insertRowsAnimated(partialRowAdditions,
118                         partialRowAdditions.getIntAttribute("firstprowix"),
119                         partialRowAdditions.getIntAttribute("numprows"));
120                 discardRowsOutsideCacheWindow();
121             }
122         } else {
123             super.addAndRemoveRows(partialRowAdditions);
124         }
125     }
126 
127     @Override
128     protected int getHierarchyColumnIndex() {
129         return colIndexOfHierarchy + (showRowHeaders ? 1 : 0);
130     }
131 
132     public class VTreeTableScrollBody extends VMagnoliaTable.MagnoliaTableBody {
133         private int indentWidth = -1;
134         private int maxIndent = 0;
135 
136         protected VTreeTableScrollBody() {
137             super();
138         }
139 
140         @Override
141         protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
142             if (uidl.hasAttribute("gen_html")) {
143                 // This is a generated row.
144                 return new VTreeTableGeneratedRow(uidl, aligns2);
145             }
146             return new VTreeTableRow(uidl, aligns2);
147         }
148 
149         public class VTreeTableRow
150                 extends VMagnoliaTable.MagnoliaTableBody.MagnoliaTableRow {
151 
152             protected boolean isTreeCellAdded = false;
153             protected com.google.gwt.dom.client.Element treeSpacer;
154             protected boolean open;
155             protected int depth;
156             protected boolean canHaveChildren;
157             protected Widget widgetInHierarchyColumn;
158 
159             public VTreeTableRow(UIDL uidl, char[] aligns2) {
160                 super(uidl, aligns2);
161                 // this fix causes #15118 and doesn't work for treetable anyway
162                 applyZeroWidthFix = false;
163             }
164 
165             @Override
166             public void addCell(UIDL rowUidl, String text, char align,
167                     String style, boolean textIsHTML, boolean isSorted,
168                     String description) {
169                 super.addCell(rowUidl, text, align, style, textIsHTML, isSorted,
170                         description);
171 
172                 addTreeSpacer(rowUidl);
173             }
174 
175             protected boolean addTreeSpacer(UIDL rowUidl) {
176                 if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
177                     Element container = (Element) getElement().getLastChild()
178                             .getFirstChild();
179 
180                     if (rowUidl.hasAttribute("icon")) {
181                         Icon icon = client
182                                 .getIcon(rowUidl.getStringAttribute("icon"));
183                         icon.setAlternateText("icon");
184                         container.insertFirst(icon.getElement());
185                     }
186 
187                     String classname = "v-treetable-treespacer";
188                     if (rowUidl.getBooleanAttribute("ca")) {
189                         canHaveChildren = true;
190                         open = rowUidl.getBooleanAttribute("open");
191                         classname += open ? " v-treetable-node-open"
192                                 : " v-treetable-node-closed";
193                     }
194 
195                     treeSpacer = Document.get().createSpanElement();
196 
197                     treeSpacer.setClassName(classname);
198                     container.insertFirst(treeSpacer);
199                     depth = rowUidl.hasAttribute("depth")
200                             ? rowUidl.getIntAttribute("depth")
201                             : 0;
202                     setIndent();
203                     isTreeCellAdded = true;
204                     return true;
205                 }
206                 return false;
207             }
208 
209             protected boolean cellShowsTreeHierarchy(int curColIndex) {
210                 if (isTreeCellAdded) {
211                     return false;
212                 }
213                 return curColIndex == getHierarchyColumnIndex();
214             }
215 
216             @Override
217             public void onBrowserEvent(Event event) {
218                 if (event.getEventTarget().cast() == treeSpacer
219                         && treeSpacer.getClassName().contains("node")) {
220                     if (event.getTypeInt() == Event.ONMOUSEUP) {
221                         sendToggleCollapsedUpdate(getKey());
222                     }
223                     return;
224                 }
225                 super.onBrowserEvent(event);
226             }
227 
228             @Override
229             public void addCell(UIDL rowUidl, Widget w, char align,
230                     String style, boolean isSorted, String description) {
231                 super.addCell(rowUidl, w, align, style, isSorted, description);
232                 if (addTreeSpacer(rowUidl)) {
233                     widgetInHierarchyColumn = w;
234                 }
235 
236             }
237 
238             protected void setIndent() {
239                 if (getIndentWidth() > 0) {
240                     treeSpacer.getParentElement().getStyle()
241                             .setPaddingLeft(getIndent(), Unit.PX);
242                     treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
243                     int colWidth = getColWidth(getHierarchyColumnIndex());
244                     if (colWidth > 0 && getIndent() > colWidth) {
245                         VTreeTablePatched.this.setColWidth(getHierarchyColumnIndex(),
246                                 getIndent(), false);
247                     }
248                 }
249             }
250 
251             @Override
252             protected void onAttach() {
253                 super.onAttach();
254                 if (getIndentWidth() < 0) {
255                     detectIndent(this);
256                     // If we detect indent here then the size of the hierarchy
257                     // column is still wrong as it has been set when the indent
258                     // was not known.
259                     int w = getCellWidthFromDom(getHierarchyColumnIndex());
260                     if (w >= 0) {
261                         setColWidth(getHierarchyColumnIndex(), w);
262                     }
263                 }
264             }
265 
266             private int getCellWidthFromDom(int cellIndex) {
267                 final Element cell = DOM.getChild(getElement(), cellIndex);
268                 String w = cell.getStyle().getProperty("width");
269                 if (w == null || "".equals(w) || !w.endsWith("px")) {
270                     return -1;
271                 } else {
272                     return Integer.parseInt(w.substring(0, w.length() - 2));
273                 }
274             }
275 
276             @Override
277             protected void setCellWidth(int cellIx, int width) {
278                 if (cellIx == getHierarchyColumnIndex()) {
279                     // take indentation padding into account if this is the
280                     // hierarchy column
281                     int indent = getIndent();
282                     if (indent != -1) {
283                         width = Math.max(width - indent, 0);
284                     }
285                 }
286                 super.setCellWidth(cellIx, width);
287             }
288 
289             protected int getIndent() {
290                 return (depth + 1) * getIndentWidth();
291             }
292         }
293 
294         protected class VTreeTableGeneratedRow extends VTreeTableRow {
295             private boolean spanColumns;
296             private boolean htmlContentAllowed;
297 
298             public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
299                 super(uidl, aligns);
300                 addStyleName("v-table-generated-row");
301             }
302 
303             public boolean isSpanColumns() {
304                 return spanColumns;
305             }
306 
307             @Override
308             protected void initCellWidths() {
309                 if (spanColumns) {
310                     setSpannedColumnWidthAfterDOMFullyInited();
311                 } else {
312                     super.initCellWidths();
313                 }
314             }
315 
316             private void setSpannedColumnWidthAfterDOMFullyInited() {
317                 // Defer setting width on spanned columns to make sure that
318                 // they are added to the DOM before trying to calculate
319                 // widths.
320                 Scheduler.get().scheduleDeferred(new ScheduledCommand() {
321 
322                     @Override
323                     public void execute() {
324                         if (showRowHeaders) {
325                             setCellWidth(0, tHead.getHeaderCell(0)
326                                     .getWidthWithIndent());
327                             calcAndSetSpanWidthOnCell(1);
328                         } else {
329                             calcAndSetSpanWidthOnCell(0);
330                         }
331                     }
332                 });
333             }
334 
335             @Override
336             protected boolean isRenderHtmlInCells() {
337                 return htmlContentAllowed;
338             }
339 
340             @Override
341             protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
342                     int visibleColumnIndex) {
343                 htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
344                 spanColumns = uidl.getBooleanAttribute("gen_span");
345 
346                 if (spanColumns) {
347                     int colCount = uidl.getChildCount();
348                     // add the first cell only
349                     for (final Object cell : uidl) {
350                         if (cell instanceof String) {
351                             addSpannedCell(uidl, cell.toString(), aligns[0], "",
352                                     htmlContentAllowed, false, null, colCount);
353                         } else {
354                             addSpannedCell(uidl, (Widget) cell, aligns[0], "",
355                                     false, colCount);
356                         }
357                         break;
358                     }
359                 } else {
360                     super.addCellsFromUIDL(uidl, aligns, col,
361                             visibleColumnIndex);
362                 }
363             }
364 
365             private void addSpannedCell(UIDL rowUidl, Widget w, char align,
366                     String style, boolean sorted, int colCount) {
367                 TableCellElement td = DOM.createTD().cast();
368                 td.setColSpan(colCount);
369                 initCellWithWidget(w, align, style, sorted, td);
370                 td.getStyle().setHeight(getRowHeight(), Unit.PX);
371                 if (addTreeSpacer(rowUidl)) {
372                     widgetInHierarchyColumn = w;
373                 }
374             }
375 
376             private void addSpannedCell(UIDL rowUidl, String text, char align,
377                     String style, boolean textIsHTML, boolean sorted,
378                     String description, int colCount) {
379                 // String only content is optimized by not using Label widget
380                 final TableCellElement td = DOM.createTD().cast();
381                 td.setColSpan(colCount);
382                 initCellWithText(text, align, style, textIsHTML, sorted,
383                         description, td);
384                 td.getStyle().setHeight(getRowHeight(), Unit.PX);
385                 addTreeSpacer(rowUidl);
386             }
387 
388             @Override
389             protected void setCellWidth(int cellIx, int width) {
390                 if (isSpanColumns()) {
391                     if (showRowHeaders) {
392                         if (cellIx == 0) {
393                             super.setCellWidth(0, width);
394                         } else {
395                             // We need to recalculate the spanning TDs width for
396                             // every cellIx in order to support column resizing.
397                             calcAndSetSpanWidthOnCell(1);
398                         }
399                     } else {
400                         // Same as above.
401                         calcAndSetSpanWidthOnCell(0);
402                     }
403                 } else {
404                     super.setCellWidth(cellIx, width);
405                 }
406             }
407 
408             private void calcAndSetSpanWidthOnCell(final int cellIx) {
409                 int spanWidth = 0;
410                 for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
411                         .getVisibleCellCount(); ix++) {
412                     spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
413                 }
414                 WidgetUtil.setWidthExcludingPaddingAndBorder(
415                         (Element) getElement().getChild(cellIx), spanWidth, 13,
416                         false);
417             }
418         }
419 
420         public int getIndentWidth() {
421             return indentWidth;
422         }
423 
424         @Override
425         protected int getMaxIndent() {
426             return maxIndent;
427         }
428 
429         @Override
430         protected void calculateMaxIndent() {
431             int maxIndent = 0;
432             for (Widget w : this) {
433                 VTreeTableRow next = (VTreeTableRow) w;
434                 maxIndent = Math.max(maxIndent, next.getIndent());
435             }
436             // MGNLUI-962 We don't want expanding the tree to have impact on column widths.
437             // this.maxIndent = maxIndent;
438         }
439 
440         private void detectIndent(VTreeTableRow vTreeTableRow) {
441             indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
442             if (indentWidth == 0) {
443                 indentWidth = -1;
444                 return;
445             }
446             for (Widget w : this) {
447                 ((VTreeTableRow) w).setIndent();
448             }
449             calculateMaxIndent();
450         }
451 
452         protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
453                 final int firstIndex, final int rows) {
454             List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
455             for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
456                 VScrollTableRow row = getRowByRowIndex(ix);
457                 if (row != null) {
458                     rowsToDelete.add(row);
459                 }
460             }
461             if (!rowsToDelete.isEmpty()) {
462                 // #8810 Only animate if there's something to animate
463                 RowCollapseAnimation anim = new RowCollapseAnimation(
464                         rowsToDelete) {
465                     @Override
466                     protected void onComplete() {
467                         super.onComplete();
468                         // Actually unlink the rows and update the cache after
469                         // the
470                         // animation is done.
471                         unlinkAndReindexRows(firstIndex, rows);
472                         discardRowsOutsideCacheWindow();
473                         ensureCacheFilled();
474                     }
475                 };
476                 anim.run(150);
477             }
478         }
479 
480         protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
481                 int firstIndex, int rows) {
482             List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
483                     firstIndex, rows);
484             if (!insertedRows.isEmpty()) {
485                 // Only animate if there's something to animate (#8810)
486                 RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
487                 anim.run(150);
488             }
489             scrollBody.calculateMaxIndent();
490             return insertedRows;
491         }
492 
493         /**
494          * Prepares the table for animation by copying the background colors of
495          * all TR elements to their respective TD elements if the TD element is
496          * transparent. This is needed, since if TDs have transparent
497          * backgrounds, the rows sliding behind them are visible.
498          */
499         private class AnimationPreparator {
500             private final int lastItemIx;
501 
502             public AnimationPreparator(int lastItemIx) {
503                 this.lastItemIx = lastItemIx;
504             }
505 
506             public void prepareTableForAnimation() {
507                 int ix = lastItemIx;
508                 VScrollTableRow row = null;
509                 while ((row = getRowByRowIndex(ix)) != null) {
510                     copyTRBackgroundsToTDs(row);
511                     --ix;
512                 }
513             }
514 
515             private void copyTRBackgroundsToTDs(VScrollTableRow row) {
516                 Element tr = row.getElement();
517                 ComputedStyle cs = new ComputedStyle(tr);
518                 String backgroundAttachment = cs
519                         .getProperty("backgroundAttachment");
520                 String backgroundClip = cs.getProperty("backgroundClip");
521                 String backgroundColor = cs.getProperty("backgroundColor");
522                 String backgroundImage = cs.getProperty("backgroundImage");
523                 String backgroundOrigin = cs.getProperty("backgroundOrigin");
524                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
525                     Element td = tr.getChild(ix).cast();
526                     if (!elementHasBackground(td)) {
527                         td.getStyle().setProperty("backgroundAttachment",
528                                 backgroundAttachment);
529                         td.getStyle().setProperty("backgroundClip",
530                                 backgroundClip);
531                         td.getStyle().setProperty("backgroundColor",
532                                 backgroundColor);
533                         td.getStyle().setProperty("backgroundImage",
534                                 backgroundImage);
535                         td.getStyle().setProperty("backgroundOrigin",
536                                 backgroundOrigin);
537                     }
538                 }
539             }
540 
541             private boolean elementHasBackground(Element element) {
542                 ComputedStyle cs = new ComputedStyle(element);
543                 String clr = cs.getProperty("backgroundColor");
544                 String img = cs.getProperty("backgroundImage");
545                 return !("rgba(0, 0, 0, 0)".equals(clr.trim())
546                         || "transparent".equals(clr.trim()) || img == null);
547             }
548 
549             public void restoreTableAfterAnimation() {
550                 int ix = lastItemIx;
551                 VScrollTableRow row = null;
552                 while ((row = getRowByRowIndex(ix)) != null) {
553                     restoreStyleForTDsInRow(row);
554 
555                     --ix;
556                 }
557             }
558 
559             private void restoreStyleForTDsInRow(VScrollTableRow row) {
560                 Element tr = row.getElement();
561                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
562                     Element td = tr.getChild(ix).cast();
563                     td.getStyle().clearProperty("backgroundAttachment");
564                     td.getStyle().clearProperty("backgroundClip");
565                     td.getStyle().clearProperty("backgroundColor");
566                     td.getStyle().clearProperty("backgroundImage");
567                     td.getStyle().clearProperty("backgroundOrigin");
568                 }
569             }
570         }
571 
572         /**
573          * Animates row expansion using the GWT animation framework.
574          *
575          * The idea is as follows:
576          *
577          * 1. Insert all rows normally
578          *
579          * 2. Insert a newly created DIV containing a new TABLE element below
580          * the DIV containing the actual scroll table body.
581          *
582          * 3. Clone the rows that were inserted in step 1 and attach the clones
583          * to the new TABLE element created in step 2.
584          *
585          * 4. The new DIV from step 2 is absolutely positioned so that the last
586          * inserted row is just behind the row that was expanded.
587          *
588          * 5. Hide the contents of the originally inserted rows by setting the
589          * DIV.v-table-cell-wrapper to display:none;.
590          *
591          * 6. Set the height of the originally inserted rows to 0.
592          *
593          * 7. The animation loop slides the DIV from step 2 downwards, while at
594          * the same pace growing the height of each of the inserted rows from 0
595          * to full height. The first inserted row grows from 0 to full and after
596          * this the second row grows from 0 to full, etc until all rows are full
597          * height.
598          *
599          * 8. Remove the DIV from step 2
600          *
601          * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
602          *
603          * 10. DONE
604          */
605         private class RowExpandAnimation extends Animation {
606 
607             private final List<VScrollTableRow> rows;
608             private Element cloneDiv;
609             private Element cloneTable;
610             private AnimationPreparator preparator;
611 
612             /**
613              * @param rows
614              *            List of rows to animate. Must not be empty.
615              */
616             public RowExpandAnimation(List<VScrollTableRow> rows) {
617                 this.rows = rows;
618                 buildAndInsertAnimatingDiv();
619                 preparator = new AnimationPreparator(
620                         rows.get(0).getIndex() - 1);
621                 preparator.prepareTableForAnimation();
622                 for (VScrollTableRow row : rows) {
623                     cloneAndAppendRow(row);
624                     row.addStyleName("v-table-row-animating");
625                     setCellWrapperDivsToDisplayNone(row);
626                     row.setHeight(getInitialHeight());
627                 }
628             }
629 
630             protected String getInitialHeight() {
631                 return "0px";
632             }
633 
634             private void cloneAndAppendRow(VScrollTableRow row) {
635                 Element clonedTR = null;
636                 clonedTR = row.getElement().cloneNode(true).cast();
637                 clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
638                 cloneTable.appendChild(clonedTR);
639             }
640 
641             protected double getBaseOffset() {
642                 return rows.get(0).getAbsoluteTop()
643                         - rows.get(0).getParent().getAbsoluteTop()
644                         - rows.size() * getRowHeight();
645             }
646 
647             private void buildAndInsertAnimatingDiv() {
648                 cloneDiv = DOM.createDiv();
649                 cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
650                 cloneTable = DOM.createTable();
651                 cloneTable.addClassName("v-treetable-animation-clone");
652                 cloneDiv.appendChild(cloneTable);
653                 insertAnimatingDiv();
654             }
655 
656             private void insertAnimatingDiv() {
657                 Element tableBody = getElement();
658                 Element tableBodyParent = tableBody.getParentElement();
659                 tableBodyParent.insertAfter(cloneDiv, tableBody);
660             }
661 
662             @Override
663             protected void onUpdate(double progress) {
664                 animateDiv(progress);
665                 animateRowHeights(progress);
666             }
667 
668             private void animateDiv(double progress) {
669                 double offset = calculateDivOffset(progress, getRowHeight());
670 
671                 cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
672             }
673 
674             private void animateRowHeights(double progress) {
675                 double rh = getRowHeight();
676                 double vlh = calculateHeightOfAllVisibleLines(progress, rh);
677                 int ix = 0;
678 
679                 while (ix < rows.size()) {
680                     double height = vlh < rh ? vlh : rh;
681                     rows.get(ix).setHeight(height + "px");
682                     vlh -= height;
683                     ix++;
684                 }
685             }
686 
687             protected double calculateHeightOfAllVisibleLines(double progress,
688                     double rh) {
689                 return rows.size() * rh * progress;
690             }
691 
692             protected double calculateDivOffset(double progress, double rh) {
693                 return progress * rows.size() * rh;
694             }
695 
696             @Override
697             protected void onComplete() {
698                 preparator.restoreTableAfterAnimation();
699                 for (VScrollTableRow row : rows) {
700                     resetCellWrapperDivsDisplayProperty(row);
701                     row.removeStyleName("v-table-row-animating");
702                 }
703                 Element tableBodyParent = getElement().getParentElement();
704                 tableBodyParent.removeChild(cloneDiv);
705             }
706 
707             private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) {
708                 Element tr = row.getElement();
709                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
710                     getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE);
711                 }
712             }
713 
714             private Element getWrapperDiv(Element tr, int tdIx) {
715                 Element td = tr.getChild(tdIx).cast();
716                 return td.getChild(0).cast();
717             }
718 
719             private void resetCellWrapperDivsDisplayProperty(
720                     VScrollTableRow row) {
721                 Element tr = row.getElement();
722                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
723                     getWrapperDiv(tr, ix).getStyle().clearProperty("display");
724                 }
725             }
726 
727         }
728 
729         /**
730          * This is the inverse of the RowExpandAnimation and is implemented by
731          * extending it and overriding the calculation of offsets and heights.
732          */
733         private class RowCollapseAnimation extends RowExpandAnimation {
734 
735             private final List<VScrollTableRow> rows;
736 
737             /**
738              * @param rows
739              *            List of rows to animate. Must not be empty.
740              */
741             public RowCollapseAnimation(List<VScrollTableRow> rows) {
742                 super(rows);
743                 this.rows = rows;
744             }
745 
746             @Override
747             protected String getInitialHeight() {
748                 return getRowHeight() + "px";
749             }
750 
751             @Override
752             protected double getBaseOffset() {
753                 return getRowHeight();
754             }
755 
756             @Override
757             protected double calculateHeightOfAllVisibleLines(double progress,
758                     double rh) {
759                 return rows.size() * rh * (1 - progress);
760             }
761 
762             @Override
763             protected double calculateDivOffset(double progress, double rh) {
764                 return -super.calculateDivOffset(progress, rh);
765             }
766         }
767     }
768 
769     /**
770      * Icons rendered into first actual column in TreeTable, not to row header
771      * cell.
772      */
773     @Override
774     protected String buildCaptionHtmlSnippet(UIDL uidl) {
775         if (uidl.getTag().equals("column")) {
776             return super.buildCaptionHtmlSnippet(uidl);
777         } else {
778             String s = uidl.getStringAttribute("caption");
779             return s;
780         }
781     }
782 
783     /** For internal use only. May be removed or replaced in the future. */
784     @Override
785     public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
786         if (collapseRequest || focusParentResponsePending) {
787             // Enqueue the event if there might be pending content changes from
788             // the server
789             if (pendingNavigationEvents.size() < 10) {
790                 // Only keep 10 keyboard events in the queue
791                 PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
792                         keycode, ctrl, shift);
793                 pendingNavigationEvents.add(pendingNavigationEvent);
794             }
795             return true;
796         }
797 
798         VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
799         if (focusedRow != null) {
800             if (focusedRow.canHaveChildren && ((keycode == KeyCodes.KEY_RIGHT
801                     && !focusedRow.open)
802                     || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
803                 if (!ctrl) {
804                     client.updateVariable(paintableId, "selectCollapsed", true,
805                             false);
806                 }
807                 sendSelectedRows(false);
808                 sendToggleCollapsedUpdate(focusedRow.getKey());
809                 return true;
810             } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
811                 // already expanded, move selection down if next is on a deeper
812                 // level (is-a-child)
813                 VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
814                         .getParent();
815                 Iterator<Widget> iterator = body.iterator();
816                 VTreeTableRow next = null;
817                 while (iterator.hasNext()) {
818                     next = (VTreeTableRow) iterator.next();
819                     if (next == focusedRow) {
820                         next = (VTreeTableRow) iterator.next();
821                         break;
822                     }
823                 }
824                 if (next != null) {
825                     if (next.depth > focusedRow.depth) {
826                         selectionPending = true;
827                         return super.handleNavigation(getNavigationDownKey(),
828                                 ctrl, shift);
829                     }
830                 } else {
831                     // Note, a minor change here for a bit false behavior if
832                     // cache rows is disabled + last visible row + no childs for
833                     // the node
834                     selectionPending = true;
835                     return super.handleNavigation(getNavigationDownKey(), ctrl,
836                             shift);
837                 }
838             } else if (keycode == KeyCodes.KEY_LEFT) {
839                 // already collapsed move selection up to parent node
840                 // do on the server side as the parent is not necessary
841                 // rendered on the client, could check if parent is visible if
842                 // a performance issue arises
843 
844                 client.updateVariable(paintableId, "focusParent",
845                         focusedRow.getKey(), true);
846 
847                 // Set flag that we should enqueue navigation events until we
848                 // get a response to this request
849                 focusParentResponsePending = true;
850 
851                 return true;
852             }
853         }
854         return super.handleNavigation(keycode, ctrl, shift);
855     }
856 
857     public void sendToggleCollapsedUpdate(String rowKey) {
858         collapsedRowKey = rowKey;
859         collapseRequest = true;
860         client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
861     }
862 
863     @Override
864     public void onBrowserEvent(Event event) {
865         super.onBrowserEvent(event);
866         if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
867             sendSelectedRows();
868         }
869     }
870 
871     @Override
872     protected void sendSelectedRows(boolean immediately) {
873         super.sendSelectedRows(immediately);
874         selectionPending = false;
875     }
876 
877     @Override
878     protected void reOrderColumn(String columnKey, int newIndex) {
879         super.reOrderColumn(columnKey, newIndex);
880         // current impl not intelligent enough to survive without visiting the
881         // server to redraw content
882         client.sendPendingVariableChanges();
883     }
884 
885     @Override
886     public void setStyleName(String style) {
887         super.setStyleName(style + " v-treetable");
888     }
889 
890     @Override
891     public void updateTotalRows(UIDL uidl) {
892         // Make sure that initializedAndAttached & al are not reset when the
893         // totalrows are updated on expand/collapse requests.
894         int newTotalRows = uidl.getIntAttribute("totalrows");
895         setTotalRows(newTotalRows);
896     }
897 
898     @Override
899     public void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
900         // Left out blank to avoid scrolling glitches described in https://jira.magnolia-cms.com/browse/MGNLUI-960.
901     }
902 }