fix(frontend): ページ遷移でスクロール位置が保持されない問題を修正

Fix #11068
This commit is contained in:
syuilo 2023-07-08 15:30:36 +09:00
parent 644023316e
commit 15683370f0
6 changed files with 53 additions and 47 deletions

View File

@ -19,15 +19,16 @@
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
### Client ### Client
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
- ドライブファイルのメニューで画像をクロップできるように - ドライブファイルのメニューで画像をクロップできるように
- 画像を動画と同様に簡単に隠せるように - 画像を動画と同様に簡単に隠せるように
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外) - オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように - 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
### Server ### Server
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました

View File

@ -17,14 +17,14 @@
</template> </template>
</template> </template>
<div :class="$style.root" style="container-type: inline-size;"> <div ref="contents" :class="$style.root" style="container-type: inline-size;">
<RouterView :key="reloadCount" :router="router"/> <RouterView :key="reloadCount" :router="router"/>
</div> </div>
</MkWindow> </MkWindow>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, onMounted, onUnmounted, provide } from 'vue'; import { ComputedRef, onMounted, onUnmounted, provide, shallowRef } from 'vue';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout'; import { popout as _popout } from '@/scripts/popout';
@ -32,11 +32,12 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
import { url } from '@/config'; import { url } from '@/config';
import { mainRouter, routes, page } from '@/router'; import { mainRouter, routes, page } from '@/router';
import { $i } from '@/account'; import { $i } from '@/account';
import { Router } from '@/nirax'; import { Router, useScrollPositionManager } from '@/nirax';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
import { openingWindowsCount } from '@/os'; import { openingWindowsCount } from '@/os';
import { claimAchievement } from '@/scripts/achievements'; import { claimAchievement } from '@/scripts/achievements';
import { getScrollContainer } from '@/scripts/scroll';
const props = defineProps<{ const props = defineProps<{
initialPath: string; initialPath: string;
@ -48,6 +49,7 @@ defineEmits<{
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue'))); const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
const contents = shallowRef<HTMLElement>();
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let windowEl = $shallowRef<InstanceType<typeof MkWindow>>(); let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
const history = $ref<{ path: string; key: any; }[]>([{ const history = $ref<{ path: string; key: any; }[]>([{
@ -139,6 +141,8 @@ function popout() {
windowEl.close(); windowEl.close();
} }
useScrollPositionManager(() => getScrollContainer(contents.value), router);
onMounted(() => { onMounted(() => {
openingWindowsCount.value++; openingWindowsCount.value++;
if (openingWindowsCount.value >= 3) { if (openingWindowsCount.value >= 3) {

View File

@ -1,7 +1,7 @@
// NIRAX --- A lightweight router // NIRAX --- A lightweight router
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { Component, shallowRef, ShallowRef } from 'vue'; import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
import { safeURIDecode } from '@/scripts/safe-uri-decode'; import { safeURIDecode } from '@/scripts/safe-uri-decode';
type RouteDef = { type RouteDef = {
@ -267,13 +267,33 @@ export class Router extends EventEmitter<{
}); });
} }
public replace(path: string, key?: string | null, emitEvent = true) { public replace(path: string, key?: string | null) {
this.navigate(path, key); this.navigate(path, key);
if (emitEvent) {
this.emit('replace', {
path,
key: this.currentKey,
});
}
} }
} }
export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
const scrollPosStore = new Map<string, number>();
onMounted(() => {
const scrollContainer = getScrollContainer();
scrollContainer.addEventListener('scroll', () => {
scrollPosStore.set(router.getCurrentKey(), scrollContainer.scrollTop);
}, { passive: true });
router.addListener('change', ctx => {
const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
scrollContainer.scroll({ top: scrollPos, behavior: 'instant' });
if (scrollPos !== 0) {
window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
scrollContainer.scroll({ top: scrollPos, behavior: 'instant' });
}, 100);
}
});
router.addListener('same', () => {
scrollContainer.scroll({ top: 0, behavior: 'smooth' });
});
});
}

View File

@ -509,41 +509,12 @@ export const mainRouter = new Router(routes, location.pathname + location.search
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
// TODO: このファイルでスクロール位置も管理する設計だとdeckに対応できないのでなんとかする
// スクロール位置取得+スクロール位置設定関数をprovideする感じでも良いかも
const scrollPosStore = new Map<string, number>();
window.setInterval(() => {
scrollPosStore.set(window.history.state?.key, window.scrollY);
}, 1000);
mainRouter.addListener('push', ctx => { mainRouter.addListener('push', ctx => {
window.history.pushState({ key: ctx.key }, '', ctx.path); window.history.pushState({ key: ctx.key }, '', ctx.path);
const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
window.scroll({ top: scrollPos, behavior: 'instant' });
if (scrollPos !== 0) {
window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
window.scroll({ top: scrollPos, behavior: 'instant' });
}, 100);
}
});
mainRouter.addListener('replace', ctx => {
window.history.replaceState({ key: ctx.key }, '', ctx.path);
});
mainRouter.addListener('same', () => {
window.scroll({ top: 0, behavior: 'smooth' });
}); });
window.addEventListener('popstate', (event) => { window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key, false); mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
const scrollPos = scrollPosStore.get(event.state?.key) ?? 0;
window.scroll({ top: scrollPos, behavior: 'instant' });
window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
window.scroll({ top: scrollPos, behavior: 'instant' });
}, 100);
}); });
export function useRouter(): Router { export function useRouter(): Router {

View File

@ -7,24 +7,29 @@
</template> </template>
</template> </template>
<div ref="contents">
<RouterView @contextmenu.stop="onContextmenu"/> <RouterView @contextmenu.stop="onContextmenu"/>
</div>
</XColumn> </XColumn>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, provide } from 'vue'; import { ComputedRef, provide, shallowRef } from 'vue';
import XColumn from './column.vue'; import XColumn from './column.vue';
import { deckStore, Column } from '@/ui/deck/deck-store'; import { deckStore, Column } from '@/ui/deck/deck-store';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
import { useScrollPositionManager } from '@/nirax';
import { getScrollContainer } from '@/scripts/scroll';
defineProps<{ defineProps<{
column: Column; column: Column;
isStacked: boolean; isStacked: boolean;
}>(); }>();
const contents = shallowRef<HTMLElement>();
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
provide('router', mainRouter); provide('router', mainRouter);
@ -61,4 +66,6 @@ function onContextmenu(ev: MouseEvent) {
}, },
}], ev); }], ev);
} }
useScrollPositionManager(() => getScrollContainer(contents.value), mainRouter);
</script> </script>

View File

@ -95,6 +95,7 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
import { deviceKind } from '@/scripts/device-kind'; import { deviceKind } from '@/scripts/device-kind';
import { miLocalStorage } from '@/local-storage'; import { miLocalStorage } from '@/local-storage';
import { CURRENT_STICKY_BOTTOM } from '@/const'; import { CURRENT_STICKY_BOTTOM } from '@/const';
import { useScrollPositionManager } from '@/nirax';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
@ -213,6 +214,8 @@ watch($$(navFooter), () => {
}, { }, {
immediate: true, immediate: true,
}); });
useScrollPositionManager(() => contents.value.rootEl, mainRouter);
</script> </script>
<style> <style>