import { markRaw } from 'vue'; import * as os from '@client/os'; import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll'; const SECOND_FETCH_LIMIT = 30; // reversed: items 配列の中身を逆順にする(新しい方が最後) export default (opts) => ({ emits: ['queue'], data() { return { items: [], queue: [], offset: 0, fetching: true, moreFetching: false, inited: false, more: false, backed: false, // 遡り中か否か isBackTop: false, }; }, computed: { empty(): boolean { return this.items.length === 0 && !this.fetching && this.inited; }, error(): boolean { return !this.fetching && !this.inited; }, }, watch: { pagination: { handler() { this.init(); }, deep: true }, queue: { handler(a, b) { if (a.length === 0 && b.length === 0) return; this.$emit('queue', this.queue.length); }, deep: true } }, created() { opts.displayLimit = opts.displayLimit || 30; this.init(); }, activated() { this.isBackTop = false; }, deactivated() { this.isBackTop = window.scrollY === 0; }, methods: { reload() { this.items = []; this.init(); }, replaceItem(finder, data) { const i = this.items.findIndex(finder); this.items[i] = data; }, removeItem(finder) { const i = this.items.findIndex(finder); this.items.splice(i, 1); }, async init() { this.queue = []; this.fetching = true; if (opts.before) opts.before(this); let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; if (params && params.then) params = await params; if (params === null) return; const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; await os.api(endpoint, { ...params, limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, }).then(items => { for (let i = 0; i < items.length; i++) { const item = items[i]; markRaw(item); if (this.pagination.reversed) { if (i === items.length - 2) item._shouldInsertAd_ = true; } else { if (i === 3) item._shouldInsertAd_ = true; } } if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { items.pop(); this.items = this.pagination.reversed ? [...items].reverse() : items; this.more = true; } else { this.items = this.pagination.reversed ? [...items].reverse() : items; this.more = false; } this.offset = items.length; this.inited = true; this.fetching = false; if (opts.after) opts.after(this, null); }, e => { this.fetching = false; if (opts.after) opts.after(this, e); }); }, async fetchMore() { if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; this.moreFetching = true; this.backed = true; let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; if (params && params.then) params = await params; const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; await os.api(endpoint, { ...params, limit: SECOND_FETCH_LIMIT + 1, ...(this.pagination.offsetMode ? { offset: this.offset, } : { untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, }), }).then(items => { for (let i = 0; i < items.length; i++) { const item = items[i]; markRaw(item); if (this.pagination.reversed) { if (i === items.length - 9) item._shouldInsertAd_ = true; } else { if (i === 10) item._shouldInsertAd_ = true; } } if (items.length > SECOND_FETCH_LIMIT) { items.pop(); this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); this.more = true; } else { this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); this.more = false; } this.offset += items.length; this.moreFetching = false; }, e => { this.moreFetching = false; }); }, async fetchMoreFeature() { if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; this.moreFetching = true; let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; if (params && params.then) params = await params; const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; await os.api(endpoint, { ...params, limit: SECOND_FETCH_LIMIT + 1, ...(this.pagination.offsetMode ? { offset: this.offset, } : { sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, }), }).then(items => { for (const item of items) { markRaw(item); } if (items.length > SECOND_FETCH_LIMIT) { items.pop(); this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); this.more = true; } else { this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); this.more = false; } this.offset += items.length; this.moreFetching = false; }, e => { this.moreFetching = false; }); }, prepend(item) { if (this.pagination.reversed) { const container = getScrollContainer(this.$el); const pos = getScrollPosition(this.$el); const viewHeight = container.clientHeight; const height = container.scrollHeight; const isBottom = (pos + viewHeight > height - 32); if (isBottom) { // オーバーフローしたら古いアイテムは捨てる if (this.items.length >= opts.displayLimit) { // このやり方だとVue 3.2以降アニメーションが動かなくなる //this.items = this.items.slice(-opts.displayLimit); while (this.items.length >= opts.displayLimit) { this.items.shift(); } this.more = true; } } this.items.push(item); // TODO } else { const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el)); if (isTop) { // Prepend the item this.items.unshift(item); // オーバーフローしたら古いアイテムは捨てる if (this.items.length >= opts.displayLimit) { // このやり方だとVue 3.2以降アニメーションが動かなくなる //this.items = this.items.slice(0, opts.displayLimit); while (this.items.length >= opts.displayLimit) { this.items.pop(); } this.more = true; } } else { this.queue.push(item); onScrollTop(this.$el, () => { for (const item of this.queue) { this.prepend(item); } this.queue = []; }); } } }, append(item) { this.items.push(item); }, } });