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}