View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.renderkit.renderer;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.Facets;
24  import org.apache.myfaces.tobago.component.LabelLayout;
25  import org.apache.myfaces.tobago.component.RendererTypes;
26  import org.apache.myfaces.tobago.component.Tags;
27  import org.apache.myfaces.tobago.context.Markup;
28  import org.apache.myfaces.tobago.event.PageActionEvent;
29  import org.apache.myfaces.tobago.event.SheetAction;
30  import org.apache.myfaces.tobago.event.SortActionEvent;
31  import org.apache.myfaces.tobago.internal.component.AbstractUIColumn;
32  import org.apache.myfaces.tobago.internal.component.AbstractUIColumnBase;
33  import org.apache.myfaces.tobago.internal.component.AbstractUIColumnNode;
34  import org.apache.myfaces.tobago.internal.component.AbstractUIColumnSelector;
35  import org.apache.myfaces.tobago.internal.component.AbstractUIData;
36  import org.apache.myfaces.tobago.internal.component.AbstractUILink;
37  import org.apache.myfaces.tobago.internal.component.AbstractUIOut;
38  import org.apache.myfaces.tobago.internal.component.AbstractUIReload;
39  import org.apache.myfaces.tobago.internal.component.AbstractUIRow;
40  import org.apache.myfaces.tobago.internal.component.AbstractUISheet;
41  import org.apache.myfaces.tobago.internal.component.AbstractUIStyle;
42  import org.apache.myfaces.tobago.internal.layout.Cell;
43  import org.apache.myfaces.tobago.internal.layout.Grid;
44  import org.apache.myfaces.tobago.internal.layout.OriginCell;
45  import org.apache.myfaces.tobago.internal.renderkit.CommandMap;
46  import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
47  import org.apache.myfaces.tobago.internal.util.JsonUtils;
48  import org.apache.myfaces.tobago.internal.util.RenderUtils;
49  import org.apache.myfaces.tobago.layout.ShowPosition;
50  import org.apache.myfaces.tobago.layout.TextAlign;
51  import org.apache.myfaces.tobago.layout.VerticalAlign;
52  import org.apache.myfaces.tobago.model.ExpandedState;
53  import org.apache.myfaces.tobago.model.Selectable;
54  import org.apache.myfaces.tobago.model.SheetState;
55  import org.apache.myfaces.tobago.model.TreePath;
56  import org.apache.myfaces.tobago.renderkit.RendererBase;
57  import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
58  import org.apache.myfaces.tobago.renderkit.css.CssItem;
59  import org.apache.myfaces.tobago.renderkit.css.Icons;
60  import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
61  import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
62  import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
63  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
64  import org.apache.myfaces.tobago.renderkit.html.HtmlButtonTypes;
65  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
66  import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
67  import org.apache.myfaces.tobago.util.AjaxUtils;
68  import org.apache.myfaces.tobago.util.ComponentUtils;
69  import org.apache.myfaces.tobago.util.ResourceUtils;
70  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
71  import org.slf4j.Logger;
72  import org.slf4j.LoggerFactory;
73  
74  import javax.el.ValueExpression;
75  import javax.faces.application.Application;
76  import javax.faces.component.NamingContainer;
77  import javax.faces.component.UIColumn;
78  import javax.faces.component.UIComponent;
79  import javax.faces.component.UIData;
80  import javax.faces.component.UINamingContainer;
81  import javax.faces.component.behavior.AjaxBehavior;
82  import javax.faces.component.behavior.ClientBehavior;
83  import javax.faces.component.behavior.ClientBehaviorHolder;
84  import javax.faces.context.FacesContext;
85  import java.io.IOException;
86  import java.lang.invoke.MethodHandles;
87  import java.text.MessageFormat;
88  import java.util.ArrayList;
89  import java.util.Collections;
90  import java.util.List;
91  import java.util.Locale;
92  import java.util.Map;
93  
94  public class SheetRenderer<T extends AbstractUISheet> extends RendererBase<T> {
95  
96    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
97  
98    private static final String SUFFIX_WIDTHS = ComponentUtils.SUB_SEPARATOR + "widths";
99    private static final String SUFFIX_COLUMN_RENDERED = ComponentUtils.SUB_SEPARATOR + "rendered";
100   private static final String SUFFIX_SCROLL_POSITION = ComponentUtils.SUB_SEPARATOR + "scrollPosition";
101   private static final String SUFFIX_SELECTED = ComponentUtils.SUB_SEPARATOR + "selected";
102   private static final String SUFFIX_LAZY = NamingContainer.SEPARATOR_CHAR + "pageActionlazy";
103   private static final String SUFFIX_PAGE_ACTION = "pageAction";
104 
105   @Override
106   public void decodeInternal(final FacesContext facesContext, final T component) {
107 
108     final List<AbstractUIColumnBase> columns = component.getAllColumns();
109     final String clientId = component.getClientId(facesContext);
110 
111     String key = clientId + SUFFIX_WIDTHS;
112     final Map<String, String> requestParameterMap = facesContext.getExternalContext().getRequestParameterMap();
113     final SheetState state = component.getState();
114     if (requestParameterMap.containsKey(key)) {
115       final String widths = requestParameterMap.get(key);
116       ensureColumnWidthsSize(state.getColumnWidths(), columns, JsonUtils.decodeIntegerArray(widths));
117     }
118 
119     key = clientId + SUFFIX_SELECTED;
120     if (requestParameterMap.containsKey(key)) {
121       final String selected = requestParameterMap.get(key);
122       if (LOG.isDebugEnabled()) {
123         LOG.debug("selected = " + selected);
124       }
125       List<Integer> selectedRows;
126       try {
127         selectedRows = JsonUtils.decodeIntegerArray(selected);
128       } catch (final NumberFormatException e) {
129         LOG.warn(selected, e);
130         selectedRows = Collections.emptyList();
131       }
132 
133       ComponentUtils.setAttribute(component, Attributes.selectedListString, selectedRows);
134     }
135 
136     final String value
137         = facesContext.getExternalContext().getRequestParameterMap().get(clientId + SUFFIX_SCROLL_POSITION);
138     if (value != null) {
139       state.getScrollPosition().update(value);
140     }
141     RenderUtils.decodedStateOfTreeData(facesContext, component);
142 
143     decodeSheetAction(facesContext, component);
144     decodeColumnAction(facesContext, columns);
145 /* this will be done by the javax.faces.component.UIData.processDecodes() because these are facets.
146     for (UIComponent facet : sheet.getFacets().values()) {
147       facet.decode(facesContext);
148     }
149 */
150   }
151 
152   private void decodeColumnAction(final FacesContext facesContext, final List<AbstractUIColumnBase> columns) {
153     for (final AbstractUIColumnBase column : columns) {
154       final boolean sortable = ComponentUtils.getBooleanAttribute(column, Attributes.sortable);
155       if (sortable) {
156         final String sourceId = facesContext.getExternalContext().getRequestParameterMap().get("javax.faces.source");
157         final String columnId = column.getClientId(facesContext);
158         final String sorterId = columnId + "_" + AbstractUISheet.SORTER_ID;
159 
160         if (sorterId.equals(sourceId)) {
161           final UIData data = (UIData) column.getParent();
162           data.queueEvent(new SortActionEvent(data, column));
163         }
164       }
165     }
166   }
167 
168 
169   private void decodeSheetAction(final FacesContext facesContext, final AbstractUISheet component) {
170     final String sourceId = facesContext.getExternalContext().getRequestParameterMap().get("javax.faces.source");
171 
172     final String clientId = component.getClientId(facesContext);
173     if (LOG.isDebugEnabled()) {
174       LOG.debug("sourceId = '{}'", sourceId);
175       LOG.debug("clientId = '{}'", clientId);
176     }
177 
178     final String sheetClientIdWithAction
179         = clientId + UINamingContainer.getSeparatorChar(facesContext) + SUFFIX_PAGE_ACTION;
180     if (sourceId != null && sourceId.startsWith(sheetClientIdWithAction)) {
181       String actionString = sourceId.substring(sheetClientIdWithAction.length());
182       int index = actionString.indexOf('-');
183       SheetAction action;
184       if (index != -1) {
185         action = SheetAction.valueOf(actionString.substring(0, index));
186       } else {
187         action = SheetAction.valueOf(actionString);
188       }
189       PageActionEvent event = null;
190       switch (action) {
191         case first:
192         case prev:
193         case next:
194         case last:
195           event = new PageActionEvent(component, action);
196           break;
197         case toPage:
198         case toRow:
199         case lazy:
200           event = new PageActionEvent(component, action);
201           final int target;
202           final String value;
203           if (index == -1) {
204             final Map<String, String> map = facesContext.getExternalContext().getRequestParameterMap();
205             value = map.get(sourceId);
206           } else {
207             value = actionString.substring(index + 1);
208           }
209           try {
210             target = Integer.parseInt(value);
211           } catch (final NumberFormatException e) {
212             LOG.error("Can't parse integer value for action " + action.name() + ": " + value);
213             break;
214           }
215           event.setValue(target);
216           break;
217         default:
218       }
219       if (event != null) {
220         component.queueEvent(event);
221       }
222     }
223   }
224 
225   @Override
226   public void encodeBeginInternal(final FacesContext facesContext, final T component) throws IOException {
227 
228     final String sheetId = component.getClientId(facesContext);
229     final Markup markup = component.getMarkup();
230     final TobagoResponseWriter writer = getResponseWriter(facesContext);
231     final AbstractUIReload reload = ComponentUtils.getReloadFacet(component);
232 
233     UIComponent header = component.getHeader();
234     if (header == null) {
235       header = ComponentUtils.createComponent(facesContext, Tags.panel.componentType(), null, "_header");
236       header.setTransient(true);
237       final List<AbstractUIColumnBase> columns = component.getAllColumns();
238       int i = 0;
239       for (final AbstractUIColumnBase column : columns) {
240         if (!(column instanceof AbstractUIRow)) {
241           final AbstractUIOut out = (AbstractUIOut) ComponentUtils.createComponent(
242               facesContext, Tags.out.componentType(), RendererTypes.Out, "_col" + i);
243 //        out.setValue(column.getLabel());
244           out.setTransient(true);
245           ValueExpression valueExpression = column.getValueExpression(Attributes.label.getName());
246           if (valueExpression != null) {
247             out.setValueExpression(Attributes.value.getName(), valueExpression);
248           } else {
249             out.setValue(ComponentUtils.getAttribute(column, Attributes.label));
250           }
251           valueExpression = column.getValueExpression(Attributes.rendered.getName());
252           if (valueExpression != null) {
253             out.setValueExpression(Attributes.rendered.getName(), valueExpression);
254           } else {
255             out.setRendered(ComponentUtils.getBooleanAttribute(column, Attributes.rendered));
256           }
257           out.setLabelLayout(LabelLayout.skip);
258           header.getChildren().add(out);
259         }
260         i++;
261       }
262       component.setHeader(header);
263     }
264     component.init(facesContext);
265 
266     // Outer sheet div
267     writer.startElement(HtmlElements.TOBAGO_SHEET);
268     writer.writeIdAttribute(sheetId);
269     HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
270     writer.writeClassAttribute(
271         component.getCustomClass(),
272         TobagoClass.SHEET.createMarkup(markup),
273         markup != null && markup.contains(Markup.SPREAD) ? TobagoClass.SPREAD : null);
274     if (reload != null && reload.isRendered()) {
275       writer.writeAttribute(DataAttributes.RELOAD, reload.getFrequency());
276     }
277     writer.writeAttribute(DataAttributes.SELECTION_MODE, component.getSelectable().name(), false);
278     writer.writeAttribute(DataAttributes.FIRST, Integer.toString(component.getFirst()), false);
279     writer.writeAttribute(CustomAttributes.ROWS, component.getRows());
280     writer.writeAttribute(CustomAttributes.ROW_COUNT, Integer.toString(component.getRowCount()), false);
281     writer.writeAttribute(CustomAttributes.LAZY, component.isLazy());
282     writer.writeAttribute(CustomAttributes.LAZY_UPDATE, component.isLazy() && AjaxUtils.isAjaxRequest(facesContext));
283 
284     final boolean autoLayout = component.isAutoLayout();
285     if (!autoLayout) {
286       writer.writeAttribute(DataAttributes.LAYOUT, JsonUtils.encode(component.getColumnLayout(), "columns"), true);
287     }
288 
289     encodeBehavior(writer, facesContext, component);
290   }
291 
292   @Override
293   public void encodeChildrenInternal(final FacesContext facesContext, final T component) throws IOException {
294     for (final UIComponent child : component.getChildren()) {
295       if (child instanceof AbstractUIStyle) {
296         child.encodeAll(facesContext);
297       }
298     }
299   }
300 
301   @Override
302   public void encodeEndInternal(final FacesContext facesContext, final T component) throws IOException {
303 
304     final TobagoResponseWriter writer = getResponseWriter(facesContext);
305 
306     final String sheetId = component.getClientId(facesContext);
307     final Selectable selectable = component.getSelectable();
308     final Application application = facesContext.getApplication();
309     final SheetState state = component.getSheetState(facesContext);
310     final List<Integer> columnWidths = component.getState().getColumnWidths();
311     final boolean definedColumnWidths = component.getState().isDefinedColumnWidths();
312     final List<Integer> selectedRows = getSelectedRows(component, state);
313     final List<AbstractUIColumnBase> columns = component.getAllColumns();
314     final boolean autoLayout = component.isAutoLayout();
315 
316     ensureColumnWidthsSize(columnWidths, columns, Collections.emptyList());
317 
318     if (!autoLayout) {
319       encodeHiddenInput(writer,
320           JsonUtils.encode(definedColumnWidths ? columnWidths : Collections.emptyList()),
321           sheetId + SUFFIX_WIDTHS);
322 
323       final ArrayList<Boolean> encodedRendered = new ArrayList<>();
324       for (final AbstractUIColumnBase column : columns) {
325         if (!(column instanceof AbstractUIRow)) {
326           encodedRendered.add(column.isRendered());
327         }
328       }
329 
330       encodeHiddenInput(writer,
331           JsonUtils.encode(encodedRendered.toArray(new Boolean[0])),
332           sheetId + SUFFIX_COLUMN_RENDERED);
333     }
334 
335     encodeHiddenInput(writer,
336         component.getState().getScrollPosition().encode(),
337         component.getClientId(facesContext) + SUFFIX_SCROLL_POSITION);
338 
339     if (selectable != Selectable.none) {
340       encodeHiddenInput(writer,
341           JsonUtils.encode(selectedRows),
342           sheetId + SUFFIX_SELECTED);
343     }
344 
345     if (component.isLazy()) {
346       encodeHiddenInput(writer, null, sheetId + SUFFIX_LAZY);
347     }
348 
349     final List<Integer> expandedValue = component.isTreeModel() ? new ArrayList<>() : null;
350 
351     encodeTableBody(
352         facesContext, component, writer, sheetId, selectable, columnWidths, selectedRows, columns, autoLayout,
353         expandedValue);
354 
355     if (component.isPagingVisible()) {
356       writer.startElement(HtmlElements.FOOTER);
357       writer.writeClassAttribute(TobagoClass.SHEET__FOOTER);
358 
359       // show row range
360       final Markup showRowRange = markupForLeftCenterRight(component.getShowRowRange());
361       if (showRowRange != Markup.NULL) {
362         final AbstractUILink command
363             = ensurePagingCommand(facesContext, component, Facets.pagerRow.name(), SheetAction.toRow.name(), false);
364         final String pagerCommandId = command.getClientId(facesContext);
365 
366         writer.startElement(HtmlElements.UL);
367         writer.writeClassAttribute(TobagoClass.SHEET__PAGING, TobagoClass.SHEET__PAGING.createMarkup(showRowRange),
368             BootstrapClass.PAGINATION);
369         writer.startElement(HtmlElements.LI);
370         writer.writeClassAttribute(BootstrapClass.PAGE_ITEM);
371         writer.writeAttribute(HtmlAttributes.TITLE,
372             ResourceUtils.getString(facesContext, "sheet.setRow"), true);
373         writer.startElement(HtmlElements.SPAN);
374         writer.writeClassAttribute(TobagoClass.SHEET__PAGING_TEXT, BootstrapClass.PAGE_LINK);
375         if (component.getRowCount() != 0) {
376           final Locale locale = facesContext.getViewRoot().getLocale();
377           final int first = component.getFirst() + 1;
378           final int last1 = component.hasRowCount()
379               ? component.getLastRowIndexOfCurrentPage()
380               : -1;
381           final boolean unknown = !component.hasRowCount();
382           final String key; // plural
383           if (unknown) {
384             key = first == last1 ? "sheet.rowX" : "sheet.rowXtoY";
385           } else {
386             key = first == last1 ? "sheet.rowXofZ" : "sheet.rowXtoYofZ";
387           }
388           final String inputMarker = "{#}";
389           final Object[] args = {inputMarker, last1 == -1 ? "?" : last1, unknown ? "" : component.getRowCount()};
390           final MessageFormat detail = new MessageFormat(ResourceUtils.getString(facesContext, key), locale);
391           final String formatted = detail.format(args);
392           final int pos = formatted.indexOf(inputMarker);
393           if (pos >= 0) {
394             writer.writeText(formatted.substring(0, pos));
395             writer.startElement(HtmlElements.SPAN);
396             writer.writeClassAttribute(TobagoClass.SHEET__PAGING_OUTPUT);
397             writer.writeText(Integer.toString(first));
398             writer.endElement(HtmlElements.SPAN);
399             writer.startElement(HtmlElements.INPUT);
400             writer.writeIdAttribute(pagerCommandId);
401             writer.writeNameAttribute(pagerCommandId);
402             writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.TEXT);
403             writer.writeClassAttribute(TobagoClass.SHEET__PAGING_INPUT);
404             writer.writeAttribute(HtmlAttributes.VALUE, first);
405             if (!unknown) {
406               writer.writeAttribute(HtmlAttributes.MAXLENGTH, Integer.toString(component.getRowCount()).length());
407             }
408             writer.endElement(HtmlElements.INPUT);
409             writer.writeText(formatted.substring(pos + inputMarker.length()));
410           } else {
411             writer.writeText(formatted);
412           }
413         }
414         ComponentUtils.removeFacet(component, Facets.pagerRow);
415         writer.endElement(HtmlElements.SPAN);
416         writer.endElement(HtmlElements.LI);
417         writer.endElement(HtmlElements.UL);
418       }
419 
420       // show direct links
421       final Markup showDirectLinks = markupForLeftCenterRight(component.getShowDirectLinks());
422       if (showDirectLinks != Markup.NULL) {
423         writer.startElement(HtmlElements.UL);
424         writer.writeClassAttribute(TobagoClass.SHEET__PAGING, TobagoClass.SHEET__PAGING.createMarkup(showDirectLinks),
425             BootstrapClass.PAGINATION);
426         if (component.isShowDirectLinksArrows()) {
427           final boolean disabled = component.isAtBeginning();
428           encodeLink(
429               facesContext, component, application, disabled, SheetAction.first, null, Icons.STEP_BACKWARD, null);
430           encodeLink(facesContext, component, application, disabled, SheetAction.prev, null, Icons.BACKWARD, null);
431         }
432         encodeDirectPagingLinks(facesContext, application, component);
433         if (component.isShowDirectLinksArrows()) {
434           final boolean disabled = component.isAtEnd();
435           encodeLink(facesContext, component, application, disabled, SheetAction.next, null, Icons.FORWARD, null);
436           encodeLink(facesContext, component, application, disabled || !component.hasRowCount(), SheetAction.last, null,
437               Icons.STEP_FORWARD, null);
438         }
439         writer.endElement(HtmlElements.UL);
440       }
441 
442       // show page range
443       final Markup showPageRange = markupForLeftCenterRight(component.getShowPageRange());
444       if (showPageRange != Markup.NULL) {
445         final AbstractUILink command
446             = ensurePagingCommand(facesContext, component, Facets.pagerPage.name(), SheetAction.toPage.name(), false);
447         final String pagerCommandId = command.getClientId(facesContext);
448 
449         writer.startElement(HtmlElements.UL);
450         writer.writeClassAttribute(TobagoClass.SHEET__PAGING, TobagoClass.SHEET__PAGING.createMarkup(showPageRange),
451             BootstrapClass.PAGINATION);
452         if (component.isShowPageRangeArrows()) {
453           final boolean disabled = component.isAtBeginning();
454           encodeLink(
455               facesContext, component, application, disabled, SheetAction.first, null, Icons.STEP_BACKWARD, null);
456           encodeLink(facesContext, component, application, disabled, SheetAction.prev, null, Icons.BACKWARD, null);
457         }
458         writer.startElement(HtmlElements.LI);
459         writer.writeClassAttribute(BootstrapClass.PAGE_ITEM);
460         writer.startElement(HtmlElements.SPAN);
461         writer.writeClassAttribute(TobagoClass.SHEET__PAGING_TEXT, BootstrapClass.PAGE_LINK);
462         writer.writeAttribute(HtmlAttributes.TITLE,
463             ResourceUtils.getString(facesContext, "sheet.setPage"), true);
464         if (component.getRowCount() != 0) {
465           final Locale locale = facesContext.getViewRoot().getLocale();
466           final int first = component.getCurrentPage() + 1;
467           final boolean unknown = !component.hasRowCount();
468           final int pages = unknown ? -1 : component.getPages();
469           final String key;
470           if (unknown) {
471             key = first == pages ? "sheet.pageX" : "sheet.pageXtoY";
472           } else {
473             key = first == pages ? "sheet.pageXofZ" : "sheet.pageXtoYofZ";
474           }
475           final String inputMarker = "{#}";
476           final Object[] args = {inputMarker, pages == -1 ? "?" : pages};
477           final MessageFormat detail = new MessageFormat(ResourceUtils.getString(facesContext, key), locale);
478           final String formatted = detail.format(args);
479           final int pos = formatted.indexOf(inputMarker);
480           if (pos >= 0) {
481             writer.writeText(formatted.substring(0, pos));
482             writer.startElement(HtmlElements.SPAN);
483             writer.writeClassAttribute(TobagoClass.SHEET__PAGING_OUTPUT);
484             writer.writeText(Integer.toString(first));
485             writer.endElement(HtmlElements.SPAN);
486             writer.startElement(HtmlElements.INPUT);
487             writer.writeIdAttribute(pagerCommandId);
488             writer.writeNameAttribute(pagerCommandId);
489             writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.TEXT);
490             writer.writeClassAttribute(TobagoClass.SHEET__PAGING_INPUT);
491             writer.writeAttribute(HtmlAttributes.VALUE, first);
492             if (!unknown) {
493               writer.writeAttribute(HtmlAttributes.MAXLENGTH, Integer.toString(pages).length());
494             }
495             writer.endElement(HtmlElements.INPUT);
496             writer.writeText(formatted.substring(pos + inputMarker.length()));
497           } else {
498             writer.writeText(formatted);
499           }
500         }
501         ComponentUtils.removeFacet(component, Facets.pagerPage);
502         writer.endElement(HtmlElements.SPAN);
503         writer.endElement(HtmlElements.LI);
504         if (component.isShowPageRangeArrows()) {
505           final boolean disabled = component.isAtEnd();
506           encodeLink(facesContext, component, application, disabled, SheetAction.next, null, Icons.FORWARD, null);
507           encodeLink(facesContext, component, application, disabled || !component.hasRowCount(), SheetAction.last, null,
508               Icons.STEP_FORWARD, null);
509         }
510         writer.endElement(HtmlElements.UL);
511       }
512 
513       writer.endElement(HtmlElements.FOOTER);
514     }
515 
516     if (component.isTreeModel()) {
517       writer.startElement(HtmlElements.INPUT);
518       writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
519       final String expandedId = sheetId + ComponentUtils.SUB_SEPARATOR + AbstractUIData.SUFFIX_EXPANDED;
520       writer.writeNameAttribute(expandedId);
521       writer.writeIdAttribute(expandedId);
522       writer.writeClassAttribute(TobagoClass.SHEET__EXPANDED);
523       writer.writeAttribute(HtmlAttributes.VALUE, JsonUtils.encode(expandedValue), false);
524       writer.endElement(HtmlElements.INPUT);
525     }
526 
527     writer.endElement(HtmlElements.TOBAGO_SHEET);
528     UIComponent header = component.getHeader();
529     if (header.isTransient()) {
530       component.getFacets().remove("header");
531     }
532   }
533 
534   private void encodeTableBody(
535       final FacesContext facesContext, final AbstractUISheet sheet, final TobagoResponseWriter writer,
536       final String sheetId,
537       final Selectable selectable, final List<Integer> columnWidths, final List<Integer> selectedRows,
538       final List<AbstractUIColumnBase> columns, final boolean autoLayout, final List<Integer> expandedValue)
539       throws IOException {
540 
541     final boolean showHeader = sheet.isShowHeader();
542     final Markup sheetMarkup = sheet.getMarkup() != null ? sheet.getMarkup() : Markup.NULL;
543     final ExpandedState expandedState = sheet.isTreeModel() ? sheet.getExpandedState() : null;
544 
545     if (showHeader && !autoLayout) {
546       // if no autoLayout, we render the header in a separate table.
547 
548       writer.startElement(HtmlElements.HEADER);
549       writer.writeClassAttribute(TobagoClass.SHEET__HEADER);
550       writer.startElement(HtmlElements.TABLE);
551       writer.writeAttribute(HtmlAttributes.CELLSPACING, "0", false);
552       writer.writeAttribute(HtmlAttributes.CELLPADDING, "0", false);
553       writer.writeAttribute(HtmlAttributes.SUMMARY, "", false);
554       writer.writeClassAttribute(
555           BootstrapClass.TABLE,
556           TobagoClass.SHEET__HEADER_TABLE,
557           sheetMarkup.contains(Markup.DARK) ? BootstrapClass.TABLE_DARK : null,
558           sheetMarkup.contains(Markup.BORDERED) ? BootstrapClass.TABLE_BORDERED : null,
559           sheetMarkup.contains(Markup.SMALL) ? BootstrapClass.TABLE_SM : null,
560           TobagoClass.TABLE_LAYOUT__FIXED);
561 
562       writeColgroup(writer, columnWidths, columns, true);
563 
564       writer.startElement(HtmlElements.TBODY);
565       encodeHeaderRows(facesContext, sheet, writer, columns);
566       writer.endElement(HtmlElements.TBODY);
567       writer.endElement(HtmlElements.TABLE);
568       writer.endElement(HtmlElements.HEADER);
569     }
570 
571     writer.startElement(HtmlElements.DIV);
572     writer.writeIdAttribute(sheetId + ComponentUtils.SUB_SEPARATOR + "data_div");
573     writer.writeClassAttribute(TobagoClass.SHEET__BODY);
574 
575     writer.startElement(HtmlElements.TABLE);
576     writer.writeAttribute(HtmlAttributes.CELLSPACING, "0", false);
577     writer.writeAttribute(HtmlAttributes.CELLPADDING, "0", false);
578     writer.writeAttribute(HtmlAttributes.SUMMARY, "", false);
579     writer.writeClassAttribute(
580         BootstrapClass.TABLE,
581         TobagoClass.SHEET__BODY_TABLE,
582         sheetMarkup.contains(Markup.DARK) ? BootstrapClass.TABLE_DARK : null,
583         sheetMarkup.contains(Markup.STRIPED) ? BootstrapClass.TABLE_STRIPED : null,
584         sheetMarkup.contains(Markup.BORDERED) ? BootstrapClass.TABLE_BORDERED : null,
585         sheetMarkup.contains(Markup.HOVER) ? BootstrapClass.TABLE_HOVER : null,
586         sheetMarkup.contains(Markup.SMALL) ? BootstrapClass.TABLE_SM : null,
587         !autoLayout ? TobagoClass.TABLE_LAYOUT__FIXED : null);
588 
589     if (showHeader && autoLayout) {
590       writer.startElement(HtmlElements.THEAD);
591       encodeHeaderRows(facesContext, sheet, writer, columns);
592       writer.endElement(HtmlElements.THEAD);
593     }
594 
595     if (!autoLayout) {
596       writeColgroup(writer, columnWidths, columns, false);
597     }
598 
599     // Print the Content
600 
601     if (LOG.isDebugEnabled()) {
602       LOG.debug("first = " + sheet.getFirst() + "   rows = " + sheet.getRows());
603     }
604 
605     writer.startElement(HtmlElements.TBODY);
606 
607     final String var = sheet.getVar();
608 
609     boolean emptySheet = true;
610     // rows = 0 means: show all
611     final int last = sheet.isRowsUnlimited() ? Integer.MAX_VALUE : sheet.getFirst() + sheet.getRows();
612 
613     for (int rowIndex = sheet.getFirst(); rowIndex < last; rowIndex++) {
614       sheet.setRowIndex(rowIndex);
615       if (!sheet.isRowAvailable()) {
616         break;
617       }
618 
619       final Object rowRendered = sheet.getAttributes().get("rowRendered");
620       if (rowRendered instanceof Boolean && !((Boolean) rowRendered)) {
621         continue;
622       }
623 
624       emptySheet = false;
625 
626       if (LOG.isDebugEnabled()) {
627         LOG.debug("var       " + var);
628         LOG.debug("list      " + sheet.getValue());
629       }
630 
631       if (sheet.isTreeModel()) {
632         final TreePath path = sheet.getPath();
633         if (sheet.isFolder() && expandedState.isExpanded(path)) {
634           expandedValue.add(rowIndex);
635         }
636       }
637 
638       writer.startElement(HtmlElements.TR);
639       writer.writeAttribute(CustomAttributes.ROW_INDEX, rowIndex);
640       final boolean selected = selectedRows.contains(rowIndex);
641       final String[] rowMarkups = (String[]) sheet.getAttributes().get("rowMarkup");
642       Markup rowMarkup = Markup.NULL;
643       if (selected) {
644         rowMarkup = rowMarkup.add(Markup.SELECTED);
645       }
646       if (rowMarkups != null) {
647         rowMarkup = rowMarkup.add(Markup.valueOf(rowMarkups));
648       }
649       final String parentId = sheet.getRowParentClientId();
650       if (parentId != null) {
651         // TODO: replace with
652         // todo writer.writeIdAttribute(parentId + SUB_SEPARATOR + AbstractUITree.SUFFIX_PARENT);
653         // todo like in TreeListboxRenderer
654         writer.writeAttribute(DataAttributes.TREE_PARENT, parentId, false);
655       }
656 
657       AbstractUIRow row = null;
658       for (final UIColumn column : columns) {
659         if (column.isRendered()) {
660           if (column instanceof AbstractUIRow) {
661             row = (AbstractUIRow) column;
662             // todo: Markup.CLICKABLE ???
663           }
664         }
665       }
666       // the row client id depends from the existence of an UIRow component! TBD: is this good?
667       writer.writeIdAttribute(row != null ? row.getClientId(facesContext): sheet.getRowClientId());
668       writer.writeClassAttribute(
669           TobagoClass.SHEET__ROW,
670           TobagoClass.SHEET__ROW.createMarkup(rowMarkup),
671           selected ? BootstrapClass.TABLE_INFO : null,
672           row != null ? row.getCustomClass() : null,
673           sheet.isRowVisible() ? null : BootstrapClass.D_NONE);
674 
675       for (final AbstractUIColumnBase column : columns) {
676         if (column.isRendered()) {
677           if (column instanceof AbstractUIColumn || column instanceof AbstractUIColumnSelector
678               || column instanceof AbstractUIColumnNode) {
679             writer.startElement(HtmlElements.TD);
680             Markup markup = column.getMarkup();
681             if (markup == null) {
682               markup = Markup.NULL;
683             }
684             if (column instanceof AbstractUIColumn) {
685               final AbstractUIColumn normalColumn = (AbstractUIColumn) column;
686               markup = markup.add(getMarkupForAlign(normalColumn));
687               markup = markup.add(getMarkupForVerticalAlign(normalColumn));
688             }
689             writer.writeClassAttribute(
690                 TobagoClass.SHEET__CELL,
691                 TobagoClass.SHEET__CELL.createMarkup(markup),
692                 column.getCustomClass());
693 
694             if (column instanceof AbstractUIColumnSelector) {
695               final AbstractUIColumnSelector selector = (AbstractUIColumnSelector) column;
696               writer.startElement(HtmlElements.INPUT);
697               writer.writeNameAttribute(sheetId + "_data_row_selector_" + rowIndex);
698               if (selectable.isSingle()) {
699                 writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.RADIO);
700               } else {
701                 writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.CHECKBOX);
702               }
703               writer.writeAttribute(HtmlAttributes.CHECKED, selected);
704               writer.writeAttribute(HtmlAttributes.DISABLED, selector.isDisabled());
705               writer.writeClassAttribute(
706                   BootstrapClass.FORM_CHECK_INLINE,
707                   TobagoClass.SHEET__COLUMN_SELECTOR);
708               writer.endElement(HtmlElements.INPUT);
709             } else /*if (normalColumn instanceof AbstractUIColumnNode)*/ {
710               column.encodeAll(facesContext);
711             } /*else {
712               final List<UIComponent> children = sheet.getRenderedChildrenOf(normalColumn);
713               for (final UIComponent grandKid : children) {
714                 grandKid.encodeAll(facesContext);
715               }
716             }*/
717 
718             writer.endElement(HtmlElements.TD);
719           }
720         }
721       }
722 
723       writer.startElement(HtmlElements.TD);
724       writer.writeClassAttribute(TobagoClass.SHEET__CELL, TobagoClass.SHEET__CELL.createMarkup(Markup.FILLER));
725       writer.startElement(HtmlElements.DIV);
726       writer.endElement(HtmlElements.DIV);
727       encodeBehavior(writer, facesContext, row);
728       writer.endElement(HtmlElements.TD);
729 
730       writer.endElement(HtmlElements.TR);
731     }
732 
733     sheet.setRowIndex(-1);
734 
735     if (emptySheet && showHeader) {
736       writer.startElement(HtmlElements.TR);
737       int countColumns = 0;
738       for (final UIColumn column : columns) {
739         if (!(column instanceof AbstractUIRow)) {
740           countColumns++;
741         }
742       }
743       writer.startElement(HtmlElements.TD);
744       writer.writeAttribute(HtmlAttributes.COLSPAN, countColumns);
745       writer.startElement(HtmlElements.DIV);
746       writer.writeClassAttribute(BootstrapClass.TEXT_CENTER);
747       writer.writeText(ResourceUtils.getString(facesContext, "sheet.empty"));
748       writer.endElement(HtmlElements.DIV);
749       writer.endElement(HtmlElements.TD);
750       if (!autoLayout) {
751         writer.startElement(HtmlElements.TD);
752         writer.writeClassAttribute(TobagoClass.SHEET__CELL, TobagoClass.SHEET__CELL.createMarkup(Markup.FILLER));
753 //      writer.write("&nbsp;");
754         writer.startElement(HtmlElements.DIV);
755         writer.endElement(HtmlElements.DIV);
756         writer.endElement(HtmlElements.TD);
757       }
758       writer.endElement(HtmlElements.TR);
759     }
760 
761     writer.endElement(HtmlElements.TBODY);
762 
763     writer.endElement(HtmlElements.TABLE);
764     writer.endElement(HtmlElements.DIV);
765 
766 // END RENDER BODY CONTENT
767   }
768 
769   private void encodeHiddenInput(final TobagoResponseWriter writer, final String value, final String idWithSuffix)
770       throws IOException {
771     writer.startElement(HtmlElements.INPUT);
772     writer.writeIdAttribute(idWithSuffix);
773     writer.writeNameAttribute(idWithSuffix);
774     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
775     writer.writeAttribute(HtmlAttributes.VALUE, value, false);
776     writer.endElement(HtmlElements.INPUT);
777   }
778 
779   private void ensureColumnWidthsSize(
780       final List<Integer> columnWidths, final List<AbstractUIColumnBase> columns, final List<Integer> samples) {
781     // we have to fill the non rendered positions with some values.
782     // on client site, we don't know nothing about the non-rendered columns.
783     int i = 0;
784     int j = 0;
785     for (final AbstractUIColumnBase column : columns) {
786       if (!(column instanceof AbstractUIRow)) {
787         final Integer newValue;
788         if (j < samples.size()) {
789           newValue = samples.get(j);
790           j++;
791         } else {
792           newValue = null;
793         }
794         if (columnWidths.size() > i) {
795           if (newValue != null) {
796             columnWidths.set(i, newValue);
797           }
798         } else {
799           columnWidths.add(newValue != null ? newValue : -1); // -1 means unknown or undefined
800         }
801         i++;
802       }
803     }
804   }
805 
806   private Markup getMarkupForAlign(final UIColumn column) {
807     final String textAlign = ComponentUtils.getStringAttribute(column, Attributes.align);
808     if (textAlign != null) {
809       switch (TextAlign.valueOf(textAlign)) {
810         case right:
811           return Markup.RIGHT;
812         case center:
813           return Markup.CENTER;
814         case justify:
815           return Markup.JUSTIFY;
816         default:
817           // nothing to do
818       }
819     }
820     return null;
821   }
822 
823   private Markup getMarkupForVerticalAlign(final AbstractUIColumn column) {
824     final VerticalAlign verticalAlign = column.getVerticalAlign();
825     if (verticalAlign != null) {
826       switch (verticalAlign) {
827         case bottom:
828           return Markup.BOTTOM;
829         case middle:
830           return Markup.MIDDLE;
831         default:
832           // nothing to do
833       }
834     }
835     return null;
836   }
837 
838   private void encodeHeaderRows(
839       final FacesContext facesContext, final AbstractUISheet sheet, final TobagoResponseWriter writer,
840       final List<AbstractUIColumnBase> columns)
841       throws IOException {
842 
843     final Selectable selectable = sheet.getSelectable();
844     final Grid grid = sheet.getHeaderGrid();
845     final boolean autoLayout = sheet.isAutoLayout();
846     final boolean multiHeader = grid.getRowCount() > 1;
847     int offset = 0;
848 
849     for (int i = 0; i < grid.getRowCount(); i++) {
850       writer.startElement(HtmlElements.TR);
851       final AbstractUIRow row = ComponentUtils.findChild(sheet, AbstractUIRow.class);
852       if (row != null) {
853         writer.writeClassAttribute(row.getCustomClass());
854       }
855       for (int j = 0; j < columns.size(); j++) {
856         final AbstractUIColumnBase column = columns.get(j);
857         if (!column.isRendered() || column instanceof AbstractUIRow) {
858           offset++;
859         } else {
860           final Cell cell = grid.getCell(j - offset, i);
861           if (cell instanceof OriginCell) {
862             writer.startElement(HtmlElements.TH);
863             if (cell.getColumnSpan() > 1) {
864               writer.writeAttribute(HtmlAttributes.COLSPAN, cell.getColumnSpan());
865             }
866             if (cell.getRowSpan() > 1) {
867               writer.writeAttribute(HtmlAttributes.ROWSPAN, cell.getRowSpan());
868             }
869 
870             final UIComponent cellComponent = cell.getComponent();
871 
872             final Markup align;
873             final String alignString = ComponentUtils.getStringAttribute(column, Attributes.align);
874             if (multiHeader && cell.getColumnSpan() > 1) {
875               align = Markup.CENTER;
876             } else if (alignString != null) {
877               switch (TextAlign.valueOf(alignString)) {
878                 case right:
879                   align = Markup.RIGHT;
880                   break;
881                 case center:
882                   align = Markup.CENTER;
883                   break;
884                 case justify:
885                   align = Markup.JUSTIFY;
886                   break;
887                 default:
888                   align = null;
889               }
890             } else {
891               align = null;
892             }
893             writer.writeClassAttribute(
894                 TobagoClass.SHEET__HEADER_CELL,
895                 TobagoClass.SHEET__CELL.createMarkup(align),
896                 column.getCustomClass());
897             writer.startElement(HtmlElements.SPAN);
898             Markup markup = Markup.NULL;
899             String tip = ComponentUtils.getStringAttribute(column, Attributes.tip);
900             // sorter icons should only displayed when there is only 1 column and not input
901             CommandMap behaviorCommands = null;
902             if (cell.getColumnSpan() == 1 && cellComponent instanceof AbstractUIOut) {
903               final boolean sortable = ComponentUtils.getBooleanAttribute(column, Attributes.sortable);
904               if (sortable) {
905                 AbstractUILink sortCommand = (AbstractUILink) ComponentUtils.getFacet(column, Facets.sorter);
906                 if (sortCommand == null) {
907                   // assign id to column
908                   column.getClientId(facesContext);
909                   final String sorterId = column.getId() + "_" + AbstractUISheet.SORTER_ID;
910                   sortCommand = (AbstractUILink) ComponentUtils.createComponent(
911                       facesContext, Tags.link.componentType(), RendererTypes.Link, sorterId);
912                   sortCommand.setTransient(true);
913                   final AjaxBehavior reloadBehavior = createReloadBehavior(sheet);
914                   sortCommand.addClientBehavior("click", reloadBehavior);
915                   ComponentUtils.setFacet(column, Facets.sorter, sortCommand);
916                 }
917                 writer.writeIdAttribute(sortCommand.getClientId(facesContext));
918                 behaviorCommands = getBehaviorCommands(facesContext, sortCommand);
919                 ComponentUtils.removeFacet(column, Facets.sorter);
920                 if (tip == null) {
921                   tip = "";
922                 } else {
923                   tip += " - ";
924                 }
925                 tip += ResourceUtils.getString(facesContext, "sheet.sorting");
926 
927                 markup = markup.add(Markup.SORTABLE);
928 
929                 final SheetState sheetState = sheet.getSheetState(facesContext);
930                 if (column.getId().equals(sheetState.getSortedColumnId())) {
931                   final String sortTitle;
932                   if (sheetState.isAscending()) {
933                     sortTitle = ResourceUtils.getString(facesContext, "sheet.ascending");
934                     markup = markup.add(Markup.ASCENDING);
935                   } else {
936                     sortTitle = ResourceUtils.getString(facesContext, "sheet.descending");
937                     markup = markup.add(Markup.DESCENDING);
938                   }
939                   tip += " - " + sortTitle;
940                 }
941               }
942             }
943 
944             writer.writeClassAttribute(TobagoClass.SHEET__HEADER, TobagoClass.SHEET__HEADER.createMarkup(markup));
945             writer.writeAttribute(HtmlAttributes.TITLE, tip, true);
946 
947             encodeBehavior(writer, behaviorCommands);
948 
949             if (column instanceof AbstractUIColumnSelector && selectable.isMulti()) {
950               writer.startElement(HtmlElements.INPUT);
951               writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.CHECKBOX);
952 
953               writer.writeClassAttribute(TobagoClass.SHEET__COLUMN_SELECTOR);
954               writer.writeAttribute(
955                   HtmlAttributes.TITLE,
956                   ResourceUtils.getString(facesContext, "sheet.selectAll"),
957                   true);
958               writer.endElement(HtmlElements.INPUT);
959             } else {
960               cellComponent.encodeAll(facesContext);
961             }
962 
963             writer.endElement(HtmlElements.SPAN);
964             if (!autoLayout) {
965               if (column.isResizable()) {
966                 encodeResizing(writer, sheet, j - offset + cell.getColumnSpan() - 1);
967               }
968             }
969 
970             writer.endElement(HtmlElements.TH);
971           }
972         }
973       }
974       if (!autoLayout) {
975         // Add two filler columns. The second one get the size of the scrollBar via JavaScript.
976         encodeHeaderFiller(writer, sheet);
977         encodeHeaderFiller(writer, sheet);
978       }
979 
980       writer.endElement(HtmlElements.TR);
981     }
982   }
983 
984   private void encodeHeaderFiller(final TobagoResponseWriter writer, final AbstractUISheet sheet) throws IOException {
985     writer.startElement(HtmlElements.TH);
986     writer.writeClassAttribute(
987         TobagoClass.SHEET__HEADER_CELL,
988         TobagoClass.SHEET__HEADER_CELL.createMarkup(Markup.FILLER));
989     writer.startElement(HtmlElements.SPAN);
990     writer.writeClassAttribute(TobagoClass.SHEET__HEADER);
991     writer.endElement(HtmlElements.SPAN);
992     writer.endElement(HtmlElements.TH);
993   }
994 
995   private void writeColgroup(
996       final TobagoResponseWriter writer, final List<Integer> columnWidths,
997       final List<AbstractUIColumnBase> columns, final boolean isHeader) throws IOException {
998     writer.startElement(HtmlElements.COLGROUP);
999 
1000     int i = 0;
1001     for (final AbstractUIColumnBase column : columns) {
1002       if (!(column instanceof AbstractUIRow)) {
1003         if (column.isRendered()) {
1004           final Integer width = columnWidths.get(i);
1005           writeCol(writer, width >= 0 ? width : null);
1006         }
1007         i++;
1008       }
1009     }
1010     writeCol(writer, null); // extra entry for resizing...
1011     if (isHeader) {
1012       writeCol(writer, null); // extra entry for headerFiller
1013     }
1014     // TODO: the value should be added to the list
1015     writer.endElement(HtmlElements.COLGROUP);
1016   }
1017 
1018   private void writeCol(final TobagoResponseWriter writer, final Integer columnWidth) throws IOException {
1019     writer.startElement(HtmlElements.COL);
1020     writer.writeAttribute(HtmlAttributes.WIDTH, columnWidth);
1021     writer.endElement(HtmlElements.COL);
1022   }
1023 
1024   private Markup markupForLeftCenterRight(final ShowPosition position) {
1025     switch (position) {
1026       case left:
1027         return Markup.LEFT;
1028       case center:
1029         return Markup.CENTER;
1030       case right:
1031         return Markup.RIGHT;
1032       default:
1033         return Markup.NULL;
1034     }
1035   }
1036 
1037   @Override
1038   public boolean getRendersChildren() {
1039     return true;
1040   }
1041 
1042   private List<Integer> getSelectedRows(final AbstractUISheet data, final SheetState state) {
1043     List<Integer> selected = (List<Integer>) ComponentUtils.getAttribute(data, Attributes.selectedListString);
1044     if (selected == null && state != null) {
1045       selected = state.getSelectedRows();
1046     }
1047     if (selected == null) {
1048       selected = Collections.emptyList();
1049     }
1050     return selected;
1051   }
1052 
1053   private void encodeLink(
1054       final FacesContext facesContext, final AbstractUISheet data, final Application application,
1055       final boolean disabled, final SheetAction action, final Integer target, final Icons icon, final CssItem liClass)
1056       throws IOException {
1057 
1058     final String facet = action == SheetAction.toPage || action == SheetAction.toRow
1059         ? action.name() + "-" + target
1060         : action.name();
1061     final AbstractUILink command = ensurePagingCommand(facesContext, data, facet, facet, disabled);
1062     if (target != null) {
1063       ComponentUtils.setAttribute(command, Attributes.pagingTarget, target);
1064     }
1065 
1066     final Locale locale = facesContext.getViewRoot().getLocale();
1067     final String message = ResourceUtils.getString(facesContext, action.getBundleKey());
1068     final String tip = new MessageFormat(message, locale).format(new Integer[]{target}); // needed fot ToPage
1069 
1070     final TobagoResponseWriter writer = getResponseWriter(facesContext);
1071     writer.startElement(HtmlElements.LI);
1072     writer.writeClassAttribute(liClass, disabled ? BootstrapClass.DISABLED : null, BootstrapClass.PAGE_ITEM);
1073     writer.startElement(HtmlElements.BUTTON);
1074     writer.writeAttribute(HtmlAttributes.TYPE, HtmlButtonTypes.BUTTON);
1075     writer.writeClassAttribute(BootstrapClass.PAGE_LINK);
1076     writer.writeIdAttribute(command.getClientId(facesContext));
1077     writer.writeAttribute(HtmlAttributes.TITLE, tip, true);
1078     writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
1079     if (icon != null) {
1080       writer.startElement(HtmlElements.I);
1081       writer.writeClassAttribute(Icons.FA, icon);
1082       writer.endElement(HtmlElements.I);
1083     } else {
1084       writer.writeText(String.valueOf(target));
1085     }
1086     if (!disabled) {
1087       encodeBehavior(writer, facesContext, command);
1088     }
1089     data.getFacets().remove(facet);
1090     writer.endElement(HtmlElements.BUTTON);
1091     writer.endElement(HtmlElements.LI);
1092   }
1093 
1094   // TODO sheet.getColumnLayout() may return the wrong number of column...
1095   // TODO
1096   // TODO
1097 
1098   private void encodeResizing(final TobagoResponseWriter writer, final AbstractUISheet sheet, final int columnIndex)
1099       throws IOException {
1100     writer.startElement(HtmlElements.SPAN);
1101     writer.writeClassAttribute(TobagoClass.SHEET__HEADER_RESIZE);
1102     writer.writeAttribute(DataAttributes.COLUMN_INDEX, Integer.toString(columnIndex), false);
1103     writer.write("&nbsp;&nbsp;"); // is needed for IE
1104     writer.endElement(HtmlElements.SPAN);
1105   }
1106 
1107   private void encodeDirectPagingLinks(
1108       final FacesContext facesContext, final Application application, final AbstractUISheet sheet)
1109       throws IOException {
1110 
1111     int linkCount = ComponentUtils.getIntAttribute(sheet, Attributes.directLinkCount);
1112     linkCount--;  // current page needs no link
1113     final ArrayList<Integer> prevs = new ArrayList<>(linkCount);
1114     int page = sheet.getCurrentPage() + 1;
1115     for (int i = 0; i < linkCount && page > 1; i++) {
1116       page--;
1117       if (page > 0) {
1118         prevs.add(0, page);
1119       }
1120     }
1121 
1122     final ArrayList<Integer> nexts = new ArrayList<>(linkCount);
1123     page = sheet.getCurrentPage() + 1;
1124     final int pages = sheet.hasRowCount() || sheet.isRowsUnlimited() ? sheet.getPages() : Integer.MAX_VALUE;
1125     for (int i = 0; i < linkCount && page < pages; i++) {
1126       page++;
1127       if (page > 1) {
1128         nexts.add(page);
1129       }
1130     }
1131 
1132     if (prevs.size() > (linkCount / 2)
1133         && nexts.size() > (linkCount - (linkCount / 2))) {
1134       while (prevs.size() > (linkCount / 2)) {
1135         prevs.remove(0);
1136       }
1137       while (nexts.size() > (linkCount - (linkCount / 2))) {
1138         nexts.remove(nexts.size() - 1);
1139       }
1140     } else if (prevs.size() <= (linkCount / 2)) {
1141       while (prevs.size() + nexts.size() > linkCount) {
1142         nexts.remove(nexts.size() - 1);
1143       }
1144     } else {
1145       while (prevs.size() + nexts.size() > linkCount) {
1146         prevs.remove(0);
1147       }
1148     }
1149 
1150     int skip = prevs.size() > 0 ? prevs.get(0) : 1;
1151     if (!sheet.isShowDirectLinksArrows() && skip > 1) {
1152       skip -= linkCount - (linkCount / 2);
1153       skip--;
1154       if (skip < 1) {
1155         skip = 1;
1156       }
1157       encodeLink(facesContext, sheet, application, false, SheetAction.toPage, skip, Icons.ELLIPSIS_H, null);
1158     }
1159     for (final Integer prev : prevs) {
1160       encodeLink(facesContext, sheet, application, false, SheetAction.toPage, prev, null, null);
1161     }
1162     encodeLink(facesContext, sheet, application, false, SheetAction.toPage,
1163         sheet.getCurrentPage() + 1, null, BootstrapClass.ACTIVE);
1164 
1165     for (final Integer next : nexts) {
1166       encodeLink(facesContext, sheet, application, false, SheetAction.toPage, next, null, null);
1167     }
1168 
1169     skip = nexts.size() > 0 ? nexts.get(nexts.size() - 1) : pages;
1170     if (!sheet.isShowDirectLinksArrows() && skip < pages) {
1171       skip += linkCount / 2;
1172       skip++;
1173       if (skip > pages) {
1174         skip = pages;
1175       }
1176       encodeLink(facesContext, sheet, application, false, SheetAction.toPage, skip, Icons.ELLIPSIS_H, null);
1177     }
1178   }
1179 
1180   private AbstractUILink ensurePagingCommand(
1181       final FacesContext facesContext, final AbstractUISheet sheet, final String facet, final String id,
1182       final boolean disabled) {
1183 
1184     final Map<String, UIComponent> facets = sheet.getFacets();
1185     AbstractUILink command = (AbstractUILink) facets.get(facet);
1186     if (command == null) {
1187       command = (AbstractUILink) ComponentUtils.createComponent(facesContext, Tags.link.componentType(),
1188           RendererTypes.Link, SUFFIX_PAGE_ACTION + id);
1189       command.setRendered(true);
1190       command.setDisabled(disabled);
1191       command.setTransient(true);
1192       facets.put(facet, command);
1193 
1194       // add AjaxBehavior
1195       final AjaxBehavior behavior = createReloadBehavior(sheet);
1196       command.addClientBehavior("click", behavior);
1197     }
1198     return command;
1199   }
1200 
1201   private AjaxBehavior createReloadBehavior(final AbstractUISheet sheet) {
1202     final AjaxBehavior reloadBehavior = findReloadBehavior(sheet);
1203     final ArrayList<String> renderIds = new ArrayList<>();
1204     renderIds.add(sheet.getId());
1205     if (reloadBehavior != null) {
1206       renderIds.addAll(reloadBehavior.getRender());
1207     }
1208     final ArrayList<String> executeIds = new ArrayList<>();
1209     executeIds.add(sheet.getId());
1210     if (reloadBehavior != null) {
1211       executeIds.addAll(reloadBehavior.getExecute());
1212     }
1213     final AjaxBehavior behavior = new AjaxBehavior();
1214     behavior.setExecute(executeIds);
1215     behavior.setRender(renderIds);
1216     behavior.setTransient(true);
1217     return behavior;
1218   }
1219 
1220   private AjaxBehavior findReloadBehavior(final ClientBehaviorHolder holder) {
1221     final List<ClientBehavior> reload = holder.getClientBehaviors().get("reload");
1222     if (reload != null && !reload.isEmpty() && reload.get(0) instanceof AjaxBehavior) {
1223       return (AjaxBehavior) reload.get(0);
1224     } else {
1225       return null;
1226     }
1227   }
1228 }