vscroll 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +17 -16
  3. package/dist/bundles/vscroll.esm5.js +403 -321
  4. package/dist/bundles/vscroll.esm5.js.map +1 -1
  5. package/dist/bundles/vscroll.esm5.min.js +2 -2
  6. package/dist/bundles/vscroll.esm5.min.js.map +1 -1
  7. package/dist/bundles/vscroll.esm6.js +310 -244
  8. package/dist/bundles/vscroll.esm6.js.map +1 -1
  9. package/dist/bundles/vscroll.esm6.min.js +2 -2
  10. package/dist/bundles/vscroll.esm6.min.js.map +1 -1
  11. package/dist/bundles/vscroll.umd.js +404 -322
  12. package/dist/bundles/vscroll.umd.js.map +1 -1
  13. package/dist/bundles/vscroll.umd.min.js +2 -2
  14. package/dist/bundles/vscroll.umd.min.js.map +1 -1
  15. package/dist/esm2015/classes/domRoutines.js +101 -66
  16. package/dist/esm2015/classes/domRoutines.js.map +1 -1
  17. package/dist/esm2015/classes/logger.js +2 -2
  18. package/dist/esm2015/classes/logger.js.map +1 -1
  19. package/dist/esm2015/classes/paddings.js +5 -5
  20. package/dist/esm2015/classes/paddings.js.map +1 -1
  21. package/dist/esm2015/classes/state/cycle.js +7 -6
  22. package/dist/esm2015/classes/state/cycle.js.map +1 -1
  23. package/dist/esm2015/classes/state/render.js +1 -1
  24. package/dist/esm2015/classes/state/render.js.map +1 -1
  25. package/dist/esm2015/classes/state/scroll.js +6 -6
  26. package/dist/esm2015/classes/state/scroll.js.map +1 -1
  27. package/dist/esm2015/classes/state.js +23 -15
  28. package/dist/esm2015/classes/state.js.map +1 -1
  29. package/dist/esm2015/classes/viewport.js +13 -22
  30. package/dist/esm2015/classes/viewport.js.map +1 -1
  31. package/dist/esm2015/index.js.map +1 -1
  32. package/dist/esm2015/interfaces/index.js.map +1 -1
  33. package/dist/esm2015/interfaces/routines.js +2 -0
  34. package/dist/esm2015/interfaces/routines.js.map +1 -0
  35. package/dist/esm2015/interfaces/state.js.map +1 -1
  36. package/dist/esm2015/interfaces/workflow.js.map +1 -1
  37. package/dist/esm2015/processes/adapter/reload.js +1 -1
  38. package/dist/esm2015/processes/adapter/reload.js.map +1 -1
  39. package/dist/esm2015/processes/adjust.js +44 -15
  40. package/dist/esm2015/processes/adjust.js.map +1 -1
  41. package/dist/esm2015/processes/end.js +18 -16
  42. package/dist/esm2015/processes/end.js.map +1 -1
  43. package/dist/esm2015/processes/fetch.js +3 -3
  44. package/dist/esm2015/processes/fetch.js.map +1 -1
  45. package/dist/esm2015/processes/init.js +2 -2
  46. package/dist/esm2015/processes/init.js.map +1 -1
  47. package/dist/esm2015/processes/render.js +6 -6
  48. package/dist/esm2015/processes/render.js.map +1 -1
  49. package/dist/esm2015/processes/scroll.js +21 -21
  50. package/dist/esm2015/processes/scroll.js.map +1 -1
  51. package/dist/esm2015/scroller.js +4 -4
  52. package/dist/esm2015/scroller.js.map +1 -1
  53. package/dist/esm2015/version.js +1 -1
  54. package/dist/esm2015/version.js.map +1 -1
  55. package/dist/esm2015/workflow.js +9 -8
  56. package/dist/esm2015/workflow.js.map +1 -1
  57. package/dist/esm5/classes/adapter.js +4 -4
  58. package/dist/esm5/classes/adapter.js.map +1 -1
  59. package/dist/esm5/classes/buffer/cache.js +1 -1
  60. package/dist/esm5/classes/buffer/cache.js.map +1 -1
  61. package/dist/esm5/classes/buffer/checkCall.js +4 -4
  62. package/dist/esm5/classes/buffer/checkCall.js.map +1 -1
  63. package/dist/esm5/classes/buffer.js +3 -3
  64. package/dist/esm5/classes/buffer.js.map +1 -1
  65. package/dist/esm5/classes/domRoutines.js +115 -66
  66. package/dist/esm5/classes/domRoutines.js.map +1 -1
  67. package/dist/esm5/classes/logger.js +21 -21
  68. package/dist/esm5/classes/logger.js.map +1 -1
  69. package/dist/esm5/classes/paddings.js +5 -5
  70. package/dist/esm5/classes/paddings.js.map +1 -1
  71. package/dist/esm5/classes/state/cycle.js +9 -8
  72. package/dist/esm5/classes/state/cycle.js.map +1 -1
  73. package/dist/esm5/classes/state/render.js +1 -1
  74. package/dist/esm5/classes/state/render.js.map +1 -1
  75. package/dist/esm5/classes/state/scroll.js +11 -11
  76. package/dist/esm5/classes/state/scroll.js.map +1 -1
  77. package/dist/esm5/classes/state.js +23 -15
  78. package/dist/esm5/classes/state.js.map +1 -1
  79. package/dist/esm5/classes/viewport.js +14 -23
  80. package/dist/esm5/classes/viewport.js.map +1 -1
  81. package/dist/esm5/index.js.map +1 -1
  82. package/dist/esm5/inputs/validation.js +4 -4
  83. package/dist/esm5/inputs/validation.js.map +1 -1
  84. package/dist/esm5/interfaces/index.js.map +1 -1
  85. package/dist/esm5/interfaces/routines.js +2 -0
  86. package/dist/esm5/interfaces/routines.js.map +1 -0
  87. package/dist/esm5/interfaces/state.js.map +1 -1
  88. package/dist/esm5/interfaces/workflow.js.map +1 -1
  89. package/dist/esm5/processes/adapter/reload.js +1 -1
  90. package/dist/esm5/processes/adapter/reload.js.map +1 -1
  91. package/dist/esm5/processes/adapter/remove.js +1 -1
  92. package/dist/esm5/processes/adapter/remove.js.map +1 -1
  93. package/dist/esm5/processes/adjust.js +48 -19
  94. package/dist/esm5/processes/adjust.js.map +1 -1
  95. package/dist/esm5/processes/clip.js +5 -5
  96. package/dist/esm5/processes/clip.js.map +1 -1
  97. package/dist/esm5/processes/end.js +18 -16
  98. package/dist/esm5/processes/end.js.map +1 -1
  99. package/dist/esm5/processes/fetch.js +5 -5
  100. package/dist/esm5/processes/fetch.js.map +1 -1
  101. package/dist/esm5/processes/init.js +2 -2
  102. package/dist/esm5/processes/init.js.map +1 -1
  103. package/dist/esm5/processes/misc/base.js +1 -1
  104. package/dist/esm5/processes/misc/base.js.map +1 -1
  105. package/dist/esm5/processes/preClip.js +2 -2
  106. package/dist/esm5/processes/preClip.js.map +1 -1
  107. package/dist/esm5/processes/preFetch.js +5 -5
  108. package/dist/esm5/processes/preFetch.js.map +1 -1
  109. package/dist/esm5/processes/render.js +6 -6
  110. package/dist/esm5/processes/render.js.map +1 -1
  111. package/dist/esm5/processes/scroll.js +22 -22
  112. package/dist/esm5/processes/scroll.js.map +1 -1
  113. package/dist/esm5/scroller.js +5 -5
  114. package/dist/esm5/scroller.js.map +1 -1
  115. package/dist/esm5/version.js +1 -1
  116. package/dist/esm5/version.js.map +1 -1
  117. package/dist/esm5/workflow.js +15 -12
  118. package/dist/esm5/workflow.js.map +1 -1
  119. package/dist/typings/classes/domRoutines.d.ts +23 -18
  120. package/dist/typings/classes/logger.d.ts +1 -1
  121. package/dist/typings/classes/paddings.d.ts +2 -2
  122. package/dist/typings/classes/state/cycle.d.ts +1 -1
  123. package/dist/typings/classes/state/render.d.ts +1 -1
  124. package/dist/typings/classes/state/scroll.d.ts +4 -4
  125. package/dist/typings/classes/state.d.ts +6 -3
  126. package/dist/typings/classes/viewport.d.ts +2 -4
  127. package/dist/typings/index.d.ts +2 -2
  128. package/dist/typings/interfaces/index.d.ts +3 -2
  129. package/dist/typings/interfaces/routines.d.ts +157 -0
  130. package/dist/typings/interfaces/state.d.ts +2 -15
  131. package/dist/typings/interfaces/workflow.d.ts +3 -0
  132. package/dist/typings/processes/adjust.d.ts +1 -0
  133. package/dist/typings/processes/end.d.ts +1 -2
  134. package/dist/typings/scroller.d.ts +1 -1
  135. package/dist/typings/workflow.d.ts +2 -2
  136. package/package.json +18 -17
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * vscroll (https://github.com/dhilt/vscroll) FESM2015
3
- * Version: 1.4.1 (2021-11-02T00:21:08.971Z)
3
+ * Version: 1.5.0 (2022-02-02T15:07:21.893Z)
4
4
  * Author: Denis Hilt
5
5
  * License: MIT
6
6
  */
@@ -328,7 +328,7 @@ const reactiveConfigStorage = new Map();
328
328
 
329
329
  var core = {
330
330
  name: 'vscroll',
331
- version: '1.4.1'
331
+ version: '1.5.0'
332
332
  };
333
333
 
334
334
  let instanceCount$1 = 0;
@@ -1276,10 +1276,10 @@ const BaseAdapterProcessFactory = (process) => { var _a; return _a = class BaseA
1276
1276
  const initProcesses = [CommonProcess.init, AdapterProcess.reset, AdapterProcess.reload];
1277
1277
  class Init extends BaseProcessFactory(CommonProcess.init) {
1278
1278
  static run(scroller, process) {
1279
- const { state: { cycle }, workflow } = scroller;
1279
+ const { state, workflow } = scroller;
1280
1280
  const isInitial = initProcesses.includes(process);
1281
1281
  scroller.logger.logCycle(true);
1282
- cycle.start(isInitial, process);
1282
+ state.startWorkflowCycle(isInitial, process);
1283
1283
  workflow.call({
1284
1284
  process: Init.process,
1285
1285
  status: ProcessStatus.next
@@ -1298,15 +1298,15 @@ class Scroll extends BaseProcessFactory(CommonProcess.scroll) {
1298
1298
  Scroll.onThrottle(scroller, position, () => Scroll.onScroll(scroller, workflow));
1299
1299
  }
1300
1300
  static onSynthetic(scroller, position) {
1301
- const { scrollState } = scroller.state;
1302
- const synthPos = scrollState.syntheticPosition;
1301
+ const { scroll } = scroller.state;
1302
+ const synthPos = scroll.syntheticPosition;
1303
1303
  if (synthPos !== null) {
1304
- if (scrollState.syntheticFulfill) {
1305
- scrollState.syntheticPosition = null;
1304
+ if (scroll.syntheticFulfill) {
1305
+ scroll.syntheticPosition = null;
1306
1306
  }
1307
- if (!scrollState.syntheticFulfill || synthPos === position) {
1307
+ if (!scroll.syntheticFulfill || synthPos === position) {
1308
1308
  scroller.logger.log(() => [
1309
- 'skipping scroll', position, `[${scrollState.syntheticFulfill ? '' : 'pre-'}synthetic]`
1309
+ 'skipping scroll', position, `[${scroll.syntheticFulfill ? '' : 'pre-'}synthetic]`
1310
1310
  ]);
1311
1311
  return true;
1312
1312
  }
@@ -1317,13 +1317,13 @@ class Scroll extends BaseProcessFactory(CommonProcess.scroll) {
1317
1317
  return false;
1318
1318
  }
1319
1319
  static onThrottle(scroller, position, done) {
1320
- const { state: { scrollState }, settings: { throttle }, logger } = scroller;
1321
- scrollState.current = Scroll.getScrollEvent(position, scrollState.previous);
1322
- const { direction, time } = scrollState.current;
1323
- const timeDiff = scrollState.previous ? time - scrollState.previous.time : Infinity;
1320
+ const { state: { scroll }, settings: { throttle }, logger } = scroller;
1321
+ scroll.current = Scroll.getScrollEvent(position, scroll.previous);
1322
+ const { direction, time } = scroll.current;
1323
+ const timeDiff = scroll.previous ? time - scroll.previous.time : Infinity;
1324
1324
  const delta = throttle - timeDiff;
1325
1325
  const shouldDelay = isFinite(delta) && delta > 0;
1326
- const alreadyDelayed = !!scrollState.scrollTimer;
1326
+ const alreadyDelayed = !!scroll.scrollTimer;
1327
1327
  logger.log(() => [
1328
1328
  direction === Direction.backward ? '\u2934' : '\u2935',
1329
1329
  position,
@@ -1331,17 +1331,17 @@ class Scroll extends BaseProcessFactory(CommonProcess.scroll) {
1331
1331
  shouldDelay ? (alreadyDelayed ? 'delayed' : `/ ${delta}ms delay`) : ''
1332
1332
  ]);
1333
1333
  if (!shouldDelay) {
1334
- if (scrollState.scrollTimer) {
1335
- clearTimeout(scrollState.scrollTimer);
1336
- scrollState.scrollTimer = null;
1334
+ if (scroll.scrollTimer) {
1335
+ clearTimeout(scroll.scrollTimer);
1336
+ scroll.scrollTimer = null;
1337
1337
  }
1338
1338
  done();
1339
1339
  return;
1340
1340
  }
1341
1341
  if (!alreadyDelayed) {
1342
- scrollState.scrollTimer = setTimeout(() => {
1342
+ scroll.scrollTimer = setTimeout(() => {
1343
1343
  logger.log(() => {
1344
- const curr = Scroll.getScrollEvent(scroller.viewport.scrollPosition, scrollState.current);
1344
+ const curr = Scroll.getScrollEvent(scroller.viewport.scrollPosition, scroll.current);
1345
1345
  return [
1346
1346
  curr.direction === Direction.backward ? '\u2934' : '\u2935',
1347
1347
  curr.position,
@@ -1350,7 +1350,7 @@ class Scroll extends BaseProcessFactory(CommonProcess.scroll) {
1350
1350
  position
1351
1351
  ];
1352
1352
  });
1353
- scrollState.scrollTimer = null;
1353
+ scroll.scrollTimer = null;
1354
1354
  done();
1355
1355
  }, delta);
1356
1356
  }
@@ -1369,11 +1369,11 @@ class Scroll extends BaseProcessFactory(CommonProcess.scroll) {
1369
1369
  return { position, direction, time };
1370
1370
  }
1371
1371
  static onScroll(scroller, workflow) {
1372
- const { state: { scrollState, cycle } } = scroller;
1373
- scrollState.previous = Object.assign({}, scrollState.current);
1374
- scrollState.current = null;
1372
+ const { state: { scroll, cycle } } = scroller;
1373
+ scroll.previous = Object.assign({}, scroll.current);
1374
+ scroll.current = null;
1375
1375
  if (cycle.busy.get()) {
1376
- scroller.logger.log(() => ['skipping scroll', scrollState.previous.position, '[pending]']);
1376
+ scroller.logger.log(() => ['skipping scroll', scroll.previous.position, '[pending]']);
1377
1377
  return;
1378
1378
  }
1379
1379
  workflow.call({
@@ -1424,7 +1424,7 @@ class Reload extends BaseAdapterProcessFactory(AdapterProcess.reload) {
1424
1424
  viewport.reset(buffer.startIndex);
1425
1425
  const payload = {};
1426
1426
  if (state.cycle.busy.get()) {
1427
- state.scrollState.cleanupTimers();
1427
+ state.scroll.stop();
1428
1428
  payload.finalize = true;
1429
1429
  state.cycle.interrupter = Reload.process;
1430
1430
  }
@@ -2183,9 +2183,9 @@ class Fetch extends BaseProcessFactory(CommonProcess.fetch) {
2183
2183
  }
2184
2184
  }
2185
2185
  else {
2186
- const { state: { scrollState, fetch }, viewport } = scroller;
2187
- if (scrollState.positionBeforeAsync === null) {
2188
- scrollState.positionBeforeAsync = viewport.scrollPosition;
2186
+ const { state: { scroll, fetch }, viewport } = scroller;
2187
+ if (scroll.positionBeforeAsync === null) {
2188
+ scroll.positionBeforeAsync = viewport.scrollPosition;
2189
2189
  }
2190
2190
  fetch.cancel = () => {
2191
2191
  box.success = () => null;
@@ -2305,13 +2305,13 @@ class PostFetch extends BaseProcessFactory(CommonProcess.postFetch) {
2305
2305
 
2306
2306
  class Render extends BaseProcessFactory(CommonProcess.render) {
2307
2307
  static run(scroller) {
2308
- const { workflow, state: { cycle, render, scrollState }, viewport } = scroller;
2308
+ const { workflow, state: { cycle, render, scroll }, viewport, routines } = scroller;
2309
2309
  scroller.logger.stat('before new items render');
2310
- if (scrollState.positionBeforeAsync === null) {
2311
- scrollState.positionBeforeAsync = viewport.scrollPosition;
2310
+ if (scroll.positionBeforeAsync === null) {
2311
+ scroll.positionBeforeAsync = viewport.scrollPosition;
2312
2312
  }
2313
- render.renderTimer = setTimeout(() => {
2314
- render.renderTimer = null;
2313
+ render.cancel = routines.render(() => {
2314
+ render.cancel = null;
2315
2315
  if (Render.doRender(scroller)) {
2316
2316
  workflow.call({
2317
2317
  process: Render.process,
@@ -2326,7 +2326,7 @@ class Render extends BaseProcessFactory(CommonProcess.render) {
2326
2326
  payload: { error: 'Can\'t associate item with element' }
2327
2327
  });
2328
2328
  }
2329
- }, 0);
2329
+ });
2330
2330
  }
2331
2331
  static doRender(scroller) {
2332
2332
  const { state: { fetch, render }, viewport, buffer, logger } = scroller;
@@ -2357,14 +2357,79 @@ class Render extends BaseProcessFactory(CommonProcess.render) {
2357
2357
  }
2358
2358
  }
2359
2359
 
2360
+ const isInterrupted = ({ call }) => !!call.interrupted;
2361
+ class End extends BaseProcessFactory(CommonProcess.end) {
2362
+ static run(scroller, { error } = {}) {
2363
+ const { workflow, state: { cycle: { interrupter } } } = scroller;
2364
+ if (!error && !interrupter) {
2365
+ // set out params accessible via Adapter
2366
+ End.calculateParams(scroller, workflow);
2367
+ }
2368
+ // explicit interruption for we don't want to go through the inner loop finalizing
2369
+ if (isInterrupted(workflow)) {
2370
+ workflow.call({ process: End.process, status: ProcessStatus.done });
2371
+ return;
2372
+ }
2373
+ const next = End.shouldContinueRun(scroller, error);
2374
+ scroller.state.endInnerLoop();
2375
+ workflow.call({
2376
+ process: End.process,
2377
+ status: next ? ProcessStatus.next : ProcessStatus.done,
2378
+ payload: Object.assign({}, (interrupter ? { process: interrupter } : {}))
2379
+ });
2380
+ }
2381
+ static calculateParams(scroller, workflow) {
2382
+ const { adapter, viewport, buffer: { items } } = scroller;
2383
+ if (adapter.wanted.firstVisible) {
2384
+ const { item } = viewport.getEdgeVisibleItem(items, Direction.backward);
2385
+ if (!item || item.element !== adapter.firstVisible.element) {
2386
+ adapter.firstVisible = item ? item.get() : EMPTY_ITEM;
2387
+ }
2388
+ }
2389
+ // the workflow can be interrupter on firstVisible change
2390
+ if (adapter.wanted.lastVisible && !isInterrupted(workflow)) {
2391
+ const { item } = viewport.getEdgeVisibleItem(items, Direction.forward);
2392
+ if (!item || item.element !== adapter.lastVisible.element) {
2393
+ adapter.lastVisible = item ? item.get() : EMPTY_ITEM;
2394
+ }
2395
+ }
2396
+ }
2397
+ static shouldContinueRun(scroller, error) {
2398
+ const { cycle, fetch, render } = scroller.state;
2399
+ // Adapter.reload or Adapter.reset
2400
+ if (cycle.interrupter) {
2401
+ return true;
2402
+ }
2403
+ // critical error
2404
+ if (error) {
2405
+ return false;
2406
+ }
2407
+ // Adapter.check
2408
+ if (fetch.simulate && fetch.isCheck && !render.noSize) {
2409
+ return true;
2410
+ }
2411
+ // Adapter.remove or Adapter.update with clip
2412
+ if (fetch.simulate && fetch.doRemove) {
2413
+ return true;
2414
+ }
2415
+ // common inner loop (App start, scroll, Adapter.clip) with full fetch
2416
+ if (!fetch.simulate && ((fetch.hasNewItems && !render.noSize) || fetch.hasAnotherPack)) {
2417
+ return true;
2418
+ }
2419
+ return false;
2420
+ }
2421
+ }
2422
+
2360
2423
  class Adjust extends BaseProcessFactory(CommonProcess.adjust) {
2361
2424
  static run(scroller) {
2362
- const { workflow, viewport, state: { scrollState } } = scroller;
2363
- scrollState.positionBeforeAdjust = viewport.scrollPosition;
2425
+ const { workflow, viewport, state: { scroll } } = scroller;
2426
+ scroll.positionBeforeAdjust = viewport.scrollPosition;
2364
2427
  Adjust.setPaddings(scroller);
2365
- scrollState.positionAfterAdjust = viewport.scrollPosition;
2428
+ scroll.positionAfterAdjust = viewport.scrollPosition;
2366
2429
  // scroll position adjustments
2367
2430
  const position = Adjust.calculatePosition(scroller);
2431
+ // additional adjustment if the position can't be reached during the initial cycle
2432
+ Adjust.setAdditionalForwardPadding(scroller, position);
2368
2433
  // set new position using animation frame
2369
2434
  Adjust.setPosition(scroller, position, () => workflow.call({
2370
2435
  process: Adjust.process,
@@ -2395,7 +2460,8 @@ class Adjust extends BaseProcessFactory(CommonProcess.adjust) {
2395
2460
  }
2396
2461
  // lack of items case
2397
2462
  const bufferSize = viewport.getScrollableSize() - forward.size - backward.size;
2398
- const viewportSizeDiff = viewport.getSize() - (bwdSize + bufferSize + fwdSize);
2463
+ const scrollSize = bwdSize + bufferSize + fwdSize;
2464
+ const viewportSizeDiff = viewport.getSize() - scrollSize;
2399
2465
  if (viewportSizeDiff > 0) {
2400
2466
  if (inverse) {
2401
2467
  bwdSize += viewportSizeDiff;
@@ -2410,7 +2476,7 @@ class Adjust extends BaseProcessFactory(CommonProcess.adjust) {
2410
2476
  scroller.logger.stat('after paddings adjustments');
2411
2477
  }
2412
2478
  static calculatePosition(scroller) {
2413
- const { viewport, buffer, state: { fetch, render, scrollState } } = scroller;
2479
+ const { viewport, buffer, state: { fetch, render, scroll } } = scroller;
2414
2480
  let position = viewport.paddings.backward.size;
2415
2481
  // increase the position to meet the expectation of the first visible item
2416
2482
  if (!isNaN(fetch.firstVisible.index) && !isNaN(buffer.firstIndex)) {
@@ -2433,8 +2499,8 @@ class Adjust extends BaseProcessFactory(CommonProcess.adjust) {
2433
2499
  });
2434
2500
  }
2435
2501
  // slow fetch/render case
2436
- if (scrollState.positionBeforeAsync !== null) {
2437
- const diff = render.positionBefore - scrollState.positionBeforeAsync;
2502
+ if (scroll.positionBeforeAsync !== null) {
2503
+ const diff = render.positionBefore - scroll.positionBeforeAsync;
2438
2504
  if (diff !== 0) {
2439
2505
  scroller.logger.log(`shift position due to fetch-render difference (${diff})`);
2440
2506
  position += diff;
@@ -2446,22 +2512,47 @@ class Adjust extends BaseProcessFactory(CommonProcess.adjust) {
2446
2512
  }
2447
2513
  return Math.round(position);
2448
2514
  }
2515
+ static setAdditionalForwardPadding(scroller, position) {
2516
+ const { viewport, buffer, state: { cycle } } = scroller;
2517
+ if (!cycle.isInitial || !End.shouldContinueRun(scroller, null)) {
2518
+ return;
2519
+ }
2520
+ const diff = position - viewport.getMaxScrollPosition();
2521
+ if (diff <= 0) {
2522
+ return;
2523
+ }
2524
+ const last = buffer.getLastVisibleItem();
2525
+ if (!last) {
2526
+ return;
2527
+ }
2528
+ let size = 0;
2529
+ let index = last.$index + 1;
2530
+ while (size <= diff && index <= buffer.absMaxIndex) {
2531
+ size += buffer.getSizeByIndex(index++);
2532
+ }
2533
+ const shift = Math.min(size, diff);
2534
+ if (shift) {
2535
+ viewport.paddings.forward.size += shift;
2536
+ scroller.logger.log(`increase fwd padding due to lack of items (${diff} -> ${shift})`);
2537
+ }
2538
+ }
2449
2539
  static setPosition(scroller, position, done) {
2450
- const { state: { scrollState }, viewport } = scroller;
2451
- if (!scrollState.hasPositionChanged(position)) {
2540
+ const { state: { scroll }, viewport, routines } = scroller;
2541
+ if (!scroll.hasPositionChanged(position)) {
2452
2542
  return done();
2453
2543
  }
2454
- scrollState.syntheticPosition = position;
2455
- scrollState.syntheticFulfill = false;
2456
- scrollState.animationFrameId = requestAnimationFrame(() => {
2457
- const inertiaDiff = scrollState.positionAfterAdjust - viewport.scrollPosition;
2544
+ scroll.syntheticPosition = position;
2545
+ scroll.syntheticFulfill = false;
2546
+ scroll.cancelAnimation = routines.animate(() => {
2547
+ scroll.cancelAnimation = null;
2548
+ const inertiaDiff = scroll.positionAfterAdjust - viewport.scrollPosition;
2458
2549
  let diffLog = '';
2459
2550
  if (inertiaDiff > 0) {
2460
2551
  position -= inertiaDiff;
2461
- scrollState.syntheticPosition = position;
2552
+ scroll.syntheticPosition = position;
2462
2553
  diffLog = ` (-${inertiaDiff})`;
2463
2554
  }
2464
- scrollState.syntheticFulfill = true;
2555
+ scroll.syntheticFulfill = true;
2465
2556
  viewport.scrollPosition = position;
2466
2557
  scroller.logger.stat('after scroll adjustment' + diffLog);
2467
2558
  done();
@@ -2601,67 +2692,6 @@ class Clip extends BaseProcessFactory(CommonProcess.clip) {
2601
2692
  }
2602
2693
  }
2603
2694
 
2604
- const isInterrupted = ({ call }) => !!call.interrupted;
2605
- class End extends BaseProcessFactory(CommonProcess.end) {
2606
- static run(scroller, { error } = {}) {
2607
- const { workflow, state: { cycle: { interrupter } } } = scroller;
2608
- if (!error && !interrupter) {
2609
- // set out params accessible via Adapter
2610
- End.calculateParams(scroller, workflow);
2611
- }
2612
- // explicit interruption for we don't want to go through the inner loop finalizing
2613
- if (isInterrupted(workflow)) {
2614
- workflow.call({ process: End.process, status: ProcessStatus.done });
2615
- return;
2616
- }
2617
- const next = End.finalizeInnerLoop(scroller, error);
2618
- workflow.call({
2619
- process: End.process,
2620
- status: next ? ProcessStatus.next : ProcessStatus.done,
2621
- payload: Object.assign({}, (interrupter ? { process: interrupter } : {}))
2622
- });
2623
- }
2624
- static calculateParams(scroller, workflow) {
2625
- const { adapter, viewport, buffer: { items } } = scroller;
2626
- if (adapter.wanted.firstVisible) {
2627
- const { item } = viewport.getEdgeVisibleItem(items, Direction.backward);
2628
- if (!item || item.element !== adapter.firstVisible.element) {
2629
- adapter.firstVisible = item ? item.get() : EMPTY_ITEM;
2630
- }
2631
- }
2632
- // the workflow can be interrupter on firstVisible change
2633
- if (adapter.wanted.lastVisible && !isInterrupted(workflow)) {
2634
- const { item } = viewport.getEdgeVisibleItem(items, Direction.forward);
2635
- if (!item || item.element !== adapter.lastVisible.element) {
2636
- adapter.lastVisible = item ? item.get() : EMPTY_ITEM;
2637
- }
2638
- }
2639
- }
2640
- static finalizeInnerLoop(scroller, error) {
2641
- const { state, state: { cycle, clip, fetch } } = scroller;
2642
- const next = !!cycle.interrupter || (error ? false : End.getNext(scroller));
2643
- cycle.innerLoop.isInitial = false;
2644
- fetch.stopSimulate();
2645
- clip.reset(true);
2646
- state.endInnerLoop();
2647
- return next;
2648
- }
2649
- static getNext(scroller) {
2650
- const { state: { fetch, render } } = scroller;
2651
- if (fetch.simulate && fetch.isCheck && !render.noSize) { // Adapter.check
2652
- return true;
2653
- }
2654
- if (fetch.simulate && fetch.doRemove) { // Adapter.remove or Adapter.update with clip
2655
- return true;
2656
- }
2657
- if ( // common inner loop (App start, Scroll, Adapter.clip) accompanied by fetch
2658
- !fetch.simulate && ((fetch.hasNewItems && !render.noSize) || fetch.hasAnotherPack)) {
2659
- return true;
2660
- }
2661
- return false;
2662
- }
2663
- }
2664
-
2665
2695
  class Logger {
2666
2696
  constructor(scroller, packageInfo, adapter) {
2667
2697
  this.logs = [];
@@ -2711,7 +2741,7 @@ class Logger {
2711
2741
  this.getLoopId = () => scroller.state.cycle.loopId;
2712
2742
  this.getLoopIdNext = () => scroller.state.cycle.loopIdNext;
2713
2743
  this.getWorkflowCycleData = () => `${settings.instanceIndex}-${scroller.state.cycle.count}`;
2714
- this.getScrollPosition = (element) => scroller.routines.getScrollPosition(element);
2744
+ this.getScrollPosition = () => scroller.routines.getScrollPosition();
2715
2745
  this.log(() => 'vscroll Workflow has been started, ' +
2716
2746
  `core: ${packageInfo.core.name} v${packageInfo.core.version}, ` +
2717
2747
  `consumer: ${packageInfo.consumer.name} v${packageInfo.consumer.version}, ` +
@@ -2767,7 +2797,7 @@ class Logger {
2767
2797
  }
2768
2798
  prepareForLog(data) {
2769
2799
  return data instanceof Event && data.target
2770
- ? this.getScrollPosition(data.target)
2800
+ ? this.getScrollPosition()
2771
2801
  : data;
2772
2802
  }
2773
2803
  logProcess(data) {
@@ -2849,64 +2879,73 @@ class Logger {
2849
2879
  }
2850
2880
 
2851
2881
  class Routines {
2852
- constructor(settings) {
2853
- this.horizontal = settings.horizontal;
2854
- this.window = settings.windowViewport;
2855
- this.viewport = settings.viewport;
2882
+ constructor(element, settings, CustomRoutines) {
2883
+ this.element = element;
2884
+ this.settings = {
2885
+ viewport: settings.viewport,
2886
+ horizontal: settings.horizontal,
2887
+ window: settings.windowViewport
2888
+ };
2889
+ // provide custom overrides for IRoutines methods
2890
+ if (CustomRoutines) {
2891
+ const routines = new CustomRoutines(element, this.settings);
2892
+ Object.getOwnPropertyNames(Object.getPrototypeOf(routines))
2893
+ .filter(method => method !== 'constructor' &&
2894
+ typeof routines[method] === 'function' &&
2895
+ typeof this[method] === 'function')
2896
+ .forEach(method => this[method] = (...args) => routines[method].apply(this, args));
2897
+ }
2898
+ // initialization
2899
+ this.viewport = this.getViewportElement();
2900
+ this.onInit(settings);
2856
2901
  }
2857
2902
  checkElement(element) {
2858
2903
  if (!element) {
2859
2904
  throw new Error('HTML element is not defined');
2860
2905
  }
2861
2906
  }
2862
- getHostElement(element) {
2863
- if (this.window) {
2907
+ getViewportElement() {
2908
+ if (this.settings.window) {
2864
2909
  return document.documentElement;
2865
2910
  }
2866
- if (this.viewport) {
2867
- return this.viewport;
2911
+ if (this.settings.viewport) {
2912
+ return this.settings.viewport;
2868
2913
  }
2869
- this.checkElement(element);
2870
- const parent = element.parentElement;
2914
+ this.checkElement(this.element);
2915
+ const parent = this.element.parentElement;
2871
2916
  this.checkElement(parent);
2872
2917
  return parent;
2873
2918
  }
2874
- getScrollEventReceiver(element) {
2875
- if (this.window) {
2876
- return window;
2919
+ onInit(settings) {
2920
+ if (settings.windowViewport) {
2921
+ if ('scrollRestoration' in history) {
2922
+ history.scrollRestoration = 'manual';
2923
+ }
2877
2924
  }
2878
- return this.getHostElement(element);
2879
- }
2880
- setupScrollRestoration() {
2881
- if ('scrollRestoration' in history) {
2882
- history.scrollRestoration = 'manual';
2925
+ if (settings.dismissOverflowAnchor) {
2926
+ this.viewport.style.overflowAnchor = 'none';
2883
2927
  }
2884
2928
  }
2885
- dismissOverflowAnchor(element) {
2886
- this.checkElement(element);
2887
- element.style.overflowAnchor = 'none';
2888
- }
2889
2929
  findElementBySelector(element, selector) {
2890
2930
  this.checkElement(element);
2891
2931
  return element.querySelector(selector);
2892
2932
  }
2893
- findPaddingElement(element, direction) {
2894
- return this.findElementBySelector(element, `[data-padding-${direction}]`);
2933
+ findPaddingElement(direction) {
2934
+ return this.findElementBySelector(this.element, `[data-padding-${direction}]`);
2895
2935
  }
2896
- findItemElement(element, id) {
2897
- return this.findElementBySelector(element, `[data-sid="${id}"]`);
2936
+ findItemElement(id) {
2937
+ return this.findElementBySelector(this.element, `[data-sid="${id}"]`);
2898
2938
  }
2899
- getScrollPosition(element) {
2900
- if (this.window) {
2901
- return window.pageYOffset;
2939
+ getScrollPosition() {
2940
+ if (this.settings.window) {
2941
+ return this.settings.horizontal ? window.pageXOffset : window.pageYOffset;
2902
2942
  }
2903
- this.checkElement(element);
2904
- return element[this.horizontal ? 'scrollLeft' : 'scrollTop'];
2943
+ return this.viewport[this.settings.horizontal ? 'scrollLeft' : 'scrollTop'];
2905
2944
  }
2906
- setScrollPosition(element, value) {
2945
+ setScrollPosition(value) {
2907
2946
  value = Math.max(0, value);
2908
- if (this.window) {
2909
- if (this.horizontal) {
2947
+ if (this.settings.window) {
2948
+ if (this.settings.horizontal) {
2910
2949
  window.scrollTo(value, window.scrollY);
2911
2950
  }
2912
2951
  else {
@@ -2914,49 +2953,62 @@ class Routines {
2914
2953
  }
2915
2954
  return;
2916
2955
  }
2917
- this.checkElement(element);
2918
- element[this.horizontal ? 'scrollLeft' : 'scrollTop'] = value;
2956
+ this.viewport[this.settings.horizontal ? 'scrollLeft' : 'scrollTop'] = value;
2919
2957
  }
2920
- getParams(element, doNotBind) {
2958
+ getElementParams(element) {
2921
2959
  this.checkElement(element);
2922
- if (this.window && doNotBind) {
2923
- const { clientWidth, clientHeight, clientLeft, clientTop } = element;
2924
- return {
2925
- 'height': clientHeight,
2926
- 'width': clientWidth,
2927
- 'top': clientTop,
2928
- 'bottom': clientTop + clientHeight,
2929
- 'left': clientLeft,
2930
- 'right': clientLeft + clientWidth,
2931
- 'x': clientLeft,
2932
- 'y': clientTop,
2933
- 'toJSON': () => null,
2934
- };
2935
- }
2936
2960
  return element.getBoundingClientRect();
2937
2961
  }
2938
- getSize(element, doNotBind) {
2939
- return this.getParams(element, doNotBind)[this.horizontal ? 'width' : 'height'];
2962
+ getWindowParams() {
2963
+ const { clientWidth, clientHeight, clientLeft, clientTop } = this.viewport;
2964
+ return {
2965
+ 'height': clientHeight,
2966
+ 'width': clientWidth,
2967
+ 'top': clientTop,
2968
+ 'bottom': clientTop + clientHeight,
2969
+ 'left': clientLeft,
2970
+ 'right': clientLeft + clientWidth,
2971
+ 'x': clientLeft,
2972
+ 'y': clientTop,
2973
+ 'toJSON': () => null,
2974
+ };
2975
+ }
2976
+ getSize(element) {
2977
+ return this.getElementParams(element)[this.settings.horizontal ? 'width' : 'height'];
2978
+ }
2979
+ getScrollerSize() {
2980
+ return this.getElementParams(this.element)[this.settings.horizontal ? 'width' : 'height'];
2981
+ }
2982
+ getViewportSize() {
2983
+ if (this.settings.window) {
2984
+ return this.getWindowParams()[this.settings.horizontal ? 'width' : 'height'];
2985
+ }
2986
+ return this.getSize(this.viewport);
2940
2987
  }
2941
2988
  getSizeStyle(element) {
2942
2989
  this.checkElement(element);
2943
- const size = element.style[this.horizontal ? 'width' : 'height'];
2990
+ const size = element.style[this.settings.horizontal ? 'width' : 'height'];
2944
2991
  return parseFloat(size) || 0;
2945
2992
  }
2946
2993
  setSizeStyle(element, value) {
2947
2994
  this.checkElement(element);
2948
2995
  value = Math.max(0, Math.round(value));
2949
- element.style[this.horizontal ? 'width' : 'height'] = `${value}px`;
2996
+ element.style[this.settings.horizontal ? 'width' : 'height'] = `${value}px`;
2950
2997
  }
2951
- getEdge(element, direction, doNotBind) {
2952
- const params = this.getParams(element, doNotBind);
2998
+ getEdge(element, direction) {
2999
+ const { horizontal } = this.settings;
3000
+ const params = this.getElementParams(element);
2953
3001
  const isFwd = direction === Direction.forward;
2954
- return params[isFwd ? (this.horizontal ? 'right' : 'bottom') : (this.horizontal ? 'left' : 'top')];
3002
+ return params[isFwd ? (horizontal ? 'right' : 'bottom') : (horizontal ? 'left' : 'top')];
2955
3003
  }
2956
- getEdge2(element, direction, relativeElement, opposite) {
2957
- // vertical only ?
2958
- return element.offsetTop - (relativeElement ? relativeElement.scrollTop : 0) +
2959
- (direction === (!opposite ? Direction.forward : Direction.backward) ? this.getSize(element) : 0);
3004
+ getViewportEdge(direction) {
3005
+ const { window, horizontal } = this.settings;
3006
+ if (window) {
3007
+ const params = this.getWindowParams();
3008
+ const isFwd = direction === Direction.forward;
3009
+ return params[isFwd ? (horizontal ? 'right' : 'bottom') : (horizontal ? 'left' : 'top')];
3010
+ }
3011
+ return this.getEdge(this.viewport, direction);
2960
3012
  }
2961
3013
  makeElementVisible(element) {
2962
3014
  this.checkElement(element);
@@ -2968,19 +3020,32 @@ class Routines {
2968
3020
  this.checkElement(element);
2969
3021
  element.style.display = 'none';
2970
3022
  }
2971
- getOffset(element) {
2972
- this.checkElement(element);
2973
- return (this.horizontal ? element.offsetLeft : element.offsetTop) || 0;
3023
+ getOffset() {
3024
+ const get = (element) => (this.settings.horizontal ? element.offsetLeft : element.offsetTop) || 0;
3025
+ return get(this.element) - (!this.settings.window ? get(this.viewport) : 0);
2974
3026
  }
2975
3027
  scrollTo(element, argument) {
2976
3028
  this.checkElement(element);
2977
3029
  element.scrollIntoView(argument);
2978
3030
  }
3031
+ render(cb) {
3032
+ const timeoutId = setTimeout(() => cb());
3033
+ return () => clearTimeout(timeoutId);
3034
+ }
3035
+ animate(cb) {
3036
+ const animationFrameId = requestAnimationFrame(() => cb());
3037
+ return () => cancelAnimationFrame(animationFrameId);
3038
+ }
3039
+ onScroll(handler) {
3040
+ const eventReceiver = this.settings.window ? window : this.viewport;
3041
+ eventReceiver.addEventListener('scroll', handler);
3042
+ return () => eventReceiver.removeEventListener('scroll', handler);
3043
+ }
2979
3044
  }
2980
3045
 
2981
3046
  class Padding {
2982
- constructor(element, direction, routines) {
2983
- const found = routines.findPaddingElement(element, direction);
3047
+ constructor(direction, routines) {
3048
+ const found = routines.findPaddingElement(direction);
2984
3049
  routines.checkElement(found);
2985
3050
  this.element = found;
2986
3051
  this.direction = direction;
@@ -2997,10 +3062,10 @@ class Padding {
2997
3062
  }
2998
3063
  }
2999
3064
  class Paddings {
3000
- constructor(element, routines, settings) {
3065
+ constructor(routines, settings) {
3001
3066
  this.settings = settings;
3002
- this.forward = new Padding(element, Direction.forward, routines);
3003
- this.backward = new Padding(element, Direction.backward, routines);
3067
+ this.forward = new Padding(Direction.forward, routines);
3068
+ this.backward = new Padding(Direction.backward, routines);
3004
3069
  }
3005
3070
  byDirection(direction, opposite) {
3006
3071
  return direction === Direction.backward
@@ -3051,27 +3116,18 @@ class Paddings {
3051
3116
  }
3052
3117
 
3053
3118
  class Viewport {
3054
- constructor(element, settings, routines, state, logger) {
3055
- this.element = element;
3119
+ constructor(settings, routines, state, logger) {
3056
3120
  this.settings = settings;
3057
3121
  this.routines = routines;
3058
3122
  this.state = state;
3059
3123
  this.logger = logger;
3060
- this.hostElement = this.routines.getHostElement(this.element);
3061
- this.scrollEventReceiver = this.routines.getScrollEventReceiver(this.element);
3062
- if (settings.windowViewport) {
3063
- this.routines.setupScrollRestoration();
3064
- }
3065
- if (settings.dismissOverflowAnchor) {
3066
- this.routines.dismissOverflowAnchor(this.hostElement);
3067
- }
3068
- this.paddings = new Paddings(this.element, this.routines, settings);
3124
+ this.paddings = new Paddings(this.routines, settings);
3069
3125
  }
3070
3126
  reset(startIndex) {
3071
3127
  this.setOffset();
3072
3128
  this.paddings.reset(this.getSize(), startIndex, this.offset);
3073
3129
  this.scrollPosition = this.paddings.backward.size || 0;
3074
- this.state.scrollState.reset();
3130
+ this.state.scroll.reset();
3075
3131
  }
3076
3132
  setPosition(value) {
3077
3133
  const oldPosition = this.scrollPosition;
@@ -3079,7 +3135,7 @@ class Viewport {
3079
3135
  this.logger.log(() => ['setting scroll position at', value, '[cancelled]']);
3080
3136
  return value;
3081
3137
  }
3082
- this.routines.setScrollPosition(this.hostElement, value);
3138
+ this.routines.setScrollPosition(value);
3083
3139
  const position = this.scrollPosition;
3084
3140
  this.logger.log(() => [
3085
3141
  'setting scroll position at', position, ...(position !== value ? [`(${value})`] : [])
@@ -3087,31 +3143,31 @@ class Viewport {
3087
3143
  return position;
3088
3144
  }
3089
3145
  get scrollPosition() {
3090
- return this.routines.getScrollPosition(this.hostElement);
3146
+ return this.routines.getScrollPosition();
3091
3147
  }
3092
3148
  set scrollPosition(value) {
3093
3149
  this.setPosition(value);
3094
3150
  }
3095
3151
  getSize() {
3096
- return this.routines.getSize(this.hostElement, true);
3152
+ return this.routines.getViewportSize();
3097
3153
  }
3098
3154
  getScrollableSize() {
3099
- return this.routines.getSize(this.element);
3155
+ return this.routines.getScrollerSize();
3156
+ }
3157
+ getMaxScrollPosition() {
3158
+ return this.getScrollableSize() - this.getSize();
3100
3159
  }
3101
3160
  getBufferPadding() {
3102
3161
  return this.getSize() * this.settings.padding;
3103
3162
  }
3104
3163
  getEdge(direction) {
3105
- return this.routines.getEdge(this.hostElement, direction, true);
3164
+ return this.routines.getViewportEdge(direction);
3106
3165
  }
3107
3166
  setOffset() {
3108
- this.offset = this.routines.getOffset(this.element);
3109
- if (!this.settings.windowViewport) {
3110
- this.offset -= this.routines.getOffset(this.hostElement);
3111
- }
3167
+ this.offset = this.routines.getOffset();
3112
3168
  }
3113
3169
  findItemElementById(id) {
3114
- return this.routines.findItemElement(this.element, id);
3170
+ return this.routines.findItemElement(id);
3115
3171
  }
3116
3172
  getEdgeVisibleItem(items, direction) {
3117
3173
  const bwd = direction === Direction.backward;
@@ -3903,6 +3959,7 @@ class InnerLoopModel {
3903
3959
  return this.count === 0;
3904
3960
  }
3905
3961
  done() {
3962
+ this.isInitial = false;
3906
3963
  this.count++;
3907
3964
  this.total++;
3908
3965
  this.busy.set(false);
@@ -3922,7 +3979,7 @@ class WorkflowCycleModel {
3922
3979
  this.innerLoop = new InnerLoopModel(loopCount);
3923
3980
  this.interrupter = null;
3924
3981
  this.busy = new Reactive(false);
3925
- this.done(cycleCount);
3982
+ this.end(cycleCount);
3926
3983
  }
3927
3984
  get loopId() {
3928
3985
  return `${this.instanceIndex}-${this.count}-${this.innerLoop.total}`;
@@ -3930,11 +3987,6 @@ class WorkflowCycleModel {
3930
3987
  get loopIdNext() {
3931
3988
  return `${this.instanceIndex}-${this.count}-${this.innerLoop.total + 1}`;
3932
3989
  }
3933
- done(count) {
3934
- this.count = count;
3935
- this.isInitial = false;
3936
- this.busy.set(false);
3937
- }
3938
3990
  start(isInitial, initiator) {
3939
3991
  this.isInitial = isInitial;
3940
3992
  this.initiator = initiator;
@@ -3943,6 +3995,11 @@ class WorkflowCycleModel {
3943
3995
  this.interrupter = null;
3944
3996
  this.busy.set(true);
3945
3997
  }
3998
+ end(count) {
3999
+ this.count = count;
4000
+ this.isInitial = false;
4001
+ this.busy.set(false);
4002
+ }
3946
4003
  dispose(forever) {
3947
4004
  if (forever) {
3948
4005
  // otherwise the value will be persisted during re-instantiation
@@ -4102,11 +4159,11 @@ class RenderModel {
4102
4159
  this.sizeBefore = 0;
4103
4160
  this.sizeAfter = 0;
4104
4161
  this.positionBefore = 0;
4105
- this.renderTimer = null;
4162
+ this.cancel = null;
4106
4163
  }
4107
4164
  }
4108
4165
 
4109
- class ScrollState {
4166
+ class ScrollModel {
4110
4167
  constructor() {
4111
4168
  this.reset();
4112
4169
  }
@@ -4118,16 +4175,16 @@ class ScrollState {
4118
4175
  this.positionBeforeAsync = null;
4119
4176
  this.positionBeforeAdjust = null;
4120
4177
  this.positionAfterAdjust = null;
4121
- this.cleanupTimers();
4178
+ this.stop();
4122
4179
  }
4123
- cleanupTimers() {
4180
+ stop() {
4124
4181
  if (this.scrollTimer) {
4125
4182
  clearTimeout(this.scrollTimer);
4126
4183
  this.scrollTimer = null;
4127
4184
  }
4128
- if (this.animationFrameId) {
4129
- cancelAnimationFrame(this.animationFrameId);
4130
- this.animationFrameId = 0;
4185
+ if (this.cancelAnimation) {
4186
+ this.cancelAnimation();
4187
+ this.cancelAnimation = null;
4131
4188
  }
4132
4189
  }
4133
4190
  hasPositionChanged(position) {
@@ -4146,25 +4203,19 @@ class State {
4146
4203
  this.fetch = new FetchModel(settings.directionPriority);
4147
4204
  this.clip = new ClipModel();
4148
4205
  this.render = new RenderModel();
4149
- this.scrollState = new ScrollState();
4206
+ this.scroll = new ScrollModel();
4150
4207
  }
4151
4208
  get time() {
4152
4209
  return Number(new Date()) - this.initTime;
4153
4210
  }
4154
- endInnerLoop() {
4155
- const { fetch, render, cycle } = this;
4156
- if (fetch.cancel) {
4157
- fetch.cancel();
4158
- fetch.cancel = null;
4159
- }
4160
- if (render.renderTimer) {
4161
- clearTimeout(render.renderTimer);
4162
- render.renderTimer = null;
4163
- }
4164
- cycle.innerLoop.done();
4211
+ startWorkflowCycle(isInitial, initiator) {
4212
+ this.cycle.start(isInitial, initiator);
4213
+ }
4214
+ endWorkflowCycle(count) {
4215
+ this.cycle.end(count);
4165
4216
  }
4166
4217
  startInnerLoop() {
4167
- const { cycle, scrollState: scroll, fetch, render, clip } = this;
4218
+ const { cycle, scroll: scroll, fetch, render, clip } = this;
4168
4219
  cycle.innerLoop.start();
4169
4220
  scroll.positionBeforeAsync = null;
4170
4221
  if (!fetch.simulate) {
@@ -4177,10 +4228,24 @@ class State {
4177
4228
  doRender: fetch.simulate && fetch.items.length > 0
4178
4229
  } : {}));
4179
4230
  }
4231
+ endInnerLoop() {
4232
+ const { fetch, clip, render, cycle } = this;
4233
+ fetch.stopSimulate();
4234
+ clip.reset(true);
4235
+ if (fetch.cancel) {
4236
+ fetch.cancel();
4237
+ fetch.cancel = null;
4238
+ }
4239
+ if (render.cancel) {
4240
+ render.cancel();
4241
+ render.cancel = null;
4242
+ }
4243
+ cycle.innerLoop.done();
4244
+ }
4180
4245
  dispose() {
4246
+ this.scroll.stop();
4181
4247
  this.cycle.dispose();
4182
4248
  this.endInnerLoop();
4183
- this.scrollState.cleanupTimers();
4184
4249
  }
4185
4250
  }
4186
4251
 
@@ -4591,21 +4656,21 @@ class Adapter {
4591
4656
  const INVALID_DATASOURCE_PREFIX = 'Invalid datasource:';
4592
4657
  let instanceCount = 0;
4593
4658
  class Scroller {
4594
- constructor({ datasource, consumer, element, workflow, scroller }) {
4659
+ constructor({ datasource, consumer, element, workflow, Routines: CustomRoutines, scroller }) {
4595
4660
  const { params: { get } } = validate(datasource, DATASOURCE);
4596
4661
  if (!get.isValid) {
4597
4662
  throw new Error(`${INVALID_DATASOURCE_PREFIX} ${get.errors[0]}`);
4598
4663
  }
4599
4664
  const packageInfo = scroller ? scroller.state.packageInfo : { consumer, core };
4600
- element = scroller ? scroller.viewport.element : element;
4665
+ element = scroller ? scroller.routines.element : element;
4601
4666
  workflow = scroller ? scroller.workflow : workflow;
4602
4667
  this.workflow = workflow;
4603
4668
  this.settings = new Settings(datasource.settings, datasource.devSettings, ++instanceCount);
4604
4669
  this.logger = new Logger(this, packageInfo, datasource.adapter);
4605
- this.routines = new Routines(this.settings);
4670
+ this.routines = new Routines(element, this.settings, CustomRoutines);
4606
4671
  this.state = new State(packageInfo, this.settings, scroller ? scroller.state : void 0);
4607
4672
  this.buffer = new Buffer(this.settings, workflow.onDataChanged, this.logger);
4608
- this.viewport = new Viewport(element, this.settings, this.routines, this.state, this.logger);
4673
+ this.viewport = new Viewport(this.settings, this.routines, this.state, this.logger);
4609
4674
  this.logger.object('vscroll settings object', this.settings, true);
4610
4675
  this.initDatasource(datasource, scroller);
4611
4676
  }
@@ -4859,14 +4924,14 @@ const runStateMachine = ({ input: { process, status, payload = {} }, methods: {
4859
4924
  };
4860
4925
 
4861
4926
  class Workflow {
4862
- constructor({ element, datasource, consumer, run }) {
4927
+ constructor({ element, datasource, consumer, run, Routines }) {
4863
4928
  this.isInitialized = false;
4864
4929
  this.initTimer = null;
4865
4930
  this.adapterRun$ = new Reactive();
4866
4931
  this.cyclesDone = 0;
4867
4932
  this.interruptionCount = 0;
4868
4933
  this.errors = [];
4869
- this.disposeScrollEventHandler = () => null;
4934
+ this.offScroll = () => null;
4870
4935
  this.propagateChanges = run;
4871
4936
  this.stateMachineMethods = {
4872
4937
  run: this.runProcess(),
@@ -4874,7 +4939,9 @@ class Workflow {
4874
4939
  done: this.done.bind(this),
4875
4940
  onError: this.onError.bind(this)
4876
4941
  };
4877
- this.scroller = new Scroller({ element, datasource, consumer, workflow: this.getUpdater() });
4942
+ this.scroller = new Scroller({
4943
+ element, datasource, consumer, workflow: this.getUpdater(), Routines
4944
+ });
4878
4945
  if (this.scroller.settings.initializeDelay) {
4879
4946
  this.initTimer = setTimeout(() => {
4880
4947
  this.initTimer = null;
@@ -4894,14 +4961,13 @@ class Workflow {
4894
4961
  status: ProcessStatus.start
4895
4962
  });
4896
4963
  // set up scroll event listener
4897
- const { scrollEventReceiver } = this.scroller.viewport;
4964
+ const { routines } = this.scroller;
4898
4965
  const onScrollHandler = event => this.callWorkflow({
4899
4966
  process: CommonProcess.scroll,
4900
4967
  status: ProcessStatus.start,
4901
4968
  payload: { event }
4902
4969
  });
4903
- scrollEventReceiver.addEventListener('scroll', onScrollHandler);
4904
- this.disposeScrollEventHandler = () => scrollEventReceiver.removeEventListener('scroll', onScrollHandler);
4970
+ this.offScroll = routines.onScroll(onScrollHandler);
4905
4971
  }
4906
4972
  changeItems(items) {
4907
4973
  this.propagateChanges(items);
@@ -4987,14 +5053,14 @@ class Workflow {
4987
5053
  const { state, logger } = this.scroller;
4988
5054
  this.cyclesDone++;
4989
5055
  logger.logCycle(false);
4990
- state.cycle.done(this.cyclesDone + 1);
5056
+ state.endWorkflowCycle(this.cyclesDone + 1);
4991
5057
  this.finalize();
4992
5058
  }
4993
5059
  dispose() {
4994
5060
  if (this.initTimer) {
4995
5061
  clearTimeout(this.initTimer);
4996
5062
  }
4997
- this.disposeScrollEventHandler();
5063
+ this.offScroll();
4998
5064
  this.adapterRun$.dispose();
4999
5065
  this.scroller.dispose(true);
5000
5066
  Object.getOwnPropertyNames(this).forEach(prop => {