001    /*
002     * Apache License
003     * Version 2.0, January 2004
004     * http://www.apache.org/licenses/
005     *
006     * Copyright 2008-2010 by chenillekit.org
007     *
008     * Licensed under the Apache License, Version 2.0 (the "License");
009     * you may not use this file except in compliance with the License.
010     * You may obtain a copy of the License at
011     *
012     * http://www.apache.org/licenses/LICENSE-2.0
013     */
014    
015    package org.chenillekit.tapestry.core.components;
016    
017    import org.apache.tapestry5.Block;
018    import org.apache.tapestry5.ClientElement;
019    import org.apache.tapestry5.ComponentResources;
020    import org.apache.tapestry5.ValueEncoder;
021    import org.apache.tapestry5.annotations.BeginRender;
022    import org.apache.tapestry5.annotations.Component;
023    import org.apache.tapestry5.annotations.Environmental;
024    import org.apache.tapestry5.annotations.Parameter;
025    import org.apache.tapestry5.annotations.Persist;
026    import org.apache.tapestry5.corelib.components.Delegate;
027    import org.apache.tapestry5.corelib.components.Loop;
028    import org.apache.tapestry5.ioc.annotations.Inject;
029    import org.apache.tapestry5.services.ComponentDefaultProvider;
030    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
031    import org.apache.tapestry5.util.StringToEnumCoercion;
032    import org.chenillekit.tapestry.core.internal.PagedSource;
033    import org.chenillekit.tapestry.core.internal.PagerPosition;
034    
035    /**
036     * Provides a paged list similar to the grid component. However, this uses a ul
037     * instead of a table by default. The list and its items are configurable via
038     * parameters.
039     *
040     * @version $Id: PagedLoop.java 674 2010-07-29 12:47:25Z homburgs $
041     */
042    public class PagedLoop implements ClientElement
043    {
044        @Environmental
045        private JavaScriptSupport javascriptSupport;
046    
047        @Parameter(value = "prop:componentResources.id", defaultPrefix = "literal")
048        private String clientId;
049    
050        /**
051         * The element to render. If not null, then the loop will render the indicated element around its body (on each pass through the loop).
052         * The default is derived from the component template.
053         */
054        @Parameter(value = "prop:componentResources.elementName", defaultPrefix = "literal")
055        private String element;
056    
057        /**
058         * Defines the collection of values for the loop to iterate over.
059         */
060        @SuppressWarnings("unused")
061        @Parameter(required = true)
062        private Iterable<?> source;
063    
064        private PagedSource<?> pagedSource;
065    
066        /**
067         * Defines where the pager (used to navigate within the "pages" of results)
068         * should be displayed: "top", "bottom", "both" or "none".
069         */
070        @Parameter(value = "bottom", defaultPrefix = "literal")
071        private String pagerPosition;
072    
073        private PagerPosition internalPagerPosition;
074    
075        /**
076         * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide
077         * up the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result set.
078         */
079        @Parameter("25")
080        private int rowsPerPage;
081    
082        @Persist
083        private int currentPage;
084    
085        /**
086         * The current value, set before the component renders its body.
087         */
088        @SuppressWarnings("unused")
089        @Parameter
090        private Object value;
091    
092        /**
093         * If true and the Loop is enclosed by a Form, then the normal state saving logic is turned off.
094         * Defaults to false, enabling state saving logic within Forms.
095         */
096        @SuppressWarnings("unused")
097        @Parameter(name = "volatile")
098        private boolean isVolatile;
099    
100        /**
101         * The index into the source items.
102         */
103        @SuppressWarnings("unused")
104        @Parameter
105        private int index;
106    
107            /**
108             * Value encoder for the value, usually determined automatically from the type of the property bound to the value
109             * parameter.
110             */
111            @Parameter(required = true)
112            private ValueEncoder encoder;
113    
114        @SuppressWarnings("unused")
115        @Component(parameters = {"source=pagedSource",
116                "element=prop:element", "value=inherit:value",
117                "volatile=inherit:volatile", "encoder=inherit:encoder",
118                "index=inherit:index"})
119        private Loop loop;
120    
121        @Component(parameters = {"source=pagedSource", "rowsPerPage=rowsPerPage",
122                "currentPage=currentPage"})
123        private Pager internalPager;
124    
125        @SuppressWarnings("unused")
126        @Component(parameters = "to=pagerTop")
127        private Delegate pagerTop;
128    
129        @SuppressWarnings("unused")
130        @Component(parameters = "to=pagerBottom")
131        private Delegate pagerBottom;
132    
133        /**
134         * A Block to render instead of the table (and pager, etc.) when the source
135         * is empty. The default is simply the text "There is no data to display".
136         * This parameter is used to customize that message, possibly including
137         * components to allow the user to create new objects.
138         */
139        @Parameter(value = "block:empty")
140        private Block empty;
141    
142            @Inject
143            private ComponentResources resources;
144    
145            @Inject
146            private ComponentDefaultProvider defaultProvider;
147    
148        private String assignedClientId;
149    
150        public String getElement()
151        {
152            return element;
153        }
154    
155        public Object getPagerTop()
156        {
157            return internalPagerPosition.isMatchTop() ? internalPager : null;
158        }
159    
160        public Object getPagerBottom()
161        {
162            return internalPagerPosition.isMatchBottom() ? internalPager : null;
163        }
164    
165        public PagedSource<?> getPagedSource()
166        {
167            return pagedSource;
168        }
169    
170        public int getRowsPerPage()
171        {
172            return rowsPerPage;
173        }
174    
175        public void setRowsPerPage(int rowsPerPage)
176        {
177            this.rowsPerPage = rowsPerPage;
178        }
179    
180        public int getCurrentPage()
181        {
182            return currentPage;
183        }
184    
185        public void setCurrentPage(int currentPage)
186        {
187            this.currentPage = currentPage;
188        }
189    
190            ValueEncoder defaultEncoder()
191            {
192                    return defaultProvider.defaultValueEncoder("value", resources);
193            }
194    
195            @SuppressWarnings("unchecked")
196        Object setupRender()
197        {
198                    if (currentPage == 0)
199                            currentPage = 1;
200    
201                    assignedClientId = javascriptSupport.allocateClientId(clientId);
202            internalPagerPosition = new StringToEnumCoercion<PagerPosition>(
203                    PagerPosition.class).coerce(pagerPosition);
204    
205            pagedSource = new PagedSource(source);
206    
207            int availableRows = pagedSource.getTotalRowCount();
208    
209            // If there's no rows, display the empty block placeholder.
210            if (availableRows == 0)
211            {
212                return empty;
213            }
214    
215            int startIndex = (currentPage - 1) * rowsPerPage;
216            int endIndex = Math.min(startIndex + rowsPerPage - 1,
217                                    availableRows - 1);
218    
219            pagedSource.prepare(startIndex, endIndex);
220    
221            return null;
222        }
223    
224        @BeginRender
225        Object begin()
226        {
227            // Skip rendering of component (template, body, etc.) when there's
228            // nothing to display.
229            // The empty placeholder will already have rendered.
230            return (pagedSource.getTotalRowCount() != 0);
231        }
232    
233        void onAction(int newPage)
234        {
235            // TODO: Validate newPage in range
236            currentPage = newPage;
237        }
238    
239        /**
240         * Returns a unique id for the element. This value will be unique for any given rendering of a
241         * page. This value is intended for use as the id attribute of the client-side element, and will
242         * be used with any DHTML/Ajax related JavaScript.
243         */
244        public String getClientId()
245        {
246            return assignedClientId;
247        }
248    }