summaryrefslogtreecommitdiff
path: root/packages/client/src/components/ui
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-01-09 21:35:35 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-01-09 21:35:35 +0900
commit586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948 (patch)
tree75b572d160488a29bcca19b12a72072bb2fa458e /packages/client/src/components/ui
parentbye chat ui (diff)
downloadmisskey-586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948.tar.gz
misskey-586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948.tar.bz2
misskey-586c11251a8c0e7ca9f8f3bbaad9bf745e6ef948.zip
wip: migrate paging components to composition api
#7681
Diffstat (limited to 'packages/client/src/components/ui')
-rw-r--r--packages/client/src/components/ui/pagination.vue258
1 files changed, 230 insertions, 28 deletions
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 64af4a54f7..79744e528d 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -13,43 +13,247 @@
</slot>
</div>
- <div v-else class="cxiknjgy">
+ <div v-else ref="rootEl">
<slot :items="items"></slot>
- <div v-show="more" key="_more_" class="more _gap">
- <MkButton v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
+ <div v-show="more" key="_more_" class="cxiknjgy _gap">
+ <MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
+ {{ $ts.loadMore }}
</MkButton>
+ <MkLoading v-else class="loading"/>
</div>
</div>
</transition>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from './button.vue';
-import paging from '@/scripts/paging';
+<script lang="ts" setup>
+import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
+import * as misskey from 'misskey-js';
+import * as os from '@/os';
+import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
+import MkButton from '@/components/ui/button.vue';
-export default defineComponent({
- components: {
- MkButton
- },
+const SECOND_FETCH_LIMIT = 30;
- mixins: [
- paging({}),
- ],
+export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
+ endpoint: E;
+ limit: number;
+ params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>;
- props: {
- pagination: {
- required: true
- },
+ /**
+ * 検索APIのような、ページング不可なエンドポイントを利用する場合
+ * (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
+ */
+ noPaging?: boolean;
- disableAutoLoad: {
- type: Boolean,
- required: false,
- default: false,
+ /**
+ * items 配列の中身を逆順にする(新しい方が最後)
+ */
+ reversed?: boolean;
+};
+
+const props = withDefaults(defineProps<{
+ pagination: Paging;
+ disableAutoLoad?: boolean;
+ displayLimit?: number;
+}>(), {
+ displayLimit: 30,
+});
+
+const rootEl = ref<HTMLElement>();
+const items = ref([]);
+const queue = ref([]);
+const offset = ref(0);
+const fetching = ref(true);
+const moreFetching = ref(false);
+const inited = ref(false);
+const more = ref(false);
+const backed = ref(false); // 遡り中か否か
+const isBackTop = ref(false);
+const empty = computed(() => items.value.length === 0 && !fetching.value && inited.value);
+const error = computed(() => !fetching.value && !inited.value);
+
+const init = async () => {
+ queue.value = [];
+ 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,
+ limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
+ }).then(res => {
+ for (let i = 0; i < res.length; i++) {
+ const item = res[i];
+ markRaw(item);
+ if (props.pagination.reversed) {
+ if (i === res.length - 2) item._shouldInsertAd_ = true;
+ } else {
+ if (i === 3) item._shouldInsertAd_ = true;
+ }
+ }
+ if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
+ res.pop();
+ items.value = props.pagination.reversed ? [...res].reverse() : res;
+ more.value = true;
+ } else {
+ items.value = props.pagination.reversed ? [...res].reverse() : res;
+ more.value = false;
+ }
+ offset.value = res.length;
+ inited.value = true;
+ fetching.value = false;
+ }, e => {
+ fetching.value = false;
+ });
+};
+
+const reload = () => {
+ items.value = [];
+ init();
+};
+
+const fetchMore = async () => {
+ if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
+ moreFetching.value = true;
+ backed.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,
+ limit: SECOND_FETCH_LIMIT + 1,
+ ...(props.pagination.offsetMode ? {
+ offset: offset.value,
+ } : {
+ untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
+ }),
+ }).then(res => {
+ for (let i = 0; i < res.length; i++) {
+ const item = res[i];
+ markRaw(item);
+ if (props.pagination.reversed) {
+ if (i === res.length - 9) item._shouldInsertAd_ = true;
+ } else {
+ if (i === 10) item._shouldInsertAd_ = true;
+ }
+ }
+ if (res.length > SECOND_FETCH_LIMIT) {
+ res.pop();
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = true;
+ } else {
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = false;
+ }
+ offset.value += res.length;
+ moreFetching.value = false;
+ }, e => {
+ moreFetching.value = false;
+ });
+};
+
+const fetchMoreAhead = async () => {
+ if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
+ 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,
+ limit: SECOND_FETCH_LIMIT + 1,
+ ...(props.pagination.offsetMode ? {
+ offset: offset.value,
+ } : {
+ sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
+ }),
+ }).then(res => {
+ for (const item of res) {
+ markRaw(item);
+ }
+ if (res.length > SECOND_FETCH_LIMIT) {
+ res.pop();
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = true;
+ } else {
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = false;
+ }
+ offset.value += res.length;
+ moreFetching.value = false;
+ }, e => {
+ moreFetching.value = false;
+ });
+};
+
+const prepend = (item) => {
+ if (props.pagination.reversed) {
+ const container = getScrollContainer(rootEl.value);
+ const pos = getScrollPosition(rootEl.value);
+ const viewHeight = container.clientHeight;
+ const height = container.scrollHeight;
+ const isBottom = (pos + viewHeight > height - 32);
+ if (isBottom) {
+ // オーバーフローしたら古いアイテムは捨てる
+ if (items.value.length >= props.displayLimit) {
+ // このやり方だとVue 3.2以降アニメーションが動かなくなる
+ //items.value = items.value.slice(-props.displayLimit);
+ while (items.value.length >= props.displayLimit) {
+ items.value.shift();
+ }
+ more.value = true;
+ }
+ }
+ items.value.push(item);
+ // TODO
+ } else {
+ const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
+ console.log(item, top);
+
+ if (isTop) {
+ // Prepend the item
+ items.value.unshift(item);
+
+ // オーバーフローしたら古いアイテムは捨てる
+ if (items.value.length >= props.displayLimit) {
+ // このやり方だとVue 3.2以降アニメーションが動かなくなる
+ //this.items = items.value.slice(0, props.displayLimit);
+ while (items.value.length >= props.displayLimit) {
+ items.value.pop();
+ }
+ more.value = true;
+ }
+ } else {
+ queue.value.push(item);
+ onScrollTop(rootEl.value, () => {
+ for (const item of queue.value) {
+ prepend(item);
+ }
+ queue.value = [];
+ });
}
- },
+ }
+};
+
+const append = (item) => {
+ items.value.push(item);
+};
+
+watch(props.pagination.params, init, { deep: true });
+watch(queue, (a, b) => {
+ if (a.length === 0 && b.length === 0) return;
+ this.$emit('queue', queue.value.length);
+}, { deep: true });
+
+init();
+
+onActivated(() => {
+ isBackTop.value = false;
+});
+
+onDeactivated(() => {
+ isBackTop.value = window.scrollY === 0;
+});
+
+defineExpose({
+ items,
+ reload,
+ fetchMoreAhead,
+ prepend,
+ append,
});
</script>
@@ -64,11 +268,9 @@ export default defineComponent({
}
.cxiknjgy {
- > .more > .button {
+ > .button {
margin-left: auto;
margin-right: auto;
- height: 48px;
- min-width: 150px;
}
}
</style>