/* Copyright 2014 InterCommIT b.v. * * This file is part of the "Weaves" project hosted on https://github.com/intercommit/Weaves * * Weaves is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Weaves is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Weaves. If not, see <http://www.gnu.org/licenses/>. * */ package nl.intercommit.weaves.components; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import nl.intercommit.weaves.base.BasicClientElement; import nl.intercommit.weaves.grid.CollectionPagedGridDataSource; import nl.intercommit.weaves.grid.HibernatePagedGridDataSource; import nl.intercommit.weaves.grid.PagedGridDataSource; import org.apache.tapestry5.Asset2; import org.apache.tapestry5.BindingConstants; import org.apache.tapestry5.Block; import org.apache.tapestry5.ClientBodyElement; import org.apache.tapestry5.ComponentEventCallback; import org.apache.tapestry5.ComponentResources; import org.apache.tapestry5.EventContext; import org.apache.tapestry5.MarkupWriter; import org.apache.tapestry5.PersistenceConstants; import org.apache.tapestry5.PropertyOverrides; import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.annotations.AfterRender; import org.apache.tapestry5.annotations.Component; import org.apache.tapestry5.annotations.Import; import org.apache.tapestry5.annotations.OnEvent; import org.apache.tapestry5.annotations.Parameter; import org.apache.tapestry5.annotations.Path; import org.apache.tapestry5.annotations.Persist; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.annotations.SetupRender; import org.apache.tapestry5.annotations.SupportsInformalParameters; import org.apache.tapestry5.beaneditor.BeanModel; import org.apache.tapestry5.corelib.components.Grid; import org.apache.tapestry5.corelib.components.Zone; import org.apache.tapestry5.grid.GridDataSource; import org.apache.tapestry5.internal.grid.CollectionGridDataSource; import org.apache.tapestry5.ioc.Messages; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.internal.util.TapestryException; import org.apache.tapestry5.services.AssetSource; import org.apache.tapestry5.services.ajax.AjaxResponseRenderer; import org.apache.tapestry5.services.ajax.JavaScriptCallback; import org.apache.tapestry5.services.javascript.InitializationPriority; import org.apache.tapestry5.services.javascript.JavaScriptSupport; import org.chenillekit.tapestry.core.components.AjaxCheckbox; /** * Custom component PagedGrid, embeds the usual tapestry grid plus extra paging row and eventhandlers for paging * Can also be extended with a checkable checkbox column ! * * Features: * * 1. Each row gets highlighted when clicked on * 2. Does not query the database for all rows, only a subset with limit * * When using checkboxes, make sure the rowidentifier function returns a unique index for the datasource, in case * of hibernate this will be a primary key so not a problem. But in case of a Collection the source may have been altered in * the meantime and thus give back the wrong rows. * * * @tapestrydoc */ @Import(library={"pagedgrid/PagedGridScript.js"},stylesheet={"pagedgrid/PagedGrid.css","pagedgrid/fixedheadertable.css"},stack="jquery") @SupportsInformalParameters public class PagedGrid<T> extends BasicClientElement { public static final String FETCH_CHILDREN_EVENT = "getParentChilds"; @Component private nl.intercommit.weaves.components.Grid childrenGrid; @Parameter(required = true) private PagedGridDataSource<T> pagedsource; @Parameter(defaultPrefix = BindingConstants.LITERAL) private String reorder; @Parameter(value="[25,50,100]") private List<Long> pagination; @Parameter(defaultPrefix = BindingConstants.LITERAL) private String add; /** * Parameter which defines if we have a column with checkboxes */ @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL) private boolean checkBoxes; /** * Parameter that tells that the parent rows have children */ @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL) private boolean tree; @Parameter(value="500",required=false,defaultPrefix=BindingConstants.LITERAL) private String maxHeight; @Parameter(value="false",defaultPrefix=BindingConstants.LITERAL) private boolean hoverAnimation; @Parameter(required=false,allowNull=true) private BeanModel<T> childModel; @Parameter private Integer rowsPerPage; @Component private AjaxCheckbox checkall; @Component private Zone expansionZone; @Persist(PersistenceConstants.SESSION) private Class<?> rowIdClass; //session persistence because it lives over multiple AJAX requests. @Persist(PersistenceConstants.SESSION) private Map<Object,Boolean> checkedItems; @Persist(PersistenceConstants.SESSION) private boolean checkedAll; @Property @Persist(PersistenceConstants.SESSION) private Integer pageSize; private int rowIndex; @Inject private ComponentResources resources; @Inject private JavaScriptSupport scriptSupport; @Inject private AjaxResponseRenderer ajaxResponseRenderer; @Inject private AssetSource as; @Inject @Symbol(SymbolConstants.PRODUCTION_MODE) private boolean prod; @Property @Inject @Path("pagedgrid/expand.png") private Asset2 expandImage; @Property @Inject @Path("pagedgrid/collapse.png") private Asset2 collapseImage; @Property @Inject @Path("pagedgrid/branch.png") private Asset2 branchImage; @Property private GridDataSource childrenSource; @Component( inheritInformalParameters=true, publishParameters="row,rowclass,columnIndex,include,exclude,model,sortModel,nonSortable,pagerposition", parameters = { "source=pagedsource", "add=prop:addedRow", "rowsPerPage=pageSize", "rowIndex=rowIndex", "reorder=prop:ordering", "overrides=customoverrides", "rowclass=rowclass", "pagedpager=pagedpager", "pagerposition=literal:bottom", "clientId=clientId"} ) private nl.intercommit.weaves.components.Grid grid; @Property @Component(parameters = { "pagination=pagination", "hasNextPage=hasNextPage", "rowsPerPage=grid.rowsperpage", "currentPage=grid.currentPage"}) private PagedGridPager pagedpager; @SetupRender private void checkParameters(MarkupWriter writer) { if (pagination.size() == 0) { throw new TapestryException("Specify at least one pagination value!",null);} checkedItems = new HashMap<Object, Boolean>(); if (pageSize == null) { if (rowsPerPage == null) { // if not defined rowsPerPage = pagination.get(0).intValue(); } pageSize = rowsPerPage; } } @AfterRender private void afterRender(MarkupWriter writer) { if (prod) { scriptSupport.importJavaScriptLibrary(as.getUnlocalizedAsset("nl/intercommit/weaves/jquery/fixedheadertable.min.js")); } else { scriptSupport.importJavaScriptLibrary(as.getUnlocalizedAsset("nl/intercommit/weaves/jquery/fixedheadertable.js")); } scriptSupport.addScript("initializeGrid('"+getClientId()+"',"+maxHeight+");"); if (pagedsource.getAvailableRows() != 0) { if (tree) { scriptSupport.addScript("observeExpansionZone('"+getClientId()+"');"); } } if (hoverAnimation) { scriptSupport.addScript("enableHovering(true);"); } if (checkBoxes) { scriptSupport.addScript(InitializationPriority.LATE,"listenToCheckAllBox('"+checkall.getClientId()+"');"); rowIdClass = pagedsource.getRowIdClass(); } checkedAll = false; } public int getRowIndex() { return rowIndex + (grid.getCurrentPage() - 1) * pageSize+1; } public void setRowIndex(int rowIndex) { this.rowIndex = rowIndex; } public String getRowClass() { return (rowIndex % 2 == 0?"odd":"even"); } public String getOrdering() { if (reorder != null) { return addExtraRows() + ","+ reorder; } return addExtraRows(); } public String getAddedRow() { if (add != null) { return addExtraRows() + ","+ add; } return addExtraRows(); } private String addExtraRows() { String extraRows = "row"; if (checkBoxes) { extraRows = extraRows + ",checkbox"; } if (tree) { extraRows = extraRows + ",expander"; } return extraRows; // hmm ok, this works } @OnEvent(value="pagesize") void onPageSizeFromPagedGrid(int rowsPerPage) { pageSize = rowsPerPage; grid.setCurrentPage(1); // reset to page1 } @OnEvent(value="fetchChildren") void fetchChildren(final Object rowId) { ajaxResponseRenderer.addCallback(new JavaScriptCallback() { public void run(JavaScriptSupport javascriptSupport) { javascriptSupport.addScript( String.format("resetRow('%s');", rowId)); }} ); resources.triggerEvent(FETCH_CHILDREN_EVENT,new Object[] {rowId}, new ComponentEventCallback<Object>() { @Override public boolean handleResult(final Object result) { if (result instanceof Block) { // this works !, block output is rendered as a single row added to the grid! ajaxResponseRenderer.addRender(new ClientBodyElement() { @Override public String getClientId() { return expansionZone.getClientId(); } @Override public Block getBody() { return (Block) result; } }); return true; } if (result instanceof GridDataSource) { // render the children grid ajaxResponseRenderer.addRender(new ClientBodyElement() { @Override public String getClientId() { return expansionZone.getClientId(); } @Override public Block getBody() { return resources.getBlock("childrenGrid"); } }); childrenSource = (GridDataSource) result; return true; } if (result instanceof Collection<?>) { // also render the children grid ajaxResponseRenderer.addRender(new ClientBodyElement() { @Override public String getClientId() { return expansionZone.getClientId(); } @Override public Block getBody() { return resources.getBlock("childrenGrid"); } }); childrenSource = new CollectionGridDataSource((Collection<?>) result); return true; } // handle other return results? return false; } } ); } public PropertyOverrides getCustomOverrides() { return new PagedGridOverrides(); } public PropertyOverrides getChildOverrides() { return new ChildGridOverrides(); } @OnEvent(value = "checkboxclicked") private void clickme(EventContext context) { if (!(rowIdClass == null || checkedItems == null)) { if (context.toStrings().length ==1) { checkedAll = !checkedAll; for (Object key: checkedItems.keySet()) { checkedItems.put(key, checkedAll); } } else { // rowObject is the actual(real) type and value of the selected item final Object rowObject = context.get(rowIdClass, 0); /* * containsKey works with equal and equal does NOT work with Long, which is usually the class * of a primary hibernate key! */ boolean found = false; for (Object key:checkedItems.keySet()) { if ((""+key).equals(""+rowObject)) { found = true; break; } } if (found) { checkedItems.put(rowObject, !checkedItems.get(rowObject)); } else { throw new TapestryException("Could not add selected row , because it is not in the grid.",this,null); } } } } /** * To fetch a list of checked items in your page, get a hold of the resources on that page and fetch the pagedgrid component. * * When dealing with a {@link HibernatePagedGridDataSource} the list contains the primary keys (Long) of the entities selected. * * When dealing with a {@link CollectionPagedGridDataSource} the list contains the index (Long) of the item in the collection * * @return a list with checked items as {@link Long}, or <code>null</code> if nothing has been selected */ public Collection<Object> getCheckedItems() { final Collection<Object> selectedItems = new LinkedList<Object>(); for (Object key: checkedItems.keySet()) { if (checkedItems.get(key)) { selectedItems.add(key); } } return selectedItems; } /** * Method to get to the underlying grid */ public Grid getGrid() { return grid; } public PagedGridDataSource<T> getPagedSource() { return pagedsource; } /** * Get the unique identifier for the currently rendered row * * @return an object containing the unique identifier to fetch from the source later on */ public Object getRowIdentifier() { final Object rowIdentifier = pagedsource.getIdentifierForRowValue(grid.getRow()); checkedItems.put(rowIdentifier,false); return rowIdentifier; } /** * Internal method used by the {@link PagedGridPager} component to create pagination links * @return */ public List<Long> getPagination() { return pagination; } public boolean getHasNextPage() { return (grid.getDataSource().getRowValue(grid.getCurrentPage()* grid.getRowsPerPage()) != null); } public void reset() { grid.reset(); checkedItems = null; } public BeanModel<?> getChildModel() { if (childModel == null) { return grid.getDataModel(); } return childModel; } /* * Overrides for parent and child grid */ private class PagedGridOverrides implements PropertyOverrides { public Block getOverrideBlock(String name) { try { return resources.getBlock(name); } catch (Exception e) { return resources.getBlockParameter(name); } } public Messages getOverrideMessages() { return resources.getContainerMessages(); } } private class ChildGridOverrides implements PropertyOverrides { public Block getOverrideBlock(String name) { if (name.endsWith("Header")) { return null; // child grid does not have headers. } try { return resources.getBlock(name+"Child"); } catch (Exception e) { return resources.getBlockParameter(name+"Child"); } } public Messages getOverrideMessages() { return resources.getContainerMessages(); } } }