All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.metawidget.faces.renderkit.html.HtmlTableLayoutRenderer Maven / Gradle / Ivy

There is a newer version: 4.2
Show newest version
// Metawidget
//
// This file is dual licensed under both the LGPL
// (http://www.gnu.org/licenses/lgpl-2.1.html) and the EPL
// (http://www.eclipse.org/org/documents/epl-v10.php). As a
// recipient of Metawidget, you may choose to receive it under either
// the LGPL or the EPL.
//
// Commercial licenses are also available. See http://metawidget.org
// for details.

package org.metawidget.faces.renderkit.html;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.component.UIInput;
import javax.faces.component.UIParameter;
import javax.faces.component.html.HtmlInputHidden;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.el.ValueBinding;

import org.metawidget.faces.FacesUtils;
import org.metawidget.faces.component.UIMetawidget;
import org.metawidget.faces.component.UIStub;
import org.metawidget.layout.iface.LayoutException;
import org.metawidget.util.WidgetBuilderUtils;
import org.metawidget.util.simple.SimpleLayoutUtils;
import org.metawidget.util.simple.StringUtils;

/**
 * Layout to arrange components in a table, with one column for labels and another for the
 * component.
 * 

* This implementation recognizes the following <f:facet> names: *

*

    *
  • header
  • *
  • footer
  • *
*

* This implementation recognizes the following <f:param> parameters: *

*

    *
  • tableStyle - CSS styles to apply to outer table tag *
  • tableStyleClass - CSS style class to apply to outer table tag *
  • columns - number of columns. Each label/component pair is considered one column *
  • columnClasses - comma delimited string of CSS style classes to apply to table * columns in order of: label, component, required *
  • labelStyle - CSS styles to apply to label column *
  • componentStyle - CSS styles to apply to component column *
  • requiredStyle - CSS styles to apply to required column (ie. the star) *
  • headerStyle - CSS styles to apply to table header *
  • headerStyleClass - CSS style class to apply to table header *
  • footerStyle - CSS styles to apply to table footer *
  • footerStyleClass - CSS style class to apply to table footer *
  • rowClasses - comma delimited string of CSS style classes to apply to alternating * table rows *
  • labelSuffix - suffix to put after the label name. Defaults to a colon (ie. * 'Name:') *
*

* The parameters columns and columnClasses might more properly be named * numberOfColumns and columnStyleClasses, but we are trying to follow the * javax.faces.component.html.HtmlDataTable convention. * * @author Richard Kennard */ @SuppressWarnings( "deprecation" ) public class HtmlTableLayoutRenderer extends HtmlLayoutRenderer { // // Private statics // private static final String TABLE_ID_PREFIX = "table-"; private static final String ROW_ID_SUFFIX = "-row"; private static final String LABEL_CELL_ID_SUFFIX = "-label-cell"; private static final String COMPONENT_CELL_ID_SUFFIX = "-cell"; private static final int JUST_COMPONENT_AND_REQUIRED = 2; private static final int LABEL_AND_COMPONENT_AND_REQUIRED = 3; // // Public methods // @Override public void encodeBegin( FacesContext context, UIComponent metawidgetComponent ) throws IOException { UIMetawidget metawidget = (UIMetawidget) metawidgetComponent; metawidget.putClientProperty( HtmlTableLayoutRenderer.class, null ); super.encodeBegin( context, metawidget ); ResponseWriter writer = context.getResponseWriter(); // Start table or, if there are hidden fields, a div around the table/hidden field // siblings. It is very important the clientId is written on to a 'root' node, else // AJAX will not correctly re-render the whole block if ( hasHtmlInputHiddenChildren( metawidget ) ) { writer.startElement( "div", metawidget ); writer.writeAttribute( "id", metawidget.getClientId( context ), "id" ); layoutHtmlInputHiddenChildren( context, metawidget ); writer.startElement( "table", metawidget ); } else { writer.startElement( "table", metawidget ); writer.writeAttribute( "id", metawidget.getClientId( context ), "id" ); } // Styles writeStyleAndClass( metawidget, writer, "table" ); // Determine label, component, required styles State state = getState( metawidget ); state.labelStyle = metawidget.getParameter( "labelStyle" ); state.componentStyle = metawidget.getParameter( "componentStyle" ); state.requiredStyle = metawidget.getParameter( "requiredStyle" ); // Determine inner styles String columnClassesParameter = metawidget.getParameter( "columnClasses" ); if ( columnClassesParameter != null ) { state.columnClasses = columnClassesParameter.split( StringUtils.SEPARATOR_COMMA ); } String rowClassesParameter = metawidget.getParameter( "rowClasses" ); if ( rowClassesParameter != null ) { state.rowClasses = rowClassesParameter.split( StringUtils.SEPARATOR_COMMA ); } // Determine number of columns String columnsParameter = metawidget.getParameter( "columns" ); if ( columnsParameter != null ) { state.columns = Integer.parseInt( columnsParameter ); if ( state.columns < 0 ) { throw LayoutException.newException( "columns must be >= 0" ); } } // Render header facet UIComponent componentHeader = metawidget.getFacet( "header" ); if ( componentHeader != null ) { writer.startElement( "thead", metawidget ); writer.startElement( "tr", metawidget ); writer.startElement( "td", metawidget ); // Header spans multiples of label/component/required int colspan = Math.max( JUST_COMPONENT_AND_REQUIRED, state.columns * LABEL_AND_COMPONENT_AND_REQUIRED ); writer.writeAttribute( "colspan", String.valueOf( colspan ), null ); writeStyleAndClass( metawidget, writer, "header" ); // Render facet FacesUtils.render( context, componentHeader ); writer.endElement( "td" ); writer.endElement( "tr" ); writer.endElement( "thead" ); } // Render footer facet (XHTML requires TFOOT come before TBODY) UIComponent componentFooter = metawidget.getFacet( "footer" ); if ( componentFooter != null ) { writer.startElement( "tfoot", metawidget ); writer.startElement( "tr", metawidget ); writer.startElement( "td", metawidget ); // Footer spans multiples of label/component/required int colspan = Math.max( JUST_COMPONENT_AND_REQUIRED, state.columns * LABEL_AND_COMPONENT_AND_REQUIRED ); writer.writeAttribute( "colspan", String.valueOf( colspan ), null ); writeStyleAndClass( metawidget, writer, "footer" ); // Render facet FacesUtils.render( context, componentFooter ); writer.endElement( "td" ); writer.endElement( "tr" ); writer.endElement( "tfoot" ); } writer.startElement( "tbody", metawidget ); } /** * Determines if the Metawidget has children which are instances of * javax.faces.component.html.HtmlInputHidden. If so, the hidden fields will be * written as siblings of the <table> and the whole thing wrapped in an outer * <div>. */ protected boolean hasHtmlInputHiddenChildren( UIComponent metawidget ) { for ( UIComponent componentChild : metawidget.getChildren() ) { if ( componentChild instanceof HtmlInputHidden ) { return true; } } return false; } /** * Layout any javax.faces.component.html.HtmlInputHidden children first, before the * <table>. */ protected void layoutHtmlInputHiddenChildren( FacesContext context, UIComponent metawidget ) throws IOException { for ( UIComponent componentChild : metawidget.getChildren() ) { if ( !( componentChild instanceof HtmlInputHidden ) ) { continue; } FacesUtils.render( context, componentChild ); } } @Override public void encodeChildren( FacesContext context, UIComponent metawidget ) throws IOException { State state = getState( metawidget ); // (layoutChildren may get called even if layoutBegin crashed. Try // to fail gracefully) if ( state == null ) { return; } List children = metawidget.getChildren(); // Next, for each child component... state.currentColumn = 0; state.currentRow = 0; for ( UIComponent componentChild : children ) { // ...that is visible... if ( componentChild instanceof UIStub ) { boolean visibleChildren = false; for ( UIComponent stubChild : componentChild.getChildren() ) { if ( !stubChild.isRendered() ) { continue; } visibleChildren = true; break; } if ( !visibleChildren ) { continue; } } if ( componentChild instanceof UIParameter ) { continue; } if ( componentChild instanceof HtmlInputHidden ) { continue; } if ( !componentChild.isRendered() ) { continue; } // ...count columns... state.currentColumn++; // ...render a label... layoutBeforeChild( context, metawidget, componentChild ); // ...and render the component layoutChild( context, metawidget, componentChild ); layoutAfterChild( context, metawidget, componentChild ); } } @Override public void encodeEnd( FacesContext context, UIComponent metawidget ) throws IOException { ResponseWriter writer = context.getResponseWriter(); writer.endElement( "tbody" ); writer.endElement( "table" ); // End div around the whole table if ( hasHtmlInputHiddenChildren( metawidget ) ) { writer.endElement( "div" ); } } // // Protected methods // protected void layoutBeforeChild( FacesContext context, UIComponent metawidget, UIComponent childComponent ) throws IOException { ResponseWriter writer = context.getResponseWriter(); String cssId = getCssId( childComponent ); // Section headings @SuppressWarnings( "unchecked" ) Map metadataAttributes = (Map) childComponent.getAttributes().get( UIMetawidget.COMPONENT_ATTRIBUTE_METADATA ); // (layoutBeforeChild may get called even if layoutBegin crashed. Try // to fail gracefully) State state = getState( metawidget ); if ( metadataAttributes != null ) { // Large components get a whole row boolean largeComponent = metawidget instanceof UIData || TRUE.equals( metadataAttributes.get( LARGE ) ); if ( largeComponent && state.currentColumn != 1 ) { writer.endElement( "tr" ); state.currentColumn = 1; } } // Start a new row, if necessary if ( state.currentColumn == 1 || state.currentColumn > state.columns ) { state.currentColumn = 1; writer.startElement( "tr", metawidget ); if ( cssId != null ) { writer.writeAttribute( "id", TABLE_ID_PREFIX + cssId + ROW_ID_SUFFIX, null ); } writeRowStyleClass( metawidget, writer, state.currentRow ); state.currentRow++; } // Start the label column boolean labelWritten = layoutLabel( context, metawidget, childComponent ); // Zero-column layouts need an extra row // (though we colour it the same from a CSS perspective) if ( labelWritten && state.columns == 0 ) { writer.endElement( "tr" ); writer.startElement( "tr", metawidget ); if ( cssId != null ) { writer.writeAttribute( "id", TABLE_ID_PREFIX + cssId + ROW_ID_SUFFIX + "2", null ); } writeRowStyleClass( metawidget, writer, state.currentRow ); } // Start the component column writer.startElement( "td", metawidget ); if ( cssId != null ) { writer.writeAttribute( "id", TABLE_ID_PREFIX + cssId + COMPONENT_CELL_ID_SUFFIX, null ); } // CSS if ( state.componentStyle != null ) { writer.writeAttribute( "style", state.componentStyle, null ); } writeColumnStyleClass( metawidget, writer, 1 ); // Colspan int colspan; // Metawidgets, tables and large components span all columns if ( childComponent instanceof UIMetawidget || childComponent instanceof UIData || SimpleLayoutUtils.isSpanAllColumns( metadataAttributes ) ) { colspan = ( state.columns * LABEL_AND_COMPONENT_AND_REQUIRED ) - 2; state.currentColumn = state.columns; if ( !labelWritten ) { colspan++; } // Nested table Metawidgets span the required column too (as they have their own // required column) if ( childComponent instanceof UIMetawidget && "table".equals( childComponent.getRendererType() ) ) { colspan++; } } else if ( !labelWritten ) { // Components without labels span two columns colspan = 2; } else { // Everyone else spans just one colspan = 1; } if ( colspan > 1 ) { writer.writeAttribute( "colspan", String.valueOf( colspan ), null ); } } /** * @return whether a label was written */ @Override protected boolean layoutLabel( FacesContext context, UIComponent metawidget, UIComponent componentNeedingLabel ) throws IOException { if ( getLabelText( componentNeedingLabel ) == null ) { return false; } ResponseWriter writer = context.getResponseWriter(); writer.startElement( "th", metawidget ); String cssId = getCssId( componentNeedingLabel ); if ( cssId != null ) { writer.writeAttribute( "id", TABLE_ID_PREFIX + cssId + LABEL_CELL_ID_SUFFIX, null ); } // CSS State state = getState( metawidget ); if ( state.labelStyle != null ) { writer.writeAttribute( "style", state.labelStyle, null ); } writeColumnStyleClass( metawidget, writer, 0 ); super.layoutLabel( context, metawidget, componentNeedingLabel ); writer.endElement( "th" ); return true; } protected void layoutAfterChild( FacesContext context, UIComponent metawidget, UIComponent childComponent ) throws IOException { ResponseWriter writer = context.getResponseWriter(); // End the component column writer.endElement( "td" ); // Render the 'required' column State state = getState( metawidget ); // (except embedded table Metawidgets, which have their own required column) if ( !( childComponent instanceof UIMetawidget && "table".equals( childComponent.getRendererType() ) ) && childComponent.getAttributes().containsKey( UIMetawidget.COMPONENT_ATTRIBUTE_METADATA ) ) { writer.startElement( "td", metawidget ); // CSS if ( state.requiredStyle != null ) { writer.writeAttribute( "style", state.requiredStyle, null ); } writeColumnStyleClass( metawidget, writer, 2 ); layoutRequired( context, metawidget, childComponent ); writer.endElement( "td" ); } // End the row, if necessary if ( state.currentColumn >= state.columns ) { state.currentColumn = 0; writer.endElement( "tr" ); } } protected void layoutRequired( FacesContext context, UIComponent metawidget, UIComponent child ) throws IOException { @SuppressWarnings( "unchecked" ) Map attributes = (Map) child.getAttributes().get( UIMetawidget.COMPONENT_ATTRIBUTE_METADATA ); ResponseWriter writer = context.getResponseWriter(); if ( attributes != null ) { if ( TRUE.equals( attributes.get( REQUIRED ) ) && !WidgetBuilderUtils.isReadOnly( attributes ) && !( (UIMetawidget) metawidget ).isReadOnly() ) { // UIStubs can have attributes="required: true". UIMetawidgets with // rendererType="simple" can be over required fields if ( child instanceof UIInput || child instanceof UIStub || child instanceof UIMetawidget ) { writer.write( "*" ); return; } } } // Render an empty div, so that the CSS can force it to a certain // width if desired for the layout (browsers seem to not respect // widths set on empty table columns) writer.startElement( "div", metawidget ); writer.endElement( "div" ); } protected String getCssId( UIComponent metawidget ) { ValueBinding binding = metawidget.getValueBinding( "value" ); if ( binding == null ) { return null; } return StringUtils.camelCase( FacesUtils.unwrapExpression( binding.getExpressionString() ), StringUtils.SEPARATOR_DOT_CHAR ); } protected void writeColumnStyleClass( UIComponent metawidget, ResponseWriter writer, int columnStyleClass ) throws IOException { State state = getState( metawidget ); // Note: As per the JSF spec, columnClasses do not repeat like rowClasses do. See... // // http://java.sun.com/javaee/javaserverfaces/1.2_MR1/docs/renderkitdocs/HTML_BASIC/javax.faces.Datajavax.faces.Table.html // // ...where it says 'If the number of [styleClasses] is less than the number of // columns specified in the "columns" attribute, no "class" attribute is output for each // column greater than the number of [styleClasses]' if ( state.columnClasses == null || state.columnClasses.length <= columnStyleClass ) { return; } String columnClass = state.columnClasses[columnStyleClass]; if ( columnClass.length() == 0 ) { return; } writer.writeAttribute( "class", columnClass.trim(), null ); } protected void writeRowStyleClass( UIComponent metawidget, ResponseWriter writer, int rowStyleClass ) throws IOException { State state = getState( metawidget ); if ( state.rowClasses == null ) { return; } String rowClass = state.rowClasses[rowStyleClass % state.rowClasses.length]; if ( rowClass.length() == 0 ) { return; } writer.writeAttribute( "class", rowClass.trim(), null ); } // // Private methods // /* package private */State getState( UIComponent metawidget ) { State state = (State) ( (UIMetawidget) metawidget ).getClientProperty( HtmlTableLayoutRenderer.class ); if ( state == null ) { state = new State(); ( (UIMetawidget) metawidget ).putClientProperty( HtmlTableLayoutRenderer.class, state ); } return state; } // // Inner class // /** * Simple, lightweight structure for saving state. */ /* package private */static class State { /* package private */int currentColumn; /* package private */int columns = 1; /* package private */int currentRow; /* package private */String labelStyle; /* package private */String componentStyle; /* package private */String requiredStyle; /* package private */String[] columnClasses; /* package private */String[] rowClasses; } }