summaryrefslogtreecommitdiff
path: root/packages/frontend/src/use/use-scroll-position-keeper.ts
blob: b584171cbe409e8201a9945008114a5c928c7f32 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
 * SPDX-FileCopyrightText: syuilo and misskey-project
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import { throttle } from 'throttle-debounce';
import { nextTick, onActivated, onDeactivated, onUnmounted, watch } from 'vue';
import type { Ref } from 'vue';

// note render skippingがオンだとズレるため、遷移直前にスクロール範囲に表示されているdata-scroll-anchor要素を特定して、復元時に当該要素までスクロールするようにする

// TODO: data-scroll-anchor がひとつも存在しない場合、または手動で useAnchor みたいなフラグをfalseで呼ばれた場合、単純にスクロール位置を使用する処理にフォールバックするようにする

export function useScrollPositionKeeper(scrollContainerRef: Ref<HTMLElement | null | undefined>): void {
	let anchorId: string | null = null;
	let ready = true;

	watch(scrollContainerRef, (el) => {
		if (!el) return;

		const onScroll = () => {
			if (!el) return;
			if (!ready) return;

			const scrollContainerRect = el.getBoundingClientRect();
			const viewPosition = scrollContainerRect.height / 2;

			const anchorEls = el.querySelectorAll('[data-scroll-anchor]');
			for (let i = anchorEls.length - 1; i > -1; i--) { // 下から見た方が速い
				const anchorEl = anchorEls[i] as HTMLElement;
				const anchorRect = anchorEl.getBoundingClientRect();
				const anchorTop = anchorRect.top;
				const anchorBottom = anchorRect.bottom;
				if (anchorTop <= viewPosition && anchorBottom >= viewPosition) {
					anchorId = anchorEl.getAttribute('data-scroll-anchor');
					break;
				}
			}
		};

		// ほんとはscrollイベントじゃなくてonBeforeDeactivatedでやりたい
		// https://github.com/vuejs/vue/issues/9454
		// https://github.com/vuejs/rfcs/pull/284
		el.addEventListener('scroll', throttle(1000, onScroll), { passive: true });
	}, {
		immediate: true,
	});

	const restore = () => {
		if (!anchorId) return;
		const scrollContainer = scrollContainerRef.value;
		if (!scrollContainer) return;
		const scrollAnchorEl = scrollContainer.querySelector(`[data-scroll-anchor="${anchorId}"]`);
		if (!scrollAnchorEl) return;
		scrollAnchorEl.scrollIntoView({
			behavior: 'instant',
			block: 'center',
			inline: 'center',
		});
	};

	onDeactivated(() => {
		ready = false;
	});

	onActivated(() => {
		restore();
		nextTick(() => {
			restore();
			window.setTimeout(() => {
				restore();

				ready = true;
			}, 100);
		});
	});
}