mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-22 14:05:12 +00:00
refactor: pagination/date-separated-list系処理を良い感じに? (#8209)
* pages/messaging/messaging-room.vue * wip * wip * wip??? * wip? * ✌️ * messaaging-room.form.vue rewrite to compositon api * refactor * 関心事でないのでとりあえず置いておく * 🎨 * 🎨 * i18n.ts * fix scroll container find function * fix * FIX * ✌️ * Fix scroll bottom detect * wip * aaaaaaaaaaa * rename * fix * fix? * ✌️ * ✌️ * clean up * clena up * refactor * scroll event once or not * fix * fix once * add safe-area-inset-bottom to spacer * fix * ✌️ * 🎨 * fix * fix * wip * ✌️ * clean up * fix lint * Update packages/client/src/components/global/sticky-container.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * Update packages/client/src/components/ui/pagination.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * Update packages/client/src/pages/messaging/messaging-room.form.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * clean up: single line comment * https://github.com/misskey-dev/misskey/pull/8209#discussion_r867386077 * fix * asobi → tolerance * pick form * pick message * pick room * fix lint * fix scroll? * fix scroll.ts * fix directives/sticky-container * update global/sticky-container.vue * fix, 🎨 * revert merge * ✌️ * fix lint errors * 🎨 * Update packages/client/src/types/date-separated-list.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * https://github.com/misskey-dev/misskey/pull/8209#discussion_r917225080 * use ' * Update packages/client/src/scripts/scroll.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * use Number.EPSILON Co-authored-by: acid-chicken <root@acid-chicken.com> * revert * fix * fix * Use % instead of vh * 🎨 * 🎨 * 🎨 * wip * wip * css modules Co-authored-by: Johann150 <johann.galle@protonmail.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
parent
519a08f8b5
commit
d2204fd5c8
9 changed files with 457 additions and 278 deletions
|
@ -1,13 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
|
import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue';
|
||||||
import MkAd from '@/components/global/MkAd.vue';
|
import MkAd from '@/components/global/MkAd.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
|
type: Array as PropType<MisskeyEntity[]>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
|
@ -33,6 +34,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props, { slots, expose }) {
|
setup(props, { slots, expose }) {
|
||||||
|
const $style = useCssModule();
|
||||||
function getDateText(time: string) {
|
function getDateText(time: string) {
|
||||||
const date = new Date(time).getDate();
|
const date = new Date(time).getDate();
|
||||||
const month = new Date(time).getMonth() + 1;
|
const month = new Date(time).getMonth() + 1;
|
||||||
|
@ -57,21 +59,25 @@ export default defineComponent({
|
||||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
||||||
) {
|
) {
|
||||||
const separator = h('div', {
|
const separator = h('div', {
|
||||||
class: 'separator',
|
class: $style['separator'],
|
||||||
key: item.id + ':separator',
|
key: item.id + ':separator',
|
||||||
}, h('p', {
|
}, h('p', {
|
||||||
class: 'date',
|
class: $style['date'],
|
||||||
}, [
|
}, [
|
||||||
h('span', [
|
h('span', {
|
||||||
|
class: $style['date-1'],
|
||||||
|
}, [
|
||||||
h('i', {
|
h('i', {
|
||||||
class: 'ti ti-chevron-up icon',
|
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||||
}),
|
}),
|
||||||
getDateText(item.createdAt),
|
getDateText(item.createdAt),
|
||||||
]),
|
]),
|
||||||
h('span', [
|
h('span', {
|
||||||
|
class: $style['date-2'],
|
||||||
|
}, [
|
||||||
getDateText(props.items[i + 1].createdAt),
|
getDateText(props.items[i + 1].createdAt),
|
||||||
h('i', {
|
h('i', {
|
||||||
class: 'ti ti-chevron-down icon',
|
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
]));
|
]));
|
||||||
|
@ -89,26 +95,62 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onBeforeLeave(el: HTMLElement) {
|
||||||
|
el.style.top = `${el.offsetTop}px`;
|
||||||
|
el.style.left = `${el.offsetLeft}px`;
|
||||||
|
}
|
||||||
|
function onLeaveCanceled(el: HTMLElement) {
|
||||||
|
el.style.top = '';
|
||||||
|
el.style.left = '';
|
||||||
|
}
|
||||||
|
|
||||||
return () => h(
|
return () => h(
|
||||||
defaultStore.state.animation ? TransitionGroup : 'div',
|
defaultStore.state.animation ? TransitionGroup : 'div',
|
||||||
defaultStore.state.animation ? {
|
{
|
||||||
class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
|
class: {
|
||||||
name: 'list',
|
[$style['date-separated-list']]: true,
|
||||||
tag: 'div',
|
[$style['date-separated-list-nogap']]: props.noGap,
|
||||||
'data-direction': props.direction,
|
[$style['reversed']]: props.reversed,
|
||||||
'data-reversed': props.reversed ? 'true' : 'false',
|
[$style['direction-down']]: props.direction === 'down',
|
||||||
} : {
|
[$style['direction-up']]: props.direction === 'up',
|
||||||
class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
|
},
|
||||||
|
...(defaultStore.state.animation ? {
|
||||||
|
name: 'list',
|
||||||
|
tag: 'div',
|
||||||
|
onBeforeLeave,
|
||||||
|
onLeaveCanceled,
|
||||||
|
} : {}),
|
||||||
},
|
},
|
||||||
{ default: renderChildren });
|
{ default: renderChildren });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" module>
|
||||||
.sqadhkmv {
|
.date-separated-list {
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
|
|
||||||
|
&:global {
|
||||||
|
> .list-move {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.deny-move-transition > .list-move {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .list-leave-active,
|
||||||
|
> .list-enter-active {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .list-leave-from,
|
||||||
|
> .list-leave-to,
|
||||||
|
> .list-leave-active {
|
||||||
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
|
||||||
> *:empty {
|
> *:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -116,73 +158,75 @@ export default defineComponent({
|
||||||
> *:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-bottom: var(--margin);
|
margin-bottom: var(--margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .list-move {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .list-enter-active {
|
.date-separated-list-nogap {
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
> * {
|
||||||
}
|
margin: 0 !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
&[data-direction="up"] {
|
&:not(:last-child) {
|
||||||
> .list-enter-from {
|
border-bottom: solid 0.5px var(--divider);
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(64px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-direction="down"] {
|
|
||||||
> .list-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-64px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .separator {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> .date {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 16px;
|
|
||||||
line-height: 32px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--dateLabelFg);
|
|
||||||
|
|
||||||
> span {
|
|
||||||
&:first-child {
|
|
||||||
margin-right: 8px;
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.noGap {
|
|
||||||
> * {
|
|
||||||
margin: 0 !important;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.direction-up {
|
||||||
|
&:global {
|
||||||
|
> .list-enter-from,
|
||||||
|
> .list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(64px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.direction-down {
|
||||||
|
&:global {
|
||||||
|
> .list-enter-from,
|
||||||
|
> .list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-64px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reversed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--dateLabelFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-1 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-1-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-2 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-2-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,16 @@
|
||||||
|
|
||||||
<template #default="{ items: notes }">
|
<template #default="{ items: notes }">
|
||||||
<div :class="[$style.root, { [$style.noGap]: noGap }]">
|
<div :class="[$style.root, { [$style.noGap]: noGap }]">
|
||||||
<MkDateSeparatedList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" :class="$style.notes">
|
<MkDateSeparatedList
|
||||||
|
ref="notes"
|
||||||
|
v-slot="{ item: note }"
|
||||||
|
:items="notes"
|
||||||
|
:direction="pagination.reversed ? 'up' : 'down'"
|
||||||
|
:reversed="pagination.reversed"
|
||||||
|
:no-gap="noGap"
|
||||||
|
:ad="true"
|
||||||
|
:class="$style.notes"
|
||||||
|
>
|
||||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
|
|
||||||
<div v-else ref="rootEl">
|
<div v-else ref="rootEl">
|
||||||
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
||||||
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading"/>
|
<MkLoading v-else class="loading"/>
|
||||||
</div>
|
</div>
|
||||||
<slot :items="items"></slot>
|
<slot :items="items" :fetching="fetching || moreFetching"></slot>
|
||||||
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
||||||
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading"/>
|
<MkLoading v-else class="loading"/>
|
||||||
|
@ -31,15 +31,18 @@
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, shallowRef, watch } from 'vue';
|
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const SECOND_FETCH_LIMIT = 30;
|
const SECOND_FETCH_LIMIT = 30;
|
||||||
|
const TOLERANCE = 16;
|
||||||
|
|
||||||
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
||||||
endpoint: E;
|
endpoint: E;
|
||||||
|
@ -58,8 +61,11 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
|
||||||
reversed?: boolean;
|
reversed?: boolean;
|
||||||
|
|
||||||
offsetMode?: boolean;
|
offsetMode?: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
|
pageEl?: HTMLElement;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script lang="ts" setup>
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pagination: Paging;
|
pagination: Paging;
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
|
@ -72,21 +78,73 @@ const emit = defineEmits<{
|
||||||
(ev: 'queue', count: number): void;
|
(ev: 'queue', count: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type Item = { id: string; [another: string]: unknown; };
|
let rootEl = $shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const rootEl = shallowRef<HTMLElement>();
|
// 遡り中かどうか
|
||||||
const items = ref<Item[]>([]);
|
let backed = $ref(false);
|
||||||
const queue = ref<Item[]>([]);
|
|
||||||
|
let scrollRemove = $ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const items = ref<MisskeyEntity[]>([]);
|
||||||
|
const queue = ref<MisskeyEntity[]>([]);
|
||||||
const offset = ref(0);
|
const offset = ref(0);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const moreFetching = ref(false);
|
const moreFetching = ref(false);
|
||||||
const more = ref(false);
|
const more = ref(false);
|
||||||
const backed = ref(false); // 遡り中か否か
|
|
||||||
const isBackTop = ref(false);
|
const isBackTop = ref(false);
|
||||||
const empty = computed(() => items.value.length === 0);
|
const empty = computed(() => items.value.length === 0);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
const {
|
||||||
|
enableInfiniteScroll
|
||||||
|
} = defaultStore.reactiveState;
|
||||||
|
|
||||||
const init = async (): Promise<void> => {
|
const contentEl = $computed(() => props.pagination.pageEl || rootEl);
|
||||||
|
const scrollableElement = $computed(() => getScrollContainer(contentEl));
|
||||||
|
|
||||||
|
// 先頭が表示されているかどうかを検出
|
||||||
|
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
|
||||||
|
let scrollObserver = $ref<IntersectionObserver>();
|
||||||
|
|
||||||
|
watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
|
||||||
|
if (scrollObserver) scrollObserver.disconnect();
|
||||||
|
|
||||||
|
scrollObserver = new IntersectionObserver(entries => {
|
||||||
|
backed = entries[0].isIntersecting;
|
||||||
|
}, {
|
||||||
|
root: scrollableElement,
|
||||||
|
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
||||||
|
threshold: 0.01,
|
||||||
|
});
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch($$(rootEl), () => {
|
||||||
|
scrollObserver.disconnect();
|
||||||
|
nextTick(() => {
|
||||||
|
if (rootEl) scrollObserver.observe(rootEl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([$$(backed), $$(contentEl)], () => {
|
||||||
|
if (!backed) {
|
||||||
|
if (!contentEl) return;
|
||||||
|
|
||||||
|
scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
|
||||||
|
} else {
|
||||||
|
if (scrollRemove) scrollRemove();
|
||||||
|
scrollRemove = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||||
|
watch(props.pagination.params, init, { deep: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(queue, (a, b) => {
|
||||||
|
if (a.length === 0 && b.length === 0) return;
|
||||||
|
emit('queue', queue.value.length);
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
async function init(): Promise<void> {
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
|
@ -96,18 +154,15 @@ const init = async (): Promise<void> => {
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
const item = res[i];
|
const item = res[i];
|
||||||
if (props.pagination.reversed) {
|
if (i === 3) item._shouldInsertAd_ = true;
|
||||||
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))) {
|
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
if (props.pagination.reversed) moreFetching.value = true;
|
||||||
|
items.value = res;
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed ? [...res].reverse() : res;
|
items.value = res;
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
offset.value = res.length;
|
offset.value = res.length;
|
||||||
|
@ -117,17 +172,16 @@ const init = async (): Promise<void> => {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
const reload = (): void => {
|
const reload = (): Promise<void> => {
|
||||||
items.value = [];
|
items.value = [];
|
||||||
init();
|
return init();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMore = async (): Promise<void> => {
|
const fetchMore = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
backed.value = true;
|
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
|
@ -142,22 +196,52 @@ const fetchMore = async (): Promise<void> => {
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
const item = res[i];
|
const item = res[i];
|
||||||
if (props.pagination.reversed) {
|
if (i === 10) item._shouldInsertAd_ = true;
|
||||||
if (i === res.length - 9) item._shouldInsertAd_ = true;
|
|
||||||
} else {
|
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reverseConcat = _res => {
|
||||||
|
const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
|
||||||
|
const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
|
||||||
|
|
||||||
|
items.value = items.value.concat(_res);
|
||||||
|
|
||||||
|
return nextTick(() => {
|
||||||
|
if (scrollableElement) {
|
||||||
|
scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
|
||||||
|
} else {
|
||||||
|
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextTick();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (res.length > SECOND_FETCH_LIMIT) {
|
if (res.length > SECOND_FETCH_LIMIT) {
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
|
||||||
more.value = true;
|
if (props.pagination.reversed) {
|
||||||
|
reverseConcat(res).then(() => {
|
||||||
|
more.value = true;
|
||||||
|
moreFetching.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.value = items.value.concat(res);
|
||||||
|
more.value = true;
|
||||||
|
moreFetching.value = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
if (props.pagination.reversed) {
|
||||||
more.value = false;
|
reverseConcat(res).then(() => {
|
||||||
|
more.value = false;
|
||||||
|
moreFetching.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.value = items.value.concat(res);
|
||||||
|
more.value = false;
|
||||||
|
moreFetching.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
moreFetching.value = false;
|
|
||||||
}, err => {
|
}, err => {
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
});
|
});
|
||||||
|
@ -180,10 +264,10 @@ const fetchMoreAhead = async (): Promise<void> => {
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.length > SECOND_FETCH_LIMIT) {
|
if (res.length > SECOND_FETCH_LIMIT) {
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
items.value = items.value.concat(res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
|
items.value = items.value.concat(res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
|
@ -193,106 +277,96 @@ const fetchMoreAhead = async (): Promise<void> => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepend = (item: Item): void => {
|
const prepend = (item: MisskeyEntity): void => {
|
||||||
if (props.pagination.reversed) {
|
// 初回表示時はunshiftだけでOK
|
||||||
if (rootEl.value) {
|
if (!rootEl) {
|
||||||
const container = getScrollContainer(rootEl.value);
|
items.value.unshift(item);
|
||||||
if (container == null) {
|
return;
|
||||||
// TODO?
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
// 初回表示時はunshiftだけでOK
|
|
||||||
if (!rootEl.value) {
|
|
||||||
items.value.unshift(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
|
||||||
|
|
||||||
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 isTop = isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE);
|
||||||
|
|
||||||
|
if (isTop) unshiftItems([item]);
|
||||||
|
else prependQueue(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const append = (item: Item): void => {
|
function unshiftItems(newItems: MisskeyEntity[]) {
|
||||||
|
const length = newItems.length + items.value.length;
|
||||||
|
items.value = [ ...newItems, ...items.value ].slice(0, props.displayLimit);
|
||||||
|
|
||||||
|
if (length >= props.displayLimit) more.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeQueue() {
|
||||||
|
if (queue.value.length === 0) return;
|
||||||
|
unshiftItems(queue.value);
|
||||||
|
queue.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependQueue(newItem: MisskeyEntity) {
|
||||||
|
queue.value.unshift(newItem);
|
||||||
|
if (queue.value.length >= props.displayLimit) {
|
||||||
|
queue.value.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendItem = (item: MisskeyEntity): void => {
|
||||||
items.value.push(item);
|
items.value.push(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeItem = (finder: (item: Item) => boolean) => {
|
const removeItem = (finder: (item: MisskeyEntity) => boolean) => {
|
||||||
const i = items.value.findIndex(finder);
|
const i = items.value.findIndex(finder);
|
||||||
items.value.splice(i, 1);
|
items.value.splice(i, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
|
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
||||||
const i = items.value.findIndex(item => item.id === id);
|
const i = items.value.findIndex(item => item.id === id);
|
||||||
items.value[i] = replacer(items.value[i]);
|
items.value[i] = replacer(items.value[i]);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
const inited = init();
|
||||||
watch(props.pagination.params, init, { deep: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(queue, (a, b) => {
|
|
||||||
if (a.length === 0 && b.length === 0) return;
|
|
||||||
emit('queue', queue.value.length);
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
isBackTop.value = false;
|
isBackTop.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
isBackTop.value = window.scrollY === 0;
|
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl?.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toBottom() {
|
||||||
|
scrollToBottom(contentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
inited.then(() => {
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(toBottom, 800);
|
||||||
|
|
||||||
|
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||||
|
// more = trueを遅らせる
|
||||||
|
setTimeout(() => {
|
||||||
|
moreFetching.value = false;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
scrollObserver.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
items,
|
items,
|
||||||
queue,
|
queue,
|
||||||
backed,
|
backed,
|
||||||
|
more,
|
||||||
|
inited,
|
||||||
reload,
|
reload,
|
||||||
prepend,
|
prepend,
|
||||||
append,
|
append: appendItem,
|
||||||
removeItem,
|
removeItem,
|
||||||
updateItem,
|
updateItem,
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
v-for="(message, i) in messages"
|
v-for="(message, i) in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
v-anim="i"
|
v-anim="i"
|
||||||
class="message"
|
class="message _panel"
|
||||||
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
|
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
|
||||||
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
|
||||||
:data-index="i"
|
:data-index="i"
|
||||||
|
|
|
@ -256,9 +256,10 @@ defineExpose({
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background: transparent;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
|
background: rgba(12, 18, 16, 0.85);
|
||||||
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
|
|
@ -1,51 +1,48 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
class=""
|
class="root"
|
||||||
@dragover.prevent.stop="onDragover"
|
@dragover.prevent.stop="onDragover"
|
||||||
@drop.prevent.stop="onDrop"
|
@drop.prevent.stop="onDrop"
|
||||||
>
|
>
|
||||||
<div class="mk-messaging-room">
|
<div class="body">
|
||||||
<div class="body">
|
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
|
||||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
|
<template #empty>
|
||||||
<template #empty>
|
<div class="_fullinfo">
|
||||||
<div class="_fullinfo">
|
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
<div>{{ i18n.ts.noMessagesYet }}</div>
|
||||||
<div>{{ i18n.ts.noMessagesYet }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default="{ items: messages, fetching: pFetching }">
|
|
||||||
<MkDateSeparatedList
|
|
||||||
v-if="messages.length > 0"
|
|
||||||
v-slot="{ item: message }"
|
|
||||||
:class="{ messages: true, 'deny-move-transition': pFetching }"
|
|
||||||
:items="messages"
|
|
||||||
direction="up"
|
|
||||||
reversed
|
|
||||||
>
|
|
||||||
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
|
|
||||||
</MkDateSeparatedList>
|
|
||||||
</template>
|
|
||||||
</MkPagination>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<div v-if="typers.length > 0" class="typers">
|
|
||||||
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
|
|
||||||
<template #users>
|
|
||||||
<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<MkEllipsis/>
|
|
||||||
</div>
|
|
||||||
<Transition :name="animation ? 'fade' : ''">
|
|
||||||
<div v-show="showIndicator" class="new-message">
|
|
||||||
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</template>
|
||||||
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
|
<template #default="{ items: messages, fetching: pFetching }">
|
||||||
</footer>
|
<MkDateSeparatedList
|
||||||
|
v-if="messages.length > 0"
|
||||||
|
v-slot="{ item: message }"
|
||||||
|
:class="{ messages: true, 'deny-move-transition': pFetching }"
|
||||||
|
:items="messages"
|
||||||
|
direction="up"
|
||||||
|
reversed
|
||||||
|
>
|
||||||
|
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
|
||||||
|
</MkDateSeparatedList>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div v-if="typers.length > 0" class="typers">
|
||||||
|
<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
|
||||||
|
<template #users>
|
||||||
|
<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
<MkEllipsis/>
|
||||||
|
</div>
|
||||||
|
<Transition :name="animation ? 'fade' : ''">
|
||||||
|
<div v-show="showIndicator" class="new-message">
|
||||||
|
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -140,7 +137,9 @@ async function fetch() {
|
||||||
document.addEventListener('visibilitychange', onVisibilitychange);
|
document.addEventListener('visibilitychange', onVisibilitychange);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
thisScrollToBottom();
|
pagingComponent.inited.then(() => {
|
||||||
|
thisScrollToBottom();
|
||||||
|
});
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
fetching = false;
|
fetching = false;
|
||||||
}, 300);
|
}, 300);
|
||||||
|
@ -305,11 +304,12 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mk-messaging-room {
|
.root {
|
||||||
position: relative;
|
display: content;
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
min-height: 80%;
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 16px auto;
|
margin: 16px auto;
|
||||||
|
@ -349,8 +349,9 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
bottom: 0;
|
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
bottom: 0;
|
||||||
|
bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
|
||||||
> .new-message {
|
> .new-message {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -395,6 +396,8 @@ definePageMetadata(computed(() => !fetching ? user ? {
|
||||||
max-height: 12em;
|
max-height: 12em;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,53 +10,67 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScrollPosition(el: Element | null): number {
|
export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top: number = 0) {
|
||||||
|
if (!el.parentElement) return top;
|
||||||
|
const data = el.dataset.stickyContainerHeaderHeight;
|
||||||
|
const newTop = data ? Number(data) + top : top;
|
||||||
|
if (el === container) return newTop;
|
||||||
|
return getStickyTop(el.parentElement, container, newTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScrollPosition(el: HTMLElement | null): number {
|
||||||
const container = getScrollContainer(el);
|
const container = getScrollContainer(el);
|
||||||
return container == null ? window.scrollY : container.scrollTop;
|
return container == null ? window.scrollY : container.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTopVisible(el: Element | null): boolean {
|
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
|
||||||
const scrollTop = getScrollPosition(el);
|
// とりあえず評価してみる
|
||||||
const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
|
if (isTopVisible(el)) {
|
||||||
|
cb();
|
||||||
|
if (once) return null;
|
||||||
|
}
|
||||||
|
|
||||||
return scrollTop <= topPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
|
|
||||||
if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
|
|
||||||
return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onScrollTop(el: Element, cb) {
|
|
||||||
const container = getScrollContainer(el) || window;
|
const container = getScrollContainer(el) || window;
|
||||||
|
|
||||||
const onScroll = ev => {
|
const onScroll = ev => {
|
||||||
if (!document.body.contains(el)) return;
|
if (!document.body.contains(el)) return;
|
||||||
if (isTopVisible(el)) {
|
if (isTopVisible(el, tolerance)) {
|
||||||
cb();
|
cb();
|
||||||
container.removeEventListener('scroll', onScroll);
|
if (once) removeListener();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function removeListener() { container.removeEventListener('scroll', onScroll); }
|
||||||
container.addEventListener('scroll', onScroll, { passive: true });
|
container.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
return removeListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onScrollBottom(el: Element, cb) {
|
export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
|
||||||
const container = getScrollContainer(el) || window;
|
const container = getScrollContainer(el);
|
||||||
|
|
||||||
|
// とりあえず評価してみる
|
||||||
|
if (isBottomVisible(el, tolerance, container)) {
|
||||||
|
cb();
|
||||||
|
if (once) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerOrWindow = container || window;
|
||||||
const onScroll = ev => {
|
const onScroll = ev => {
|
||||||
if (!document.body.contains(el)) return;
|
if (!document.body.contains(el)) return;
|
||||||
const pos = getScrollPosition(el);
|
if (isBottomVisible(el, 1, container)) {
|
||||||
if (pos + el.clientHeight > el.scrollHeight - 1) {
|
|
||||||
cb();
|
cb();
|
||||||
container.removeEventListener('scroll', onScroll);
|
if (once) removeListener();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
container.addEventListener('scroll', onScroll, { passive: true });
|
|
||||||
|
function removeListener() {
|
||||||
|
containerOrWindow.removeEventListener('scroll', onScroll);
|
||||||
|
}
|
||||||
|
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
return removeListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scroll(el: Element, options: {
|
export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
|
||||||
top?: number;
|
|
||||||
left?: number;
|
|
||||||
behavior?: ScrollBehavior;
|
|
||||||
}) {
|
|
||||||
const container = getScrollContainer(el);
|
const container = getScrollContainer(el);
|
||||||
if (container == null) {
|
if (container == null) {
|
||||||
window.scroll(options);
|
window.scroll(options);
|
||||||
|
@ -65,21 +79,51 @@ export function scroll(el: Element, options: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
|
/**
|
||||||
|
* Scroll to Top
|
||||||
|
* @param el Scroll container element
|
||||||
|
* @param options Scroll options
|
||||||
|
*/
|
||||||
|
export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
|
||||||
scroll(el, { top: 0, ...options });
|
scroll(el, { top: 0, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
|
/**
|
||||||
scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
|
* Scroll to Bottom
|
||||||
|
* @param el Content element
|
||||||
|
* @param options Scroll options
|
||||||
|
* @param container Scroll container element
|
||||||
|
*/
|
||||||
|
export function scrollToBottom(
|
||||||
|
el: HTMLElement,
|
||||||
|
options: ScrollToOptions = {},
|
||||||
|
container = getScrollContainer(el),
|
||||||
|
) {
|
||||||
|
if (container) {
|
||||||
|
container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options });
|
||||||
|
} else {
|
||||||
|
window.scroll({
|
||||||
|
top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBottom(el: Element, asobi = 0) {
|
export function isTopVisible(el: HTMLElement, tolerance: number = 1): boolean {
|
||||||
const container = getScrollContainer(el);
|
const scrollTop = getScrollPosition(el);
|
||||||
const current = container
|
return scrollTop <= tolerance;
|
||||||
? el.scrollTop + el.offsetHeight
|
}
|
||||||
: window.scrollY + window.innerHeight;
|
|
||||||
const max = container
|
export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
|
||||||
? el.scrollHeight
|
if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
|
||||||
: document.body.offsetHeight;
|
return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
|
||||||
return current >= (max - asobi);
|
}
|
||||||
|
|
||||||
|
// https://ja.javascript.info/size-and-scroll-window#ref-932
|
||||||
|
export function getBodyScrollHeight() {
|
||||||
|
return Math.max(
|
||||||
|
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||||
|
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||||
|
document.body.clientHeight, document.documentElement.clientHeight
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
6
packages/frontend/src/types/date-separated-list.ts
Normal file
6
packages/frontend/src/types/date-separated-list.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export type MisskeyEntity = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
_shouldInsertAd_?: boolean;
|
||||||
|
[x: string]: any;
|
||||||
|
};
|
|
@ -37,12 +37,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import XHeader from './header.vue';
|
import XHeader from './header.vue';
|
||||||
import { host, instanceName } from '@/config';
|
import { host, instanceName } from '@/config';
|
||||||
import { search } from '@/scripts/search';
|
import { search } from '@/scripts/search';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { ColdDeviceStorage } from '@/store';
|
import { ColdDeviceStorage } from '@/store';
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
|
@ -52,7 +51,6 @@ const DESKTOP_THRESHOLD = 1100;
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XHeader,
|
XHeader,
|
||||||
MkPagination,
|
|
||||||
MkButton,
|
MkButton,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue