001/*
002 * #%L
003 * GwtMaterial
004 * %%
005 * Copyright (C) 2015 - 2017 GwtMaterialDesign
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 * 
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 * 
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package gwt.material.design.client.data.infinite;
021
022import com.google.gwt.core.client.Scheduler;
023import com.google.gwt.dom.client.Element;
024import com.google.gwt.view.client.ProvidesKey;
025import com.google.gwt.view.client.Range;
026import gwt.material.design.client.base.MaterialWidget;
027import gwt.material.design.client.data.SortContext;
028import gwt.material.design.client.data.loader.LoadCallback;
029import gwt.material.design.client.data.loader.LoadConfig;
030import gwt.material.design.client.data.loader.LoadResult;
031import gwt.material.design.client.events.DefaultHandlerRegistry;
032import gwt.material.design.client.events.HandlerRegistry;
033import gwt.material.design.client.ui.table.DataDisplay;
034import gwt.material.design.client.base.InterruptibleTask;
035import gwt.material.design.client.data.AbstractDataView;
036import gwt.material.design.client.data.DataSource;
037import gwt.material.design.client.data.component.CategoryComponent;
038import gwt.material.design.client.data.component.Component;
039import gwt.material.design.client.data.component.Components;
040import gwt.material.design.client.data.component.RowComponent;
041import gwt.material.design.client.jquery.JQueryExtension;
042import gwt.material.design.client.ui.table.TableScaffolding;
043import gwt.material.design.jquery.client.api.JQueryElement;
044
045import java.util.ArrayList;
046import java.util.List;
047import java.util.logging.Level;
048import java.util.logging.Logger;
049
050import static gwt.material.design.jquery.client.api.JQuery.$;
051
052/**
053 * The InfiniteDataView is designed for lazy loading data, while using minimal DOM row elements.<br/>
054 * The data source will invoke a load when the view index changes on scrolling.<br/>
055 * <br/>
056 * <b>How to use:</b>
057 * <ul>
058 *   <li>View size can be configured manually or can be set to dynamic view using {@link #DYNAMIC_VIEW}.</li>
059 *   <li>Provide a valid {@link DataSource} implementation.</li>
060 * </ul>
061 *
062 * @author Ben Dol
063 */
064public class InfiniteDataView<T> extends AbstractDataView<T> implements HasLoader {
065
066    private static final Logger logger = Logger.getLogger(InfiniteDataView.class.getName());
067
068    /**
069     * Dynamic view will detect available space for the row views.
070     */
071    public static final int DYNAMIC_VIEW = -1;
072
073    // Static view size specification
074    // -1 is dynamic view sizing.
075    private int viewSize = DYNAMIC_VIEW;
076    private boolean dynamicView = true;
077
078    // Buffers for artificial spacing when
079    // cycling the rows within the view.
080    private JQueryElement bufferTop;
081    private JQueryElement bufferBottom;
082
083    // The current index of the view.
084    protected int viewIndex;
085    protected int lastScrollTop = 0;
086
087    // Lading new data flag
088    private boolean loading;
089    private boolean forceScroll;
090
091    // Data loading task
092    private InterruptibleTask loaderTask;
093
094    private int loaderBuffer = 10;
095    private int loaderIndex;
096    private int loaderSize;
097
098    // Data loader delay millis
099    private int loaderDelay = 200;
100
101    private List<T> loaderCache;
102
103    // Cache the selected rows for persistence
104    private List<T> selectedModels = new ArrayList<>();
105
106    // Cached models
107    protected InfiniteDataCache<T> dataCache = new InfiniteDataCache<>();
108
109    // Handler registry
110    private HandlerRegistry handlers;
111
112    public InfiniteDataView(int totalRows, DataSource<T> dataSource) {
113        this(totalRows, DYNAMIC_VIEW, dataSource);
114    }
115
116    public InfiniteDataView(int totalRows, int viewSize, DataSource<T> dataSource) {
117        super();
118        this.viewSize = viewSize;
119
120        setTotalRows(totalRows);
121        setDataSource(dataSource);
122    }
123
124    public InfiniteDataView(String name, int totalRows, DataSource<T> dataSource) {
125        this(name, totalRows, null, dataSource);
126    }
127
128    public InfiniteDataView(String name, int totalRows, ProvidesKey<T> keyProvider, DataSource<T> dataSource) {
129        super(name, keyProvider);
130
131        setTotalRows(totalRows);
132        setDataSource(dataSource);
133    }
134
135    public InfiniteDataView(String name, int totalRows, int viewSize, DataSource<T> dataSource) {
136        this(name, totalRows, viewSize, null, dataSource);
137    }
138
139    public InfiniteDataView(String name, int totalRows, int viewSize, ProvidesKey<T> keyProvider, DataSource<T> dataSource) {
140        super(name, keyProvider);
141        this.viewSize = viewSize;
142
143        setTotalRows(totalRows);
144        setDataSource(dataSource);
145    }
146
147    @Override
148    protected void onConstructed() {
149        setRenderer(new InfiniteRenderer<>());
150    }
151
152    @Override
153    protected void onSetup(TableScaffolding scaffolding) {
154        dynamicView = viewSize == DYNAMIC_VIEW;
155        if(dynamicView) {
156            setVisibleRange(0, getVisibleRowCapacity());
157            setViewSize(range.getLength());
158        }
159
160        JQueryElement topWrapper = $("<div>");
161        bufferTop = $("<div class='bufferTop'>");
162        topWrapper.append(bufferTop);
163        tbody.insert(new MaterialWidget(topWrapper.asElement()), 0);
164
165        bufferBottom = $("<div class='bufferBottom'>");
166        tableBody.append(bufferBottom);
167
168        handlers.clearHandlers();
169
170        handlers.registerHandler(display.addCategoryOpenedHandler(event -> {
171            dataCache.clear();
172            updateRows(viewIndex, true);
173            forceScroll = true;
174        }));
175
176        handlers.registerHandler(display.addCategoryClosedHandler(event -> {
177            dataCache.clear();
178            updateRows(viewIndex, true);
179            forceScroll = true;
180        }));
181
182        handlers.registerHandler(display.addRowSelectHandler(event -> {
183            if(event.isSelected()) {
184                if(!selectedModels.contains(event.getModel())) {
185                    selectedModels.add(event.getModel());
186                }
187            } else {
188                selectedModels.remove(event.getModel());
189            }
190        }));
191
192        handlers.registerHandler(display.addSelectAllHandler(event -> {
193            for(T model : event.getModels()) {
194                if (event.isSelected()) {
195                    if (!selectedModels.contains(model)) {
196                        selectedModels.add(model);
197                    }
198                } else {
199                    selectedModels.remove(model);
200                }
201            }
202        }));
203
204        // Setup the scroll event handlers
205        JQueryExtension.$(tableBody).scrollY(id, (e, scroll) ->  onVerticalScroll());
206
207        super.onSetup(scaffolding);
208    }
209
210    @Override
211    public void setDisplay(DataDisplay<T> display) {
212        super.setDisplay(display);
213
214        if(handlers != null) {
215            handlers.clearHandlers();
216        }
217        // Assign a new registry.
218        handlers = new DefaultHandlerRegistry(this.display, false);
219    }
220
221    @Override
222    public void render(Components<Component<?>> components) {
223        int calcRowHeight = getCalculatedRowHeight();
224        int topHeight = loaderIndex * calcRowHeight;
225        int catHeight = getCategoryHeight();
226        bufferTop.height(topHeight + (isUseCategories() ? (getPassedCategories().size() * catHeight) : 0));
227
228        int categories = isUseCategories() ? getCategories().size() : 0;
229        int bottomHeight = ((totalRows - viewSize - loaderBuffer) * calcRowHeight) - (categories * catHeight) - topHeight;
230        bufferBottom.height(bottomHeight);
231
232        super.render(components);
233
234        tableBody.scrollTop(lastScrollTop);
235    }
236
237    @Override
238    public boolean renderRows(Components<RowComponent<T>> rows) {
239        int prevCategories = categories.size();
240        if(super.renderRows(rows)) {
241            if (isUseCategories()) {
242                // Update the view size to accommodate the new categories
243                int newCatCount = categories.size() - prevCategories;
244                if (newCatCount != 0) {
245                    setVisibleRange(viewIndex, viewSize - newCatCount);
246                    setViewSize(range.getLength());
247                }
248
249                // show all the categories
250                List<CategoryComponent> lastHidden = new ArrayList<>();
251                for (CategoryComponent category : categories) {
252                    if (category.isRendered()) {
253                        category.getWidget().setVisible(true);
254                    }
255
256                    boolean hidden = false;
257                    if (isCategoryEmpty(category)) {
258                        Range range = getVisibleRange();
259                        int reach = range.getStart() + range.getLength();
260
261                        if (reach < getTotalRows()) {
262                            if (category.isRendered()) {
263                                category.getWidget().setVisible(false);
264                            }
265                            lastHidden.add(category);
266                            hidden = true;
267                        }
268                    }
269
270                    if (!hidden) {
271                        // Reshow the previously hidden categories
272                        // This is because we have found a valid category
273                        // after these were hidden, implying valid data.
274                        for (CategoryComponent hiddenCategory : lastHidden) {
275                            if (hiddenCategory.isRendered()) {
276                                hiddenCategory.getWidget().setVisible(true);
277                            }
278                        }
279                    }
280                }
281
282                // hide passed empty categories
283                for (CategoryComponent category : getPassedCategories()) {
284                    if (category.isRendered()) {
285                        category.getWidget().setVisible(false);
286                    }
287                }
288
289                subheaderLib.recalculate(true);
290            }
291            return true;
292        }
293        return false;
294    }
295
296    @Override
297    protected boolean doSort(SortContext<T> sortContext, Components<RowComponent<T>> rows) {
298        if(super.doSort(sortContext, rows)) {
299            // TODO: Potentially sort the cache data?
300            dataCache.clear(); // invalidate the cache upon successful sorts
301            return true;
302        } else {
303            return false;
304        }
305    }
306
307    protected void setViewSize(int viewSize) {
308        this.viewSize = viewSize;
309    }
310
311    @Override
312    public void refresh() {
313        super.refresh();
314        int rangeStart = range.getStart();
315        setVisibleRange(rangeStart, dynamicView ? getVisibleRowCapacity() : viewSize);
316        setViewSize(range.getLength());
317        updateRows(viewIndex, true);
318        forceScroll = true;
319    }
320
321    @Override
322    public void addCategory(CategoryComponent category) {
323        super.addCategory(category);
324        // Update the view size to accommodate the new category
325        setVisibleRange(viewIndex, viewSize + 1);
326        setViewSize(range.getLength());
327    }
328
329    public double getVisibleHeight() {
330        // We only want to account for row space.
331        return tableBody.height() - headerRow.$this().height();
332    }
333
334    protected Object onVerticalScroll() {
335        if(!rendering) {
336            int index = (int) Math.ceil(tableBody.scrollTop() / getCalculatedRowHeight());
337            if(index == 0 || index != viewIndex) {
338                updateRows(index, false);
339            }
340        }
341        return true;
342    }
343
344    protected void updateRows(int newIndex, boolean reload) {
345        // the number of rows visible within the grid's viewport
346        viewIndex = Math.min(newIndex, Math.max(0, totalRows - viewSize));
347        requestData(viewIndex, !reload);
348    }
349
350    protected void requestData(int index, boolean checkCache) {
351        if(loading) {
352            // Avoid loading again before the last load
353            return;
354        }
355        logger.fine("requestData() offset: " + index + ", viewSize: " + viewSize);
356        loaderIndex = Math.max(0, index - loaderBuffer);
357        loaderSize = viewSize + loaderBuffer;
358        if (loaderTask == null) {
359            loaderTask = new InterruptibleTask() {
360                @Override
361                public void onExecute() {
362                    if(checkCache) {
363                        List<T> cachedData = dataCache.getCache(loaderIndex, loaderSize);
364                        if (!cachedData.isEmpty()) {
365                            // Found in the cache
366                            loaderCache = cachedData;
367                        }
368                    }
369                    doLoad();
370                }
371            };
372        }
373
374        loaderTask.delay(loaderDelay);
375    }
376
377    protected void doLoad() {
378        loading = true;
379        // Check if the data was found in the cache
380        if(loaderCache != null && !loaderCache.isEmpty()) {
381            loaded(loaderIndex, loaderCache, false);
382            loaderCache.clear();
383            loaderCache = null;
384        } else {
385            setLoadMask(true);
386
387            dataSource.load(new LoadConfig<>(loaderIndex, loaderSize, getSortContext(), getOpenCategories()),
388                    new LoadCallback<T>() {
389                @Override
390                public void onSuccess(LoadResult<T> result) {
391                    loaded(result.getOffset(), result.getData(), result.getTotalLength(), result.isCacheData());
392                }
393                @Override
394                public void onFailure(Throwable caught) {
395                    logger.log(Level.SEVERE, "Load failure", caught);
396                    //TODO: What we need to do on failure? Maybe clear table?
397                }
398            });
399        }
400    }
401
402    @Override
403    public void setLoadMask(boolean loadMask) {
404        if(loadMask || !forceScroll) {
405            super.setLoadMask(loadMask);
406
407            // Ensure the mask element uses max height
408            Scheduler.get().scheduleDeferred(() -> {
409                if (loadMask && maskElement != null) {
410                    maskElement.height(bufferBottom.outerHeight(true) + bufferTop.outerHeight(true)
411                        + tableBody.outerHeight(true) + 1000 + "px");
412                }
413            });
414        }
415    }
416
417    @Override
418    public void loaded(int startIndex, List<T> data) {
419        loaded(startIndex, data, getTotalRows(), true);
420    }
421
422    /**
423     * Provide the option to load data with a cache parameter.
424     */
425    public void loaded(int startIndex, List<T> data, boolean cacheData) {
426        loaded(startIndex, data, getTotalRows(), cacheData);
427    }
428
429    /**
430     * With infinite data loading it is often required to
431     * change the total rows, upon loading of new data.
432     * See {@link #loaded(int, List)} for standard use.
433     *
434     * @param startIndex the new start index
435     * @param data the new list of data loaded
436     * @param totalRows the new total row count
437     */
438    public void loaded(int startIndex, List<T> data, int totalRows, boolean cacheData) {
439        lastScrollTop = tableBody.scrollTop();
440        setTotalRows(totalRows);
441        setVisibleRange(startIndex, loaderSize);
442
443        if(cacheData) {
444            dataCache.addCache(startIndex, data);
445        }
446        super.loaded(startIndex, data);
447        loading = false;
448
449        if(forceScroll) {
450            forceScroll = false;
451            updateRows((int) Math.ceil(tableBody.scrollTop() / getCalculatedRowHeight()), false);
452        }
453
454        // Ensure selection persistence
455        for(T model : selectedModels) {
456            Element row = getRowElementByModel(model);
457            if(row != null) {
458                selectRow(row, false);
459            }
460        }
461    }
462
463    /**
464     * Returns the total number of rows that are visible given
465     * the current grid height.
466     */
467    public int getVisibleRowCapacity() {
468        int rh = getCalculatedRowHeight();
469        double visibleHeight = getVisibleHeight();
470        int rows = (int) ((visibleHeight < 1) ? 0 : Math.floor(visibleHeight / rh));
471
472        int calcHeight = rh * rows;
473
474        while (calcHeight < visibleHeight) {
475            rows++;
476            calcHeight = rh * rows;
477        }
478
479        logger.finest("row height: " + rh + " visibleHeight: " + visibleHeight + " visible rows: "
480            + rows + " calcHeight: " + calcHeight);
481        return rows;
482    }
483
484    @Override
485    public void setRowHeight(int rowHeight) {
486        super.setRowHeight(rowHeight);
487
488        // Update the view row size.
489        if(isSetup()) {
490            refresh();
491        }
492    }
493
494    @Override
495    public List<T> getSelectedRowModels(boolean visibleOnly) {
496        if(visibleOnly) {
497            return super.getSelectedRowModels(true);
498        } else {
499            return selectedModels;
500        }
501    }
502
503    @Override
504    public int getLoaderDelay() {
505        return loaderDelay;
506    }
507
508    @Override
509    public void setLoaderDelay(int loaderDelay) {
510        this.loaderDelay = loaderDelay;
511    }
512
513    @Override
514    public int getLoaderBuffer() {
515        return loaderBuffer;
516    }
517
518    @Override
519    public void setLoaderBuffer(int loaderBuffer) {
520        this.loaderBuffer = Math.max(1, loaderBuffer);
521    }
522
523    @Override
524    public boolean isLoading() {
525        return loading;
526    }
527
528    public boolean isDynamicView() {
529        return dynamicView;
530    }
531}