|
43 | 43 | BEHIND: 'BEHIND' // scroll down or right.
|
44 | 44 |
|
45 | 45 | };
|
46 |
| - var SIZE_TYPE = { |
| 46 | + var CALC_TYPE = { |
47 | 47 | INIT: 'INIT',
|
48 | 48 | FIXED: 'FIXED',
|
49 | 49 | DYNAMIC: 'DYNAMIC'
|
50 | 50 | };
|
| 51 | + var LEADING_BUFFER = 1; |
51 | 52 |
|
52 | 53 | var Virtual = /*#__PURE__*/function () {
|
53 | 54 | function Virtual(param, updateHook) {
|
|
64 | 65 | this.updateHook = updateHook; // size data.
|
65 | 66 |
|
66 | 67 | this.sizes = new Map();
|
67 |
| - this.caches = new Map(); |
68 | 68 | this.firstRangeTotalSize = 0;
|
69 | 69 | this.firstRangeAverageSize = 0;
|
70 |
| - this.lastCalculatedIndex = 0; |
71 |
| - this.sizeType = SIZE_TYPE.INIT; |
72 |
| - this.sizeTypeValue = 0; // scroll data. |
| 70 | + this.lastCalcIndex = 0; |
| 71 | + this.fixedSizeValue = 0; |
| 72 | + this.calcType = CALC_TYPE.INIT; // scroll data. |
73 | 73 |
|
74 | 74 | this.offset = 0;
|
75 | 75 | this.direction = ''; // range data.
|
|
79 | 79 | if (this.param && !this.param.disabled) {
|
80 | 80 | this.checkRange(0, param.keeps - 1);
|
81 | 81 | } // benchmark test data.
|
| 82 | + // this.__bsearchCalls = 0 |
| 83 | + // this.__getIndexOffsetCalls = 0 |
82 | 84 |
|
83 |
| - |
84 |
| - this.__bsearchCalls = 0; |
85 |
| - this.__getIndexOffsetCalls = 0; |
86 |
| - this.__getIndexOffsetCacheHits = 0; |
87 | 85 | }
|
88 | 86 | }, {
|
89 | 87 | key: "destroy",
|
|
100 | 98 | range.padFront = this.range.padFront;
|
101 | 99 | range.padBehind = this.range.padBehind;
|
102 | 100 | return range;
|
| 101 | + } |
| 102 | + }, { |
| 103 | + key: "isLower", |
| 104 | + value: function isLower() { |
| 105 | + return this.direction === DIRECTION_TYPE.BEHIND; |
| 106 | + } |
| 107 | + }, { |
| 108 | + key: "isUpper", |
| 109 | + value: function isUpper() { |
| 110 | + return this.direction === DIRECTION_TYPE.FRONT; |
103 | 111 | } // return start index offset.
|
104 | 112 |
|
105 | 113 | }, {
|
|
122 | 130 | // if there is no size value different from this at next comming saving
|
123 | 131 | // we think it's a fixed size list, otherwise is dynamic size list.
|
124 | 132 |
|
125 |
| - if (this.sizeType === SIZE_TYPE.INIT) { |
126 |
| - this.sizeTypeValue = size; |
127 |
| - this.sizeType = SIZE_TYPE.FIXED; |
128 |
| - } else if (this.sizeType === SIZE_TYPE.FIXED && this.sizeTypeValue !== size) { |
129 |
| - this.sizeType = SIZE_TYPE.DYNAMIC; // it's no use at all. |
| 133 | + if (this.calcType === CALC_TYPE.INIT) { |
| 134 | + this.fixedSizeValue = size; |
| 135 | + this.calcType = CALC_TYPE.FIXED; |
| 136 | + } else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) { |
| 137 | + this.calcType = CALC_TYPE.DYNAMIC; // it's no use at all. |
130 | 138 |
|
131 |
| - delete this.sizeTypeValue; |
| 139 | + delete this.fixedSizeValue; |
132 | 140 | } // calculate the average size only in the first range.
|
133 | 141 |
|
134 | 142 |
|
|
139 | 147 | // it's done using.
|
140 | 148 | delete this.firstRangeTotalSize;
|
141 | 149 | }
|
142 |
| - } // when dataSources length change, we need to force update |
143 |
| - // just keep the same range and recalculate pad front and behind. |
| 150 | + } // in some special situation (e.g. length change) we need to update in a row |
| 151 | + // try goiong to render next range by a leading buffer according to current direction. |
144 | 152 |
|
145 | 153 | }, {
|
146 |
| - key: "handleDataSourcesLengthChange", |
147 |
| - value: function handleDataSourcesLengthChange() { |
148 |
| - this.updateRange(this.range.start, this.range.end); |
| 154 | + key: "handleDataSourcesChange", |
| 155 | + value: function handleDataSourcesChange() { |
| 156 | + var start = this.range.start; |
| 157 | + |
| 158 | + if (this.direction === DIRECTION_TYPE.FRONT) { |
| 159 | + start = start - LEADING_BUFFER; |
| 160 | + } else if (this.direction === DIRECTION_TYPE.BEHIND) { |
| 161 | + start = start + LEADING_BUFFER; |
| 162 | + } |
| 163 | + |
| 164 | + start = Math.max(start, 0); |
| 165 | + this.updateRange(start, this.getEndByStart(start)); |
149 | 166 | } // when slot size change, we also need force update.
|
150 | 167 |
|
151 | 168 | }, {
|
152 | 169 | key: "handleSlotSizeChange",
|
153 | 170 | value: function handleSlotSizeChange() {
|
154 |
| - this.handleDataSourcesLengthChange(); |
| 171 | + this.handleDataSourcesChange(); |
155 | 172 | } // calculating range on scroll.
|
156 | 173 |
|
157 | 174 | }, {
|
|
171 | 188 | }
|
172 | 189 | } // ----------- public method end. -----------
|
173 | 190 |
|
174 |
| - }, { |
175 |
| - key: "isFixedSize", |
176 |
| - value: function isFixedSize() { |
177 |
| - return this.sizeType === SIZE_TYPE.FIXED; |
178 |
| - } |
179 | 191 | }, {
|
180 | 192 | key: "handleFront",
|
181 | 193 | value: function handleFront() {
|
182 | 194 | var overs = this.getScrollOvers(); // should not change range if start doesn't exceed overs.
|
183 | 195 |
|
184 | 196 | if (overs > this.range.start) {
|
185 | 197 | return;
|
186 |
| - } // move up start by a buffer length. |
| 198 | + } // move up start by a buffer length, and make sure its safety. |
187 | 199 |
|
188 | 200 |
|
189 | 201 | var start = Math.max(overs - this.param.buffer, 0);
|
|
212 | 224 | } // if this list is fixed size, that can be easily.
|
213 | 225 |
|
214 | 226 |
|
215 |
| - if (this.isFixedSize()) { |
216 |
| - return Math.floor(offset / this.sizeTypeValue); |
| 227 | + if (this.isFixedType()) { |
| 228 | + return Math.floor(offset / this.fixedSizeValue); |
217 | 229 | }
|
218 | 230 |
|
219 | 231 | var low = 0;
|
|
222 | 234 | var high = this.param.uniqueIds.length;
|
223 | 235 |
|
224 | 236 | while (low <= high) {
|
| 237 | + // this.__bsearchCalls++ |
225 | 238 | middle = low + Math.floor((high - low) / 2);
|
226 | 239 | middleOffset = this.getIndexOffset(middle);
|
227 |
| - this.__bsearchCalls++; |
228 | 240 |
|
229 | 241 | if (middleOffset === offset) {
|
230 | 242 | return middle;
|
|
236 | 248 | }
|
237 | 249 |
|
238 | 250 | return low > 0 ? --low : 0;
|
239 |
| - } // return a scroll offset from given index. |
240 |
| - // @todo can efficiency be improved more here? |
| 251 | + } // return a scroll offset from given index, can efficiency be improved more here? |
| 252 | + // although the call frequency is very high, its only a superposition of numbers. |
241 | 253 |
|
242 | 254 | }, {
|
243 | 255 | key: "getIndexOffset",
|
244 | 256 | value: function getIndexOffset(givenIndex) {
|
245 |
| - // we know this without calculate! |
| 257 | + // we know this. |
246 | 258 | if (!givenIndex) {
|
247 | 259 | return 0;
|
248 |
| - } // get from cache if possible. |
249 |
| - |
250 |
| - |
251 |
| - if (this.caches.has(givenIndex)) { |
252 |
| - this.__getIndexOffsetCacheHits++; |
253 |
| - return this.caches.get(givenIndex); |
254 | 260 | }
|
255 | 261 |
|
256 | 262 | var offset = 0;
|
257 | 263 | var indexSize = 0;
|
258 | 264 |
|
259 | 265 | for (var index = 0; index <= givenIndex; index++) {
|
260 |
| - this.__getIndexOffsetCalls++; // cache last index offset if exist. |
261 |
| - |
262 |
| - if (index && indexSize) { |
263 |
| - this.caches.set(index, offset); |
264 |
| - } |
265 |
| - |
| 266 | + // this.__getIndexOffsetCalls++ |
266 | 267 | indexSize = this.sizes.get(this.param.uniqueIds[index]);
|
267 | 268 | offset = offset + (indexSize || this.getEstimateSize());
|
268 | 269 | } // remember last calculate index.
|
269 | 270 |
|
270 | 271 |
|
271 |
| - this.lastCalculatedIndex = Math.max(this.lastCalculatedIndex, givenIndex - 1); |
272 |
| - this.lastCalculatedIndex = Math.min(this.lastCalculatedIndex, this.getLastIndex()); |
| 272 | + this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1); |
| 273 | + this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex()); |
273 | 274 | return offset;
|
| 275 | + } |
| 276 | + }, { |
| 277 | + key: "isFixedType", |
| 278 | + value: function isFixedType() { |
| 279 | + return this.calcType === CALC_TYPE.FIXED; |
274 | 280 | } // return the real last index.
|
275 | 281 |
|
276 | 282 | }, {
|
|
323 | 329 | }, {
|
324 | 330 | key: "getPadFront",
|
325 | 331 | value: function getPadFront() {
|
326 |
| - if (this.isFixedSize()) { |
327 |
| - return this.sizeTypeValue * this.range.start; |
| 332 | + if (this.isFixedType()) { |
| 333 | + return this.fixedSizeValue * this.range.start; |
328 | 334 | } else {
|
329 | 335 | return this.getIndexOffset(this.range.start);
|
330 | 336 | }
|
|
337 | 343 | var end = this.range.end;
|
338 | 344 | var lastIndex = this.getLastIndex();
|
339 | 345 |
|
340 |
| - if (this.isFixedSize()) { |
341 |
| - return (lastIndex - end) * this.sizeTypeValue; |
342 |
| - } // if already calculate all, return the exactly padding. |
| 346 | + if (this.isFixedType()) { |
| 347 | + return (lastIndex - end) * this.fixedSizeValue; |
| 348 | + } // if calculated all already, return the exactly offset. |
343 | 349 |
|
344 | 350 |
|
345 |
| - if (this.lastCalculatedIndex === lastIndex) { |
| 351 | + if (this.lastCalcIndex === lastIndex) { |
346 | 352 | return this.getIndexOffset(lastIndex) - this.getIndexOffset(end);
|
347 | 353 | } else {
|
348 |
| - // if not, return a estimate padding. |
| 354 | + // if not, return a estimate offset. |
349 | 355 | return (lastIndex - end) * this.getEstimateSize();
|
350 | 356 | }
|
351 |
| - } // get estimate size for one item. |
| 357 | + } // get estimate size for one item, get from param.size at first range. |
352 | 358 |
|
353 | 359 | }, {
|
354 | 360 | key: "getEstimateSize",
|
|
400 | 406 | "default": 'vertical' // the other value is horizontal.
|
401 | 407 |
|
402 | 408 | },
|
| 409 | + upperThreshold: { |
| 410 | + type: Number, |
| 411 | + "default": 0 |
| 412 | + }, |
| 413 | + lowerThreshold: { |
| 414 | + type: Number, |
| 415 | + "default": 0 |
| 416 | + }, |
403 | 417 | start: {
|
404 | 418 | type: Number,
|
405 | 419 | "default": 0
|
|
430 | 444 | },
|
431 | 445 | footerClass: {
|
432 | 446 | type: String,
|
433 |
| - "default": 'div' |
| 447 | + "default": '' |
434 | 448 | },
|
435 | 449 | disabled: {
|
436 | 450 | type: Boolean,
|
|
511 | 525 | },
|
512 | 526 | // tell parent current size identify by unqiue key.
|
513 | 527 | dispatchSizeChange: function dispatchSizeChange() {
|
514 |
| - this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize()); |
| 528 | + this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize(), this.hasInitial); |
515 | 529 | }
|
516 | 530 | }
|
517 | 531 | }; // wrapping for item.
|
|
552 | 566 | // string value also use for aria role attribute.
|
553 | 567 | FOOTER: 'footer'
|
554 | 568 | };
|
555 |
| - var VirtualList = Vue.component('virtual-list', { |
| 569 | + var NAME = 'virtual-list'; |
| 570 | + var VirtualList = Vue.component(NAME, { |
556 | 571 | props: VirtualProps,
|
557 | 572 | data: function data() {
|
558 | 573 | return {
|
|
563 | 578 | dataSources: function dataSources(newValue, oldValue) {
|
564 | 579 | if (newValue.length !== oldValue.length) {
|
565 | 580 | this.virtual.updateParam('uniqueIds', this.getUniqueIdFromDataSources());
|
566 |
| - this.virtual.handleDataSourcesLengthChange(); |
| 581 | + this.virtual.handleDataSourcesChange(); |
567 | 582 | }
|
568 | 583 | }
|
569 | 584 | },
|
|
581 | 596 | // recommend for a third of keeps.
|
582 | 597 | uniqueIds: this.getUniqueIdFromDataSources()
|
583 | 598 | }, this.onRangeChanged); // just for debug
|
584 |
| - |
585 |
| - window.virtual = this.virtual; // also need sync initial range first. |
| 599 | + // window.virtual = this.virtual |
| 600 | + // also need sync initial range first. |
586 | 601 |
|
587 | 602 | this.range = this.virtual.getRange(); // listen item size changing.
|
588 | 603 |
|
|
609 | 624 | this.virtual.saveSize(id, size);
|
610 | 625 | },
|
611 | 626 | // event called when slot mounted or size changed.
|
612 |
| - onSlotResized: function onSlotResized(type, size) { |
| 627 | + onSlotResized: function onSlotResized(type, size, hasInit) { |
613 | 628 | if (type === SLOT_TYPE.HEADER) {
|
614 | 629 | this.virtual.updateParam('slotHeaderSize', size);
|
615 | 630 | } else if (type === SLOT_TYPE.FOOTER) {
|
616 | 631 | this.virtual.updateParam('slotFooterSize', size);
|
617 | 632 | }
|
618 | 633 |
|
619 |
| - this.virtual.handleSlotSizeChange(); |
| 634 | + if (hasInit) { |
| 635 | + this.virtual.handleSlotSizeChange(); |
| 636 | + } |
620 | 637 | },
|
621 | 638 | // here is the rerendering entry.
|
622 | 639 | onRangeChanged: function onRangeChanged(range) {
|
|
630 | 647 | }
|
631 | 648 |
|
632 | 649 | var offset = root[this.directionKey];
|
633 |
| - this.emitEvent(offset, evt); |
634 | 650 | this.virtual.handleScroll(offset);
|
| 651 | + this.emitEvent(offset, evt); |
635 | 652 | },
|
636 | 653 | getUniqueIdFromDataSources: function getUniqueIdFromDataSources() {
|
637 | 654 | var _this = this;
|
|
653 | 670 | // ref element is definitely available here.
|
654 | 671 | var root = this.$refs.root;
|
655 | 672 | var range = this.virtual.getRange();
|
| 673 | + var isLower = this.virtual.isLower(); |
| 674 | + var isUpper = this.virtual.isUpper(); |
656 | 675 | var offsetShape = root[this.isHorizontal ? 'clientWidth' : 'clientHeight'];
|
657 |
| - var scrollShape = root[this.isHorizontal ? 'scrollWidth' : 'scrollHeight']; // only non-empty & offset === 0 calls totop. |
| 676 | + var scrollShape = root[this.isHorizontal ? 'scrollWidth' : 'scrollHeight']; |
658 | 677 |
|
659 |
| - if (!!this.dataSources.length && !offset) { |
660 |
| - this.$emit('totop', evt, range); |
661 |
| - } else if (offset + offsetShape >= scrollShape) { |
662 |
| - this.$emit('tobottom', evt, range); |
| 678 | + if (isUpper && !!this.dataSources.length && offset - this.upperThreshold <= 0) { |
| 679 | + this.$emit('toupper', evt, range); |
| 680 | + } else if (isLower && offset + offsetShape + this.lowerThreshold >= scrollShape) { |
| 681 | + this.$emit('tolower', evt, range); |
663 | 682 | } else {
|
664 |
| - this.$emit('onscroll', evt, range); |
| 683 | + this.$emit('scroll', evt, range); |
665 | 684 | }
|
666 | 685 | },
|
667 | 686 | // get the real render slots based on range data.
|
|
671 | 690 | var end = this.disabled ? this.dataSources.length - 1 : this.range.end;
|
672 | 691 |
|
673 | 692 | for (var index = start; index <= end; index++) {
|
674 |
| - slots.push(h(Item, { |
675 |
| - "class": this.itemClass, |
676 |
| - props: { |
677 |
| - tag: this.itemTag, |
678 |
| - event: EVENT_TYPE.ITEM, |
679 |
| - horizontal: this.isHorizontal, |
680 |
| - uniqueKey: this.dataSources[index][this.dataKey], |
681 |
| - source: this.dataSources[index], |
682 |
| - component: this.dataComponent |
683 |
| - } |
684 |
| - })); |
| 693 | + var dataSource = this.dataSources[index]; |
| 694 | + |
| 695 | + if (dataSource) { |
| 696 | + slots.push(h(Item, { |
| 697 | + "class": this.itemClass, |
| 698 | + props: { |
| 699 | + tag: this.itemTag, |
| 700 | + event: EVENT_TYPE.ITEM, |
| 701 | + horizontal: this.isHorizontal, |
| 702 | + uniqueKey: dataSource[this.dataKey], |
| 703 | + source: dataSource, |
| 704 | + component: this.dataComponent |
| 705 | + } |
| 706 | + })); |
| 707 | + } else { |
| 708 | + console.warn("[".concat(NAME, "]: cannot get the index ").concat(index, " from data-sources.")); |
| 709 | + } |
685 | 710 | }
|
686 | 711 |
|
687 | 712 | return slots;
|
|
0 commit comments