2023-07-27 14:31:52 +09:00
|
|
|
|
<!--
|
|
|
|
|
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
-->
|
|
|
|
|
|
|
2020-01-30 04:37:25 +09:00
|
|
|
|
<template>
|
2023-01-14 15:01:28 +09:00
|
|
|
|
<Transition
|
2023-05-19 13:58:09 +09:00
|
|
|
|
:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
|
|
|
|
|
:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
|
|
|
|
|
:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
|
|
|
|
|
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
2023-01-14 15:01:28 +09:00
|
|
|
|
mode="out-in"
|
|
|
|
|
|
>
|
2021-04-22 22:29:33 +09:00
|
|
|
|
<MkLoading v-if="fetching"/>
|
|
|
|
|
|
|
2023-08-24 06:34:30 +00:00
|
|
|
|
<MkError v-else-if="empty && error" @retry="reload()"/>
|
2021-04-22 22:29:33 +09:00
|
|
|
|
|
2021-11-19 19:36:12 +09:00
|
|
|
|
<div v-else-if="empty" key="_empty_" class="empty">
|
2021-12-23 16:10:13 +09:00
|
|
|
|
<slot name="empty">
|
|
|
|
|
|
<div class="_fullinfo">
|
2023-06-09 14:00:53 +09:00
|
|
|
|
<img :src="infoImageUrl" class="_ghost"/>
|
2022-07-20 22:24:26 +09:00
|
|
|
|
<div>{{ i18n.ts.nothing }}</div>
|
2021-12-23 16:10:13 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</slot>
|
2020-01-30 04:37:25 +09:00
|
|
|
|
</div>
|
2021-04-22 22:29:33 +09:00
|
|
|
|
|
2022-01-09 21:35:35 +09:00
|
|
|
|
<div v-else ref="rootEl">
|
2023-01-14 15:01:28 +09:00
|
|
|
|
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
2023-07-05 00:59:37 +09:00
|
|
|
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
|
2022-07-20 22:24:26 +09:00
|
|
|
|
{{ i18n.ts.loadMore }}
|
2022-05-19 20:32:55 +09:00
|
|
|
|
</MkButton>
|
|
|
|
|
|
<MkLoading v-else class="loading"/>
|
|
|
|
|
|
</div>
|
2023-08-24 06:34:30 +00:00
|
|
|
|
<slot :items="providingItems" :fetching="fetching || moreFetching" :denyMoveTransition="denyMoveTransition"></slot>
|
2023-01-14 15:01:28 +09:00
|
|
|
|
<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
|
2023-07-05 00:59:37 +09:00
|
|
|
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
|
2022-07-20 22:24:26 +09:00
|
|
|
|
{{ i18n.ts.loadMore }}
|
2021-04-22 22:29:33 +09:00
|
|
|
|
</MkButton>
|
2022-01-09 21:35:35 +09:00
|
|
|
|
<MkLoading v-else class="loading"/>
|
2021-04-22 22:29:33 +09:00
|
|
|
|
</div>
|
2020-01-30 04:37:25 +09:00
|
|
|
|
</div>
|
2022-12-30 13:37:14 +09:00
|
|
|
|
</Transition>
|
2020-01-30 04:37:25 +09:00
|
|
|
|
</template>
|
|
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
<script lang="ts">
|
|
|
|
|
|
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
2023-09-04 13:33:38 +09:00
|
|
|
|
import * as Misskey from 'misskey-js';
|
2022-01-09 21:35:35 +09:00
|
|
|
|
import * as os from '@/os';
|
2023-07-25 03:40:42 +00:00
|
|
|
|
import { isBottomVisible, isTopVisible, getScrollContainer, scrollToBottom, scrollToTop, scrollBy, scroll, getBodyScrollHeight } from '@/scripts/scroll';
|
2023-02-25 08:18:12 +09:00
|
|
|
|
import { useDocumentVisibility } from '@/scripts/use-document-visibility';
|
2022-09-06 18:21:49 +09:00
|
|
|
|
import MkButton from '@/components/MkButton.vue';
|
2023-01-13 18:25:40 +09:00
|
|
|
|
import { defaultStore } from '@/store';
|
|
|
|
|
|
import { MisskeyEntity } from '@/types/date-separated-list';
|
2022-07-20 22:24:26 +09:00
|
|
|
|
import { i18n } from '@/i18n';
|
2023-07-26 12:22:26 +00:00
|
|
|
|
import { isWebKit } from '@/scripts/useragent';
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
|
|
|
|
|
const SECOND_FETCH_LIMIT = 30;
|
2023-07-17 09:29:52 +00:00
|
|
|
|
const TOLERANCE = 6;
|
2023-07-05 00:59:37 +09:00
|
|
|
|
const APPEAR_MINIMUM_INTERVAL = 600;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
2023-09-04 13:33:38 +09:00
|
|
|
|
export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = {
|
2022-01-09 21:35:35 +09:00
|
|
|
|
endpoint: E;
|
2023-07-11 07:53:56 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 一度にAPIへ取得する件数
|
|
|
|
|
|
*/
|
2022-01-09 21:35:35 +09:00
|
|
|
|
limit: number;
|
2023-07-11 07:53:56 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* タイムラインに表示する最大件数
|
|
|
|
|
|
*/
|
|
|
|
|
|
displayLimit?: number;
|
|
|
|
|
|
|
2023-09-04 13:33:38 +09:00
|
|
|
|
params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 検索APIのような、ページング不可なエンドポイントを利用する場合
|
|
|
|
|
|
* (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
|
|
|
|
|
|
*/
|
|
|
|
|
|
noPaging?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* items 配列の中身を逆順にする(新しい方が最後)
|
|
|
|
|
|
*/
|
|
|
|
|
|
reversed?: boolean;
|
2022-01-10 01:00:50 +09:00
|
|
|
|
|
|
|
|
|
|
offsetMode?: boolean;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
pageEl?: HTMLElement;
|
|
|
|
|
|
};
|
2023-07-05 00:59:37 +09:00
|
|
|
|
|
|
|
|
|
|
type MisskeyEntityMap = Map<string, MisskeyEntity>;
|
|
|
|
|
|
|
|
|
|
|
|
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
|
|
|
|
|
return entities.map(en => [en.id, en]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
|
|
|
|
|
|
return new Map([...map, ...arrayToEntries(entities)]);
|
|
|
|
|
|
}
|
2023-07-19 03:23:31 +00:00
|
|
|
|
|
2023-07-26 12:22:26 +00:00
|
|
|
|
const timelineBackTopBehavior = computed(() => isWebKit() ? 'newest' : defaultStore.reactiveState.timelineBackTopBehavior.value);
|
2023-01-13 18:25:40 +09:00
|
|
|
|
</script>
|
|
|
|
|
|
<script lang="ts" setup>
|
2023-06-09 14:00:53 +09:00
|
|
|
|
import { infoImageUrl } from '@/instance';
|
|
|
|
|
|
|
2022-01-09 21:35:35 +09:00
|
|
|
|
const props = withDefaults(defineProps<{
|
|
|
|
|
|
pagination: Paging;
|
|
|
|
|
|
disableAutoLoad?: boolean;
|
|
|
|
|
|
}>(), {
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
let rootEl = $shallowRef<HTMLElement>();
|
|
|
|
|
|
|
2023-07-11 12:36:45 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* スクロールが先頭にある場合はfalse
|
|
|
|
|
|
* スクロールが先頭にない場合にtrue
|
|
|
|
|
|
*/
|
2023-07-17 07:38:42 +00:00
|
|
|
|
// 先頭にいるか(prependでキューに追加するかどうかの判定に使う)
|
2023-01-13 18:25:40 +09:00
|
|
|
|
let backed = $ref(false);
|
2023-07-17 07:38:42 +00:00
|
|
|
|
// true→falseの変更でexecuteQueueする
|
2023-07-11 13:11:25 +00:00
|
|
|
|
let weakBacked = $ref(false);
|
2022-01-10 01:00:50 +09:00
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
let scrollRemove = $ref<(() => void) | null>(null);
|
|
|
|
|
|
|
2023-07-05 00:59:37 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 表示するアイテムのソース
|
|
|
|
|
|
* 最新が0番目
|
|
|
|
|
|
*/
|
|
|
|
|
|
const items = ref<MisskeyEntityMap>(new Map());
|
2023-08-24 06:34:30 +00:00
|
|
|
|
const providingItems = computed(() => Array.from(items.value.values()));
|
2023-07-05 00:59:37 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* タブが非アクティブなどの場合に更新を貯めておく
|
2023-07-11 14:14:15 +00:00
|
|
|
|
* 最新が最後(パフォーマンス上の理由でitemsと逆にした)
|
2023-07-05 00:59:37 +09:00
|
|
|
|
*/
|
|
|
|
|
|
const queue = ref<MisskeyEntityMap>(new Map());
|
2023-07-11 14:39:59 +00:00
|
|
|
|
const queueSize = computed(() => queue.value.size);
|
2023-07-05 00:59:37 +09:00
|
|
|
|
|
2022-01-09 21:35:35 +09:00
|
|
|
|
const offset = ref(0);
|
2023-07-05 00:59:37 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初期化中かどうか(trueならMkLoadingで全て隠す)
|
|
|
|
|
|
*/
|
2022-01-09 21:35:35 +09:00
|
|
|
|
const fetching = ref(true);
|
2023-07-05 00:59:37 +09:00
|
|
|
|
|
2023-07-11 07:53:56 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* onActivatedでtrue, onDeactivatedでfalseになる
|
|
|
|
|
|
*/
|
2023-07-11 15:17:52 +00:00
|
|
|
|
const active = ref(true);
|
2023-07-11 07:53:56 +00:00
|
|
|
|
|
2022-01-09 21:35:35 +09:00
|
|
|
|
const moreFetching = ref(false);
|
|
|
|
|
|
const more = ref(false);
|
2023-07-05 00:59:37 +09:00
|
|
|
|
const preventAppearFetchMore = ref(false);
|
|
|
|
|
|
const preventAppearFetchMoreTimer = ref<number | null>(null);
|
|
|
|
|
|
const empty = computed(() => items.value.size === 0);
|
2022-01-21 16:43:56 +09:00
|
|
|
|
const error = ref(false);
|
2023-01-13 18:25:40 +09:00
|
|
|
|
const {
|
2023-01-14 15:01:28 +09:00
|
|
|
|
enableInfiniteScroll,
|
2023-01-13 18:25:40 +09:00
|
|
|
|
} = defaultStore.reactiveState;
|
|
|
|
|
|
|
2023-07-11 07:53:56 +00:00
|
|
|
|
const displayLimit = computed(() => props.pagination.displayLimit ?? props.pagination.limit * 2);
|
|
|
|
|
|
|
2023-02-22 15:28:17 +09:00
|
|
|
|
const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
|
2023-07-13 04:38:04 +00:00
|
|
|
|
const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) ?? null : null);
|
|
|
|
|
|
const scrollableElementOrHtml = $computed(() => scrollableElement ?? document.getElementsByName('html')[0]);
|
2023-01-13 18:25:40 +09:00
|
|
|
|
|
2023-02-25 08:18:12 +09:00
|
|
|
|
const visibility = useDocumentVisibility();
|
|
|
|
|
|
|
2023-07-18 07:15:20 +00:00
|
|
|
|
const isPausingUpdateByExecutingQueue = ref(false);
|
2023-07-25 10:47:31 +00:00
|
|
|
|
const denyMoveTransition = ref(false);
|
2023-07-18 07:15:20 +00:00
|
|
|
|
|
2023-07-11 12:36:45 +00:00
|
|
|
|
//#region scrolling
|
2023-07-13 05:27:53 +00:00
|
|
|
|
const checkFn = props.pagination.reversed ? isBottomVisible : isTopVisible;
|
2023-07-14 12:03:59 +00:00
|
|
|
|
const checkTop = (tolerance?: number) => {
|
2023-07-14 13:11:59 +00:00
|
|
|
|
if (!contentEl) return true;
|
|
|
|
|
|
if (!document.body.contains(contentEl)) return true;
|
2023-07-14 12:03:59 +00:00
|
|
|
|
return checkFn(contentEl, tolerance, scrollableElement);
|
2023-07-13 05:27:53 +00:00
|
|
|
|
};
|
2023-07-11 12:36:45 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* IntersectionObserverで大まかに検出
|
|
|
|
|
|
* https://qiita.com/mkataigi/items/0154aefd2223ce23398e
|
|
|
|
|
|
*/
|
2023-01-13 18:25:40 +09:00
|
|
|
|
let scrollObserver = $ref<IntersectionObserver>();
|
|
|
|
|
|
|
|
|
|
|
|
watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
|
|
|
|
|
|
if (scrollObserver) scrollObserver.disconnect();
|
|
|
|
|
|
|
|
|
|
|
|
scrollObserver = new IntersectionObserver(entries => {
|
2023-07-17 08:59:47 +00:00
|
|
|
|
if (!active.value) return; // activeでない時は触らない
|
2023-07-17 15:04:48 +00:00
|
|
|
|
weakBacked = entries[0].intersectionRatio >= 0.1;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
}, {
|
2023-07-13 05:27:53 +00:00
|
|
|
|
root: scrollableElement,
|
2023-07-17 15:16:50 +00:00
|
|
|
|
rootMargin: props.pagination.reversed ? '-100% 0px 1000% 0px' : '1000% 0px -100% 0px',
|
|
|
|
|
|
threshold: [0.01, 0.05, 0.1, 0.12, 0.15],
|
2023-01-13 18:25:40 +09:00
|
|
|
|
});
|
|
|
|
|
|
}, { immediate: true });
|
|
|
|
|
|
|
2023-07-13 05:27:53 +00:00
|
|
|
|
watch([$$(rootEl), $$(scrollObserver)], () => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
scrollObserver?.disconnect();
|
2023-07-13 07:20:11 +00:00
|
|
|
|
if (rootEl) scrollObserver?.observe(rootEl);
|
2023-01-13 18:25:40 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2023-07-11 12:36:45 +00:00
|
|
|
|
/**
|
2023-07-17 07:38:42 +00:00
|
|
|
|
* weakBackedがtrue→falseになったらexecuteQueue
|
2023-07-11 12:36:45 +00:00
|
|
|
|
*/
|
2023-07-17 07:38:42 +00:00
|
|
|
|
watch($$(weakBacked), () => {
|
2023-07-26 12:22:26 +00:00
|
|
|
|
if (timelineBackTopBehavior.value === 'next' && !weakBacked) {
|
2023-07-11 13:23:56 +00:00
|
|
|
|
executeQueue();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2023-07-11 13:11:25 +00:00
|
|
|
|
|
2023-07-17 10:24:18 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* backedがtrue→falseになってもexecuteQueue
|
|
|
|
|
|
*/
|
|
|
|
|
|
watch($$(backed), () => {
|
|
|
|
|
|
if (!backed) {
|
|
|
|
|
|
executeQueue();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-07-17 07:38:42 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* onScrollTop/onScrollBottomでbackedを厳密に検出する
|
|
|
|
|
|
*/
|
2023-07-11 13:11:25 +00:00
|
|
|
|
watch([$$(weakBacked), $$(contentEl)], () => {
|
2023-07-13 05:27:53 +00:00
|
|
|
|
if (scrollRemove) scrollRemove();
|
|
|
|
|
|
scrollRemove = null;
|
|
|
|
|
|
|
2023-07-11 13:11:25 +00:00
|
|
|
|
if (weakBacked || !contentEl) {
|
2023-07-13 05:27:53 +00:00
|
|
|
|
if (weakBacked) backed = true;
|
2023-07-11 13:11:25 +00:00
|
|
|
|
return;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
}
|
2023-07-11 13:11:25 +00:00
|
|
|
|
|
|
|
|
|
|
scrollRemove = (() => {
|
2023-07-13 05:27:53 +00:00
|
|
|
|
const checkBacked = () => {
|
2023-07-17 08:59:47 +00:00
|
|
|
|
if (!active.value) return; // activeでない時は触らない
|
2023-07-17 07:38:42 +00:00
|
|
|
|
backed = !checkTop(TOLERANCE);
|
2023-07-11 13:11:25 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// とりあえず評価してみる
|
2023-07-13 05:27:53 +00:00
|
|
|
|
checkBacked();
|
2023-07-11 13:11:25 +00:00
|
|
|
|
|
2023-07-13 04:38:04 +00:00
|
|
|
|
const container = scrollableElementOrHtml;
|
2023-07-11 13:11:25 +00:00
|
|
|
|
|
2023-07-13 05:36:32 +00:00
|
|
|
|
function removeListener() { container.removeEventListener('scroll', checkBacked); }
|
2023-07-13 05:27:53 +00:00
|
|
|
|
container.addEventListener('scroll', checkBacked, { passive: true });
|
2023-07-11 13:11:25 +00:00
|
|
|
|
return removeListener;
|
|
|
|
|
|
})();
|
2023-01-13 18:25:40 +09:00
|
|
|
|
});
|
2023-07-13 03:53:48 +00:00
|
|
|
|
|
2023-07-25 03:40:42 +00:00
|
|
|
|
function preventDefault(ev: Event) {
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* アイテムを上に追加した場合に追加分だけスクロールを下にずらす
|
2023-07-25 04:01:26 +00:00
|
|
|
|
* Safariでは使わない方がいいかも?
|
2023-07-25 03:40:42 +00:00
|
|
|
|
* @param fn DOM操作(unshiftItemsなど)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function adjustScroll(fn: () => void): Promise<void> {
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
|
|
|
|
|
|
const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
|
|
|
|
|
|
// スクロールをやめさせる
|
|
|
|
|
|
try {
|
|
|
|
|
|
// なぜかscrollableElementOrHtmlがundefinedであるというエラーが出る
|
|
|
|
|
|
scrollableElementOrHtml.addEventListener('wheel', preventDefault, { passive: false });
|
|
|
|
|
|
scrollableElementOrHtml.addEventListener('touchmove', preventDefault, { passive: false });
|
|
|
|
|
|
// スクロールを強制的に停止
|
|
|
|
|
|
scroll(scrollableElement, { top: oldScroll, behavior: 'instant' });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err, { scrollableElementOrHtml });
|
|
|
|
|
|
}
|
2023-07-25 10:47:31 +00:00
|
|
|
|
denyMoveTransition.value = true;
|
2023-07-25 03:40:42 +00:00
|
|
|
|
fn();
|
2023-07-26 07:33:17 +00:00
|
|
|
|
return await nextTick().then(() => {
|
|
|
|
|
|
const top = oldScroll + ((scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight()) - oldHeight);
|
|
|
|
|
|
scroll(scrollableElement, { top, behavior: 'instant' });
|
|
|
|
|
|
// なぜかscrollableElementOrHtmlがundefinedであるというエラーが出る
|
|
|
|
|
|
scrollableElementOrHtml.removeEventListener('wheel', preventDefault);
|
|
|
|
|
|
scrollableElementOrHtml.removeEventListener('touchmove', preventDefault);
|
2023-07-25 10:47:31 +00:00
|
|
|
|
}).then(() => nextTick()).finally(() => {
|
|
|
|
|
|
denyMoveTransition.value = false;
|
2023-07-25 03:40:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
2023-07-11 14:14:15 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 初期化
|
|
|
|
|
|
* scrollAfterInitなどの後処理もあるので、reload関数を使うべき
|
|
|
|
|
|
*
|
|
|
|
|
|
* 注意: moreFetchingをtrueにするのでfalseにする必要がある
|
|
|
|
|
|
*/
|
2023-01-13 18:25:40 +09:00
|
|
|
|
async function init(): Promise<void> {
|
2023-07-06 06:43:05 +00:00
|
|
|
|
items.value = new Map();
|
2023-07-05 00:59:37 +09:00
|
|
|
|
queue.value = new Map();
|
2022-01-09 21:35:35 +09:00
|
|
|
|
fetching.value = true;
|
|
|
|
|
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
|
|
|
|
|
await os.api(props.pagination.endpoint, {
|
|
|
|
|
|
...params,
|
2023-04-03 11:50:17 +09:00
|
|
|
|
limit: props.pagination.limit ?? 10,
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}).then(res => {
|
|
|
|
|
|
for (let i = 0; i < res.length; i++) {
|
|
|
|
|
|
const item = res[i];
|
2023-01-13 18:25:40 +09:00
|
|
|
|
if (i === 3) item._shouldInsertAd_ = true;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}
|
2023-04-03 11:50:17 +09:00
|
|
|
|
|
|
|
|
|
|
if (res.length === 0 || props.pagination.noPaging) {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
concatItems(res);
|
2023-04-03 11:50:17 +09:00
|
|
|
|
more.value = false;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
} else {
|
2023-07-11 14:14:15 +00:00
|
|
|
|
moreFetching.value = true;
|
2023-07-05 00:59:37 +09:00
|
|
|
|
concatItems(res);
|
2023-04-03 11:50:17 +09:00
|
|
|
|
more.value = true;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}
|
2023-04-03 11:50:17 +09:00
|
|
|
|
|
2022-01-09 21:35:35 +09:00
|
|
|
|
offset.value = res.length;
|
2022-01-21 16:43:56 +09:00
|
|
|
|
error.value = false;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
fetching.value = false;
|
2022-05-26 15:53:09 +02:00
|
|
|
|
}, err => {
|
2022-01-21 16:43:56 +09:00
|
|
|
|
error.value = true;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
fetching.value = false;
|
|
|
|
|
|
});
|
2023-01-13 18:25:40 +09:00
|
|
|
|
}
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
2023-07-11 14:14:15 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* initの後に呼ぶ
|
|
|
|
|
|
* コンポーネント作成直後でinitが呼ばれた時はonMountedで呼ばれる
|
|
|
|
|
|
* reloadでinitが呼ばれた時はreload内でinitの後に呼ばれる
|
|
|
|
|
|
*/
|
|
|
|
|
|
function scrollAfterInit() {
|
|
|
|
|
|
if (props.pagination.reversed) {
|
|
|
|
|
|
nextTick(() => {
|
2023-07-13 05:27:53 +00:00
|
|
|
|
setTimeout(async () => {
|
2023-07-11 14:39:59 +00:00
|
|
|
|
if (contentEl) {
|
|
|
|
|
|
scrollToBottom(contentEl);
|
|
|
|
|
|
// scrollToしてもbacked周りがうまく動かないので手動で戻す必要がある
|
|
|
|
|
|
weakBacked = false;
|
|
|
|
|
|
}
|
2023-07-11 14:14:15 +00:00
|
|
|
|
}, 200);
|
|
|
|
|
|
|
|
|
|
|
|
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
|
|
|
|
|
// more = trueを遅らせる
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
moreFetching.value = false;
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
setTimeout(() => {
|
2023-07-11 14:39:59 +00:00
|
|
|
|
scrollToTop(scrollableElement);
|
|
|
|
|
|
// scrollToしてもbacked周りがうまく動かないので手動で戻す必要がある
|
|
|
|
|
|
weakBacked = false;
|
|
|
|
|
|
|
2023-07-11 14:14:15 +00:00
|
|
|
|
moreFetching.value = false;
|
|
|
|
|
|
}, 200);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const reload = async (): Promise<void> => {
|
|
|
|
|
|
await init();
|
|
|
|
|
|
scrollAfterInit();
|
2022-01-09 21:35:35 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2023-07-11 14:14:15 +00:00
|
|
|
|
if (props.pagination.params && isRef(props.pagination.params)) {
|
|
|
|
|
|
watch(props.pagination.params, reload, { deep: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-01-10 01:00:50 +09:00
|
|
|
|
const fetchMore = async (): Promise<void> => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
moreFetching.value = true;
|
|
|
|
|
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
|
|
|
|
|
await os.api(props.pagination.endpoint, {
|
|
|
|
|
|
...params,
|
2023-04-03 11:50:17 +09:00
|
|
|
|
limit: SECOND_FETCH_LIMIT,
|
2022-01-09 21:35:35 +09:00
|
|
|
|
...(props.pagination.offsetMode ? {
|
|
|
|
|
|
offset: offset.value,
|
|
|
|
|
|
} : {
|
2023-07-14 07:20:45 +00:00
|
|
|
|
untilId: Array.from(items.value.keys()).at(-1),
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}),
|
|
|
|
|
|
}).then(res => {
|
|
|
|
|
|
for (let i = 0; i < res.length; i++) {
|
|
|
|
|
|
const item = res[i];
|
2023-01-13 18:25:40 +09:00
|
|
|
|
if (i === 10) item._shouldInsertAd_ = true;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}
|
2023-01-13 18:25:40 +09:00
|
|
|
|
|
2023-07-25 03:40:42 +00:00
|
|
|
|
const reverseConcat = (_res) => adjustScroll(() => concatMapWithArray(items.value, _res));
|
2023-01-13 18:25:40 +09:00
|
|
|
|
|
2023-04-03 11:50:17 +09:00
|
|
|
|
if (res.length === 0) {
|
2023-01-13 18:25:40 +09:00
|
|
|
|
if (props.pagination.reversed) {
|
2023-07-18 07:24:44 +00:00
|
|
|
|
reverseConcat(res);
|
|
|
|
|
|
more.value = false;
|
|
|
|
|
|
moreFetching.value = false;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
} else {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
items.value = concatMapWithArray(items.value, res);
|
2023-04-03 11:50:17 +09:00
|
|
|
|
more.value = false;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
moreFetching.value = false;
|
|
|
|
|
|
}
|
2022-01-09 21:35:35 +09:00
|
|
|
|
} else {
|
2023-01-13 18:25:40 +09:00
|
|
|
|
if (props.pagination.reversed) {
|
2023-07-18 07:24:44 +00:00
|
|
|
|
reverseConcat(res);
|
|
|
|
|
|
more.value = true;
|
|
|
|
|
|
moreFetching.value = false;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
} else {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
items.value = concatMapWithArray(items.value, res);
|
2023-04-03 11:50:17 +09:00
|
|
|
|
more.value = true;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
moreFetching.value = false;
|
|
|
|
|
|
}
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}
|
|
|
|
|
|
offset.value += res.length;
|
2022-05-26 15:53:09 +02:00
|
|
|
|
}, err => {
|
2022-01-09 21:35:35 +09:00
|
|
|
|
moreFetching.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2022-01-10 01:00:50 +09:00
|
|
|
|
const fetchMoreAhead = async (): Promise<void> => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
moreFetching.value = true;
|
|
|
|
|
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
|
|
|
|
|
await os.api(props.pagination.endpoint, {
|
|
|
|
|
|
...params,
|
2023-04-03 11:50:17 +09:00
|
|
|
|
limit: SECOND_FETCH_LIMIT,
|
2022-01-09 21:35:35 +09:00
|
|
|
|
...(props.pagination.offsetMode ? {
|
|
|
|
|
|
offset: offset.value,
|
|
|
|
|
|
} : {
|
2023-07-14 07:20:45 +00:00
|
|
|
|
sinceId: Array.from(items.value.keys()).at(-1),
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}),
|
|
|
|
|
|
}).then(res => {
|
2023-04-03 11:50:17 +09:00
|
|
|
|
if (res.length === 0) {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
items.value = concatMapWithArray(items.value, res);
|
2023-04-03 11:50:17 +09:00
|
|
|
|
more.value = false;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
} else {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
items.value = concatMapWithArray(items.value, res);
|
2023-04-03 11:50:17 +09:00
|
|
|
|
more.value = true;
|
2022-01-09 21:35:35 +09:00
|
|
|
|
}
|
|
|
|
|
|
offset.value += res.length;
|
|
|
|
|
|
moreFetching.value = false;
|
2022-05-26 15:53:09 +02:00
|
|
|
|
}, err => {
|
2022-01-09 21:35:35 +09:00
|
|
|
|
moreFetching.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-07-05 00:59:37 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
|
|
|
|
|
|
* APPEAR_MINIMUM_INTERVALミリ秒以内に2回fetchMoreが呼ばれるのを防ぐ
|
|
|
|
|
|
*/
|
|
|
|
|
|
const fetchMoreApperTimeoutFn = (): void => {
|
|
|
|
|
|
preventAppearFetchMore.value = false;
|
|
|
|
|
|
preventAppearFetchMoreTimer.value = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
const fetchMoreAppearTimeout = (): void => {
|
|
|
|
|
|
preventAppearFetchMore.value = true;
|
|
|
|
|
|
preventAppearFetchMoreTimer.value = window.setTimeout(fetchMoreApperTimeoutFn, APPEAR_MINIMUM_INTERVAL);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const appearFetchMore = async (): Promise<void> => {
|
|
|
|
|
|
if (preventAppearFetchMore.value) return;
|
|
|
|
|
|
await fetchMore();
|
|
|
|
|
|
fetchMoreAppearTimeout();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const appearFetchMoreAhead = async (): Promise<void> => {
|
|
|
|
|
|
if (preventAppearFetchMore.value) return;
|
|
|
|
|
|
await fetchMoreAhead();
|
|
|
|
|
|
fetchMoreAppearTimeout();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-07-11 07:53:56 +00:00
|
|
|
|
onActivated(() => {
|
2023-07-17 08:59:47 +00:00
|
|
|
|
nextTick(() => {
|
2023-07-17 09:11:31 +00:00
|
|
|
|
active.value = true;
|
2023-07-17 08:59:47 +00:00
|
|
|
|
});
|
2023-02-25 08:18:12 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2023-07-11 07:53:56 +00:00
|
|
|
|
onDeactivated(() => {
|
|
|
|
|
|
active.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-07-21 11:04:58 +00:00
|
|
|
|
watch([active, visibility], () => {
|
2023-07-18 13:19:04 +00:00
|
|
|
|
if (!backed && active.value && visibility.value === 'visible') {
|
2023-07-18 13:11:42 +00:00
|
|
|
|
executeQueue();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-07-05 00:59:37 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 最新のものとして1つだけアイテムを追加する
|
|
|
|
|
|
* ストリーミングから降ってきたアイテムはこれで追加する
|
|
|
|
|
|
* @param item アイテム
|
|
|
|
|
|
*/
|
2023-01-13 18:25:40 +09:00
|
|
|
|
const prepend = (item: MisskeyEntity): void => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
if (items.value.size === 0) {
|
|
|
|
|
|
items.value.set(item.id, item);
|
|
|
|
|
|
fetching.value = false;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2022-01-21 16:43:56 +09:00
|
|
|
|
|
2023-07-11 07:53:56 +00:00
|
|
|
|
if (
|
2023-07-18 07:15:20 +00:00
|
|
|
|
!isPausingUpdateByExecutingQueue.value && // スクロール調整中はキューに追加する
|
2023-07-18 13:02:20 +00:00
|
|
|
|
visibility.value !== 'hidden' && // バックグラウンドの場合はキューに追加する
|
2023-07-17 14:05:06 +00:00
|
|
|
|
queueSize.value === 0 && // キューに残っている場合はキューに追加する
|
2023-07-17 15:28:29 +00:00
|
|
|
|
active.value // keepAliveで隠されている間はキューに追加する
|
2023-07-11 07:53:56 +00:00
|
|
|
|
) {
|
2023-07-17 15:28:29 +00:00
|
|
|
|
if (!backed) {
|
2023-07-18 07:15:20 +00:00
|
|
|
|
// かなりスクロールの先頭にいる場合
|
2023-07-17 15:28:29 +00:00
|
|
|
|
if (items.value.has(item.id)) return; // 既にタイムラインにある場合は何もしない
|
2023-07-18 13:02:20 +00:00
|
|
|
|
unshiftItems([item]);
|
2023-07-26 12:22:26 +00:00
|
|
|
|
} else if (timelineBackTopBehavior.value === 'next' && !weakBacked) {
|
2023-07-17 15:28:29 +00:00
|
|
|
|
// ちょっと先頭にいる場合はスクロールを調整する
|
|
|
|
|
|
prependQueue(item);
|
|
|
|
|
|
executeQueue();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 先頭にいない場合はキューに追加する
|
|
|
|
|
|
prependQueue(item);
|
|
|
|
|
|
}
|
2023-07-11 07:53:56 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
prependQueue(item);
|
|
|
|
|
|
}
|
2023-01-13 18:25:40 +09:00
|
|
|
|
};
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
2023-07-05 00:59:37 +09:00
|
|
|
|
/**
|
2023-07-11 07:53:56 +00:00
|
|
|
|
* 新着アイテムをitemsの先頭に追加し、limitを適用する
|
2023-07-05 00:59:37 +09:00
|
|
|
|
* @param newItems 新しいアイテムの配列
|
2023-07-11 07:53:56 +00:00
|
|
|
|
* @param limit デフォルトはdisplayLimit
|
2023-07-05 00:59:37 +09:00
|
|
|
|
*/
|
2023-07-11 07:53:56 +00:00
|
|
|
|
function unshiftItems(newItems: MisskeyEntity[], limit = displayLimit.value) {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
const length = newItems.length + items.value.size;
|
2023-07-26 07:33:17 +00:00
|
|
|
|
items.value = new Map([...arrayToEntries(newItems), ...(newItems.length >= limit ? [] : items.value)].slice(0, limit));
|
2023-07-05 00:59:37 +09:00
|
|
|
|
|
2023-07-13 03:53:48 +00:00
|
|
|
|
if (length >= limit) more.value = true;
|
2023-07-05 00:59:37 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 古いアイテムをitemsの末尾に追加し、displayLimitを適用する
|
|
|
|
|
|
* @param oldItems 古いアイテムの配列
|
|
|
|
|
|
*/
|
|
|
|
|
|
function concatItems(oldItems: MisskeyEntity[]) {
|
|
|
|
|
|
const length = oldItems.length + items.value.size;
|
2023-07-11 07:53:56 +00:00
|
|
|
|
items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, displayLimit.value));
|
2023-01-13 18:25:40 +09:00
|
|
|
|
|
2023-07-11 07:53:56 +00:00
|
|
|
|
if (length >= displayLimit.value) more.value = true;
|
2023-01-13 18:25:40 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-07-13 03:53:48 +00:00
|
|
|
|
async function executeQueue() {
|
2023-07-28 15:58:00 +00:00
|
|
|
|
// キューが空の場合でもタイムライン表示数を制限する役割がある
|
2023-07-28 15:57:34 +00:00
|
|
|
|
// ため続行する!
|
|
|
|
|
|
// if (queue.value.size === 0) return;
|
|
|
|
|
|
|
2023-07-18 07:15:20 +00:00
|
|
|
|
if (isPausingUpdateByExecutingQueue.value) return;
|
2023-07-26 12:48:34 +00:00
|
|
|
|
if (timelineBackTopBehavior.value === 'newest') {
|
2023-07-26 07:33:17 +00:00
|
|
|
|
// Safariは最新のアイテムにするだけ
|
|
|
|
|
|
const newItems = Array.from(queue.value.values()).slice(-1 * props.pagination.limit);
|
|
|
|
|
|
unshiftItems(newItems);
|
|
|
|
|
|
queue.value = new Map();
|
2023-07-25 04:01:26 +00:00
|
|
|
|
} else {
|
2023-07-30 13:56:11 +00:00
|
|
|
|
if (queue.value.size > 0) {
|
|
|
|
|
|
const queueArr = Array.from(queue.value.entries());
|
|
|
|
|
|
queue.value = new Map(queueArr.slice(props.pagination.limit));
|
|
|
|
|
|
const newItems = Array.from({ length: Math.min(queueArr.length, props.pagination.limit) }, (_, i) => queueArr[i][1]).reverse();
|
|
|
|
|
|
isPausingUpdateByExecutingQueue.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
await adjustScroll(() => unshiftItems(newItems, Infinity));
|
|
|
|
|
|
backed = true;
|
|
|
|
|
|
}
|
2023-07-26 07:33:17 +00:00
|
|
|
|
|
2023-07-25 10:47:31 +00:00
|
|
|
|
denyMoveTransition.value = true;
|
2023-07-26 07:33:17 +00:00
|
|
|
|
items.value = new Map([...items.value].slice(0, displayLimit.value));
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
isPausingUpdateByExecutingQueue.value = false;
|
|
|
|
|
|
denyMoveTransition.value = false;
|
2023-07-25 04:01:26 +00:00
|
|
|
|
}
|
2023-01-13 18:25:40 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function prependQueue(newItem: MisskeyEntity) {
|
2023-07-11 14:14:15 +00:00
|
|
|
|
queue.value.set(newItem.id, newItem);
|
2023-01-13 18:25:40 +09:00
|
|
|
|
}
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
2023-07-05 00:59:37 +09:00
|
|
|
|
/*
|
|
|
|
|
|
* アイテムを末尾に追加する(使うの?)
|
|
|
|
|
|
*/
|
2023-01-13 18:25:40 +09:00
|
|
|
|
const appendItem = (item: MisskeyEntity): void => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
items.value.set(item.id, item);
|
2022-01-09 21:35:35 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2023-07-05 00:59:37 +09:00
|
|
|
|
const removeItem = (id: string) => {
|
|
|
|
|
|
items.value.delete(id);
|
|
|
|
|
|
queue.value.delete(id);
|
2022-05-15 15:20:01 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
const item = items.value.get(id);
|
|
|
|
|
|
if (item) items.value.set(id, replacer(item));
|
|
|
|
|
|
|
|
|
|
|
|
const queueItem = queue.value.get(id);
|
|
|
|
|
|
if (queueItem) queue.value.set(id, replacer(queueItem));
|
2022-01-10 00:45:20 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
const inited = init();
|
2022-01-09 21:35:35 +09:00
|
|
|
|
|
2023-01-13 18:25:40 +09:00
|
|
|
|
onMounted(() => {
|
2023-07-11 15:17:52 +00:00
|
|
|
|
active.value = true;
|
2023-07-11 14:14:15 +00:00
|
|
|
|
inited.then(scrollAfterInit);
|
2023-01-13 18:25:40 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2023-07-05 00:59:37 +09:00
|
|
|
|
if (preventAppearFetchMoreTimer.value) {
|
|
|
|
|
|
clearTimeout(preventAppearFetchMoreTimer.value);
|
|
|
|
|
|
preventAppearFetchMoreTimer.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
scrollObserver?.disconnect();
|
2023-07-13 07:20:11 +00:00
|
|
|
|
if (scrollRemove) scrollRemove();
|
2022-01-09 21:35:35 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({
|
2022-01-10 20:17:38 +09:00
|
|
|
|
items,
|
2022-04-30 21:52:07 +09:00
|
|
|
|
queue,
|
2023-01-13 18:25:40 +09:00
|
|
|
|
more,
|
|
|
|
|
|
inited,
|
2023-07-11 14:39:59 +00:00
|
|
|
|
queueSize,
|
2023-07-24 07:19:22 +00:00
|
|
|
|
backed: $$(backed),
|
2022-01-09 21:35:35 +09:00
|
|
|
|
reload,
|
|
|
|
|
|
prepend,
|
2023-01-13 18:25:40 +09:00
|
|
|
|
append: appendItem,
|
2022-05-15 15:20:01 +02:00
|
|
|
|
removeItem,
|
2022-01-10 00:45:20 +09:00
|
|
|
|
updateItem,
|
2020-01-30 04:37:25 +09:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2023-01-14 15:01:28 +09:00
|
|
|
|
<style lang="scss" module>
|
|
|
|
|
|
.transition_fade_enterActive,
|
|
|
|
|
|
.transition_fade_leaveActive {
|
2021-04-22 22:29:33 +09:00
|
|
|
|
transition: opacity 0.125s ease;
|
|
|
|
|
|
}
|
2023-01-14 15:01:28 +09:00
|
|
|
|
.transition_fade_enterFrom,
|
|
|
|
|
|
.transition_fade_leaveTo {
|
2021-04-22 22:29:33 +09:00
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-14 15:01:28 +09:00
|
|
|
|
.more {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
margin-right: auto;
|
2020-01-30 04:37:25 +09:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|