|
|
<template>
|
|
|
<div class="cube-recycle-list">
|
|
|
<div class="cube-recycle-list-main">
|
|
|
<div class="cube-recycle-list-items" :style="{height: heights + 'px'}">
|
|
|
<div ref="items" class="cube-recycle-list-items">
|
|
|
<div
|
|
|
v-for="(item, index) in visibleItems"
|
|
|
:key="index"
|
|
|
v-for="item in visibleItems"
|
|
|
:key="item.index"
|
|
|
class="cube-recycle-list-item"
|
|
|
:class="thumbClass"
|
|
|
:ref="'loads'+index"
|
|
|
:style="getItemStyle(item, index)"
|
|
|
>
|
|
|
<div
|
|
|
v-if="infinite"
|
|
|
:class="{'cube-recycle-list-transition': infinite}"
|
|
|
:style="{opacity: +!item.loaded}"
|
|
|
>
|
|
|
<slot name="tombstone"></slot>
|
|
|
</div>
|
|
|
<div
|
|
|
v-if="!item.placeholder"
|
|
|
:class="{'cube-recycle-list-transition': infinite}"
|
|
|
:style="{opacity: item.loaded}"
|
|
|
>
|
|
|
<slot name="item" :data="{data: item.data, index}"></slot>
|
|
|
:ref="'loads'+item.index"
|
|
|
:style="getItemStyle(item)">
|
|
|
<div v-if="!item.placeholder || !item.height">
|
|
|
<slot name="item" :data="item"></slot>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div
|
|
|
v-if="!infinite"
|
|
|
class="cube-recycle-list-loading">
|
|
|
<div class="cube-recycle-list-loading">
|
|
|
<slot name="spinner">
|
|
|
<div class="cube-recycle-list-loading-content" v-show="!noMore" :style="{visibility: loadings.length ? 'visible' : 'hidden'}">
|
|
|
<cube-loading class="spinner"></cube-loading>
|
...
|
...
|
@@ -54,7 +40,6 @@ export default { |
|
|
data() {
|
|
|
return {
|
|
|
items: [],
|
|
|
list: [],
|
|
|
heights: this.thumbs.length ? 1000 : 0,
|
|
|
startIndex: 0,
|
|
|
currentIndex: 0,
|
...
|
...
|
@@ -91,14 +76,19 @@ export default { |
|
|
computed: {
|
|
|
visibleItems() {
|
|
|
if (this.thumbsList.length) {
|
|
|
return this.thumbsList.map(item => {
|
|
|
return {
|
|
|
data: item
|
|
|
};
|
|
|
}).concat(this.items.slice(this.thumbsList.length, this.items.length));
|
|
|
return this.thumbItems.concat(this.items.slice(this.thumbsList.length, this.items.length));
|
|
|
}
|
|
|
return this.items;
|
|
|
},
|
|
|
thumbItems() {
|
|
|
return this.thumbsList.map((item, index) => {
|
|
|
return {
|
|
|
data: item,
|
|
|
isThumb: true,
|
|
|
index
|
|
|
};
|
|
|
});
|
|
|
},
|
|
|
tombHeight() {
|
|
|
return this.infinite ? this.$refs.tomb && this.$refs.tomb.offsetHeight : 0;
|
|
|
},
|
...
|
...
|
@@ -114,21 +104,6 @@ export default { |
|
|
};
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
list(newV) {
|
|
|
if (newV.length) {
|
|
|
this.loadings.pop();
|
|
|
if (!this.loadings.length) {
|
|
|
this.loadItems();
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
items(newV) {
|
|
|
if (newV.length > this.list.length) {
|
|
|
this.getItems();
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
this.scrollEvent = throttle(this._onScroll.bind(this), 100);
|
|
|
let supportsPassive = false;
|
...
|
...
|
@@ -153,71 +128,68 @@ export default { |
|
|
init() {
|
|
|
this.load(true);
|
|
|
},
|
|
|
getItemStyle(item, index) {
|
|
|
getItemStyle(item) {
|
|
|
const style = {};
|
|
|
|
|
|
if (!this.isThumb) {
|
|
|
style.transform = `translate3d(0, ${item.top}px, 0)`;
|
|
|
}
|
|
|
if (item.placeholder) {
|
|
|
if (item.height) {
|
|
|
if (!item.unlockHight) {
|
|
|
style.height = `${item.height}px`;
|
|
|
}
|
|
|
} else if (item.willchange) {
|
|
|
style.transition = 'height 300ms cubic-bezier(0.165, 0.84, 0.44, 1)';
|
|
|
style['will-change'] = 'height';
|
|
|
style.height = `${item.height}px`;
|
|
|
} else {
|
|
|
style['z-index'] = this.visibleItems.length - index;
|
|
|
}
|
|
|
if (!item.height && !this.isThumb) {
|
|
|
style.opacity = 0;
|
|
|
} else if (!item.isThumb) {
|
|
|
style.position = 'absolute';
|
|
|
style.top = `${-1000}px`;
|
|
|
style.visibility = 'hidden';
|
|
|
}
|
|
|
return style;
|
|
|
},
|
|
|
load(reload) {
|
|
|
if (this.infinite) {
|
|
|
// increase capacity of items to display tombstone
|
|
|
this.items.length += this.size;
|
|
|
this.loadItems();
|
|
|
} else if ((!this.loadings.length && !this.noMore) || reload) {
|
|
|
if ((!this.loadings.length && !this.noMore) || reload) {
|
|
|
this.getItems(reload);
|
|
|
}
|
|
|
},
|
|
|
getItems(reload) {
|
|
|
if (reload) {
|
|
|
this.noMore = false;
|
|
|
this.list = [];
|
|
|
this.items = [];
|
|
|
}
|
|
|
this.loadings.push('pending');
|
|
|
this.onFetch().then((res) => {
|
|
|
/* istanbul ignore if */
|
|
|
if (!res) {
|
|
|
this.noMore = true;
|
|
|
this.loadings.pop();
|
|
|
} else {
|
|
|
this.list = this.list.concat(res);
|
|
|
this.loadItems(res);
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
async loadItems() {
|
|
|
let end = this.infinite ? this.items.length : this.list.length;
|
|
|
let item;
|
|
|
async loadItems(list) {
|
|
|
const lastItem = this.items[this.items.length - 1];
|
|
|
const start = lastItem ? lastItem.index + 1 : 0;
|
|
|
|
|
|
for (let i = this.items.length; i < end; i++) {
|
|
|
item = this.items[i];
|
|
|
/* istanbul ignore if */
|
|
|
if (item && item.loaded) {
|
|
|
continue;
|
|
|
}
|
|
|
await this.loadItem(i);
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
await this.loadItem(i + start, list[i]);
|
|
|
}
|
|
|
if (this.loadings.length) {
|
|
|
this.loadings.pop();
|
|
|
}
|
|
|
if (this.thumbsList.length) {
|
|
|
this.thumbsList = [];
|
|
|
}
|
|
|
},
|
|
|
loadItem(i) {
|
|
|
loadItem(i, item) {
|
|
|
return new Promise(r => {
|
|
|
this.setItem(i, this.list[i]);
|
|
|
const insertIndex = this.setItem(i, item);
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
this.updateItemHeight(i);
|
|
|
r();
|
|
|
setTimeout(() => {
|
|
|
this.updateItemHeight(insertIndex);
|
|
|
r();
|
|
|
}, 100);
|
|
|
});
|
|
|
});
|
|
|
},
|
...
|
...
|
@@ -226,14 +198,18 @@ export default { |
|
|
this.thumbsList[index] = data;
|
|
|
this.thumbsList = this.thumbsList.map(item => item);
|
|
|
}
|
|
|
this.$set(this.items, index, {
|
|
|
const insertIndex = this.items.length;
|
|
|
|
|
|
this.$set(this.items, insertIndex, {
|
|
|
data: data || {},
|
|
|
height: 0,
|
|
|
top: -1000,
|
|
|
isTombstone: !data,
|
|
|
loaded: data ? 1 : 0,
|
|
|
placeholder: false
|
|
|
placeholder: false,
|
|
|
unlockHight: false,
|
|
|
willchange: false,
|
|
|
index
|
|
|
});
|
|
|
return insertIndex;
|
|
|
},
|
|
|
updateItemHeight(index, resize) {
|
|
|
if (index === 0 && !resize) {
|
...
|
...
|
@@ -243,7 +219,7 @@ export default { |
|
|
// update item height
|
|
|
let increHeight = 0;
|
|
|
let cur = this.items[index];
|
|
|
let dom = this.$refs[`loads${index}`];
|
|
|
let dom = this.$refs[`loads${cur.index}`];
|
|
|
|
|
|
try {
|
|
|
if (dom && dom[0]) {
|
...
|
...
|
@@ -266,18 +242,8 @@ export default { |
|
|
|
|
|
return increHeight;
|
|
|
},
|
|
|
updateItemTop(startIndex, increHeight) {
|
|
|
// loop all items to update item top and list height
|
|
|
for (let i = startIndex; i < this.items.length; i++) {
|
|
|
let pre = this.items[i - 1];
|
|
|
|
|
|
this.items[i].top = pre ? pre.top + pre.height : 0;
|
|
|
}
|
|
|
this.heights += increHeight;
|
|
|
},
|
|
|
updateIndex() {
|
|
|
// update visible items start index
|
|
|
let top = this.$el.scrollTop;
|
|
|
updateIndex(scrollTop) {
|
|
|
let top = scrollTop;
|
|
|
let hasTopItem = false;
|
|
|
|
|
|
for (let i = 0; i < this.items.length; i++) {
|
...
|
...
|
@@ -292,26 +258,65 @@ export default { |
|
|
}
|
|
|
}
|
|
|
if (hasTopItem) {
|
|
|
this.currentIndex = this.startIndex;
|
|
|
this.currentIndex = this.items[this.startIndex].index;
|
|
|
} else {
|
|
|
this.currentIndex = this.items.length - 1;
|
|
|
this.currentIndex = this.items[this.items.length - 1].index;
|
|
|
}
|
|
|
},
|
|
|
_onScroll() {
|
|
|
const scrollTop = this.$el.scrollTop;
|
|
|
const heights = this.$refs.items.offsetHeight;
|
|
|
|
|
|
// trigger load
|
|
|
if (scrollTop + this.$el.offsetHeight > this.heights - this.offset) {
|
|
|
if (scrollTop + this.$el.offsetHeight > heights - this.offset) {
|
|
|
this.load();
|
|
|
}
|
|
|
this.updateIndex();
|
|
|
this.updateIndex(scrollTop);
|
|
|
|
|
|
this.$emit('scroll', {scrollTop, startIndex: this.currentIndex, item: this.items[this.currentIndex]});
|
|
|
},
|
|
|
resize(index) {
|
|
|
const increHeight = this.updateItemHeight(index, true);
|
|
|
findItemIndex(itemIndex) {
|
|
|
return this.items.findIndex(item => item.index === itemIndex);
|
|
|
},
|
|
|
resize(itemIndex) {
|
|
|
const findItemIndex = this.findItemIndex(itemIndex);
|
|
|
|
|
|
if (findItemIndex >= 0) {
|
|
|
this.updateItemHeight(findItemIndex, true);
|
|
|
}
|
|
|
},
|
|
|
delete(itemIndex) {
|
|
|
const findItemIndex = this.findItemIndex(itemIndex);
|
|
|
|
|
|
if (findItemIndex >= 0) {
|
|
|
this.items[findItemIndex].willchange = true;
|
|
|
let dom = this.$refs[`loads${itemIndex}`];
|
|
|
|
|
|
dom && dom[0].addEventListener('transitionend', () => {
|
|
|
this.items.splice(findItemIndex, 1);
|
|
|
this._onScroll();
|
|
|
}, {
|
|
|
once: true
|
|
|
});
|
|
|
this.$nextTick(() => {
|
|
|
this.items[findItemIndex].height = 0;
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
unlockHight({index, promise}) {
|
|
|
const findItemIndex = this.findItemIndex(index);
|
|
|
|
|
|
this.updateItemTop(index, increHeight);
|
|
|
if (findItemIndex >= 0) {
|
|
|
const cur = this.items[findItemIndex];
|
|
|
|
|
|
if (cur) {
|
|
|
cur.unlockHight = true;
|
|
|
promise.then(() => {
|
|
|
this.updateItemHeight(findItemIndex, true);
|
|
|
cur.unlockHight = false;
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
components: {
|
...
|
...
|
@@ -336,7 +341,6 @@ export default { |
|
|
visibility: hidden
|
|
|
.cube-recycle-list-item
|
|
|
width: 100%
|
|
|
position: absolute
|
|
|
box-sizing: border-box
|
|
|
&.thumb
|
|
|
position relative
|
...
|
...
|
|