Skip to content

Commit

Permalink
* fix scrolling to post item after isFetchedAfterMount changed happ…
Browse files Browse the repository at this point in the history
…ens before any DOM inside `<RendererList>` gets rendered

* move `watchEffect()` as `scrollToPostListItemByRoute()` and `scrollToPostListItem()` to `components/Post/renderers/rendererList.ts`
@ `<Post>`

* remove addition function wrapping @ `<BilibiliVote>.watch()`
$ sed -ri 's/(RouteLocationNormalized)Loaded/\1; .
@ fe
  • Loading branch information
n0099 committed Feb 25, 2024
1 parent 3d0b4fb commit 27a0b19
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 49 deletions.
6 changes: 3 additions & 3 deletions fe/src/components/Post/queryForm/QueryForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ import type { RouteObjectRaw } from '@/stores/triggerRouteUpdate';
import { useTriggerRouteUpdateStore } from '@/stores/triggerRouteUpdate';
import { computed, ref, watch } from 'vue';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import type { RouteLocationNormalized } from 'vue-router';
import { useRouter } from 'vue-router';
import { RangePicker } from 'ant-design-vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
Expand Down Expand Up @@ -359,7 +359,7 @@ const checkParams = async (): Promise<boolean> => {
return _.isEmpty(invalidParamsIndex.value) && !(isOrderByInvalid.value || isFidInvalid.value);
};
const parseRoute = (route: RouteLocationNormalizedLoaded) => {
const parseRoute = (route: RouteLocationNormalized) => {
assertRouteNameIsStr(route.name);
uniqueParams.value = _.mapValues(uniqueParams.value, _.unary(fillParamDefaultValue)) as KnownUniqueParams;
params.value = [];
Expand All @@ -377,7 +377,7 @@ const parseRoute = (route: RouteLocationNormalizedLoaded) => {
fillParamDefaultValue({ name, value }));
}
};
const parseRouteToGetFlattenParams = async (route: RouteLocationNormalizedLoaded)
const parseRouteToGetFlattenParams = async (route: RouteLocationNormalized)
: Promise<ReturnType<typeof flattenParams> | false> => {
parseRoute(route);
if (await checkParams())
Expand Down
41 changes: 39 additions & 2 deletions fe/src/components/Post/renderers/rendererList.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getRouteCursorParam } from '@/router';
import { convertRemToPixels } from '@/shared';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import type { RouteLocationNormalized } from 'vue-router';
import * as _ from 'lodash-es';

export const getReplyTitleTopOffset = () =>
convertRemToPixels(5) - convertRemToPixels(0.625); // inset-block-start and margin-block-start
export const postListItemScrollPosition = (route: RouteLocationNormalizedLoaded)
export const postListItemScrollPosition = (route: RouteLocationNormalized)
: (ScrollToOptions & { el: string }) | false => {
const hash = route.hash.slice(1);
if (_.isEmpty(hash))
Expand All @@ -16,3 +16,40 @@ export const postListItemScrollPosition = (route: RouteLocationNormalizedLoaded)
top: hash.startsWith('t') ? 0 : getReplyTitleTopOffset()
};
};
const scrollToPostListItem = (el: Element) => {
// simply invoke el.scrollIntoView() for only once will scroll the element to the top of the viewport
// and then some other elements above it such as img[loading='lazy'] may change its box size
// that would lead to reflow resulting in the element being pushed down or up out of viewport
// due to document.scrollingElement.scrollTop changed a lot
const tryScroll = () => {
// not using a passive callback by IntersectionObserverto to prevent getBoundingClientRect() caused force reflow
// due to it will only emit once the configured thresholds are reached
// thus the top offset might be far from 0 that is top aligned with viewport when the callback is called
// since the element is still near the bottom of viewport at that point of time
// even if the thresholds steps by each percentage like [0.01, 0.02, ..., 1] to let triggers callback more often
// 1% of a very high element is still a big number that may not emit when scrolling ends
// and the element reached the top of viewport
const elTop = el.getBoundingClientRect().top;
const replyTitleTopOffset = getReplyTitleTopOffset();
if (Math.abs(elTop) < replyTitleTopOffset + (window.innerHeight * 0.05)) // at most 5dvh tolerance
removeEventListener('scrollend', tryScroll);
else
document.documentElement.scrollBy({ top: elTop - replyTitleTopOffset });
};
tryScroll();
addEventListener('scrollend', tryScroll);
};
export const scrollToPostListItemByRoute = (route: RouteLocationNormalized) => {
const scrollPosition = postListItemScrollPosition(route);
if (scrollPosition === false)
return;
const el = document.querySelector(scrollPosition.el);
if (el === null)
return;
requestIdleCallback(function retry(deadline) {
if (deadline.timeRemaining() > 0)
scrollToPostListItem(el);
else
requestIdleCallback(retry);
});
};
4 changes: 2 additions & 2 deletions fe/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Cursor } from '@/api/index.d';
import { notyShow } from '@/shared';
import type { Component } from 'vue';
import { onUnmounted, ref } from 'vue';
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordMultipleViews, RouteRecordMultipleViewsWithChildren, RouteRecordSingleView, RouteRecordSingleViewWithChildren, RouterScrollBehavior, _RouteRecordBase } from 'vue-router';
import type { RouteLocationNormalized, RouteRecordMultipleViews, RouteRecordMultipleViewsWithChildren, RouteRecordSingleView, RouteRecordSingleViewWithChildren, RouterScrollBehavior, _RouteRecordBase } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import nprogress from 'nprogress';
import * as _ from 'lodash-es';
Expand All @@ -15,7 +15,7 @@ export const setComponentCustomScrollBehaviour = (cb: RouterScrollBehavior) => {
onUnmounted(() => { componentCustomScrollBehaviour.value = undefined });
};

export const assertRouteNameIsStr: (name: RouteLocationNormalizedLoaded['name']) => asserts name is string = name => {
export const assertRouteNameIsStr: (name: RouteLocationNormalized['name']) => asserts name is string = name => {
if (!_.isString(name))
throw new Error('https://github.com/vuejs/vue-router-next/issues/1185');
}; // https://github.com/microsoft/TypeScript/issues/34523#issuecomment-700491122
Expand Down
4 changes: 2 additions & 2 deletions fe/src/views/BilibiliVote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -679,9 +679,9 @@ const loadCharts = {
};
watch(() => query.value.top5CandidateCountGroupByTimeGranularity,
() => { loadCharts.top5CandidateCountGroupByTime() });
loadCharts.top5CandidateCountGroupByTime);
watch(() => query.value.allVoteCountGroupByTimeGranularity,
() => { loadCharts.allVoteCountGroupByTime() });
loadCharts.allVoteCountGroupByTime);
onMounted(() => {
_.map(chartElements, (elRef, chartName: ChartName) => {
if (elRef.value === undefined)
Expand Down
43 changes: 5 additions & 38 deletions fe/src/views/Post.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ import PlaceholderPostList from '@/components/placeholders/PlaceholderPostList.v
import { useApiPosts } from '@/api';
import type { ApiPosts, Cursor } from '@/api/index.d';
import { getReplyTitleTopOffset, postListItemScrollPosition } from '@/components/Post/renderers/rendererList';
import { scrollToPostListItemByRoute } from '@/components/Post/renderers/rendererList';
import { compareRouteIsNewQuery, getRouteCursorParam } from '@/router';
import type { ObjUnknown } from '@/shared';
import { notyShow, scrollBarWidth, titleTemplate } from '@/shared';
import { useTriggerRouteUpdateStore } from '@/stores/triggerRouteUpdate';
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { onBeforeRouteUpdate, useRoute } from 'vue-router';
import { watchOnce } from '@vueuse/core';
Expand Down Expand Up @@ -102,43 +102,10 @@ const fetchPosts = (queryParams: ObjUnknown[], cursor: Cursor) => {
});
};
const scrollToPostListItem = (el: Element) => {
// simply invoke el.scrollIntoView() for only once will scroll the element to the top of the viewport
// and then some other elements above it such as img[loading='lazy'] may change its box size
// that would lead to reflow resulting in the element being pushed down or up out of viewport
// due to document.scrollingElement.scrollTop changed a lot
const tryScroll = () => {
// not using a passive callback by IntersectionObserverto to prevent getBoundingClientRect() caused force reflow
// due to it will only emit once the configured thresholds are reached
// thus the top offset might be far from 0 that is top aligned with viewport when the callback is called
// since the element is still near the bottom of viewport at that point of time
// even if the thresholds steps by each percentage like [0.01, 0.02, ..., 1] to let triggers callback more often
// 1% of a very high element is still a big number that may not emit when scrolling ends
// and the element reached the top of viewport
const elTop = el.getBoundingClientRect().top;
const replyTitleTopOffset = getReplyTitleTopOffset();
if (Math.abs(elTop) < replyTitleTopOffset + (window.innerHeight * 0.05)) // at most 5dvh tolerance
removeEventListener('scrollend', tryScroll);
else
document.documentElement.scrollBy({ top: elTop - replyTitleTopOffset });
};
tryScroll();
addEventListener('scrollend', tryScroll);
};
watchEffect(() => {
watch(isFetchedAfterMount, async () => {
if (isFetchedAfterMount.value && renderType.value === 'list') {
const scrollPosition = postListItemScrollPosition(lastFetchingRoute.value);
if (scrollPosition === false)
return;
const el = document.querySelector(scrollPosition.el);
if (el === null)
return;
requestIdleCallback(function retry(deadline) {
if (deadline.timeRemaining() > 0)
scrollToPostListItem(el);
else
requestIdleCallback(retry);
});
await nextTick();
scrollToPostListItemByRoute(lastFetchingRoute.value);
}
});
Expand Down
4 changes: 2 additions & 2 deletions fe/src/views/User.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { compareRouteIsNewQuery, getRouteCursorParam, routeNameSuffix, setCompon
import { notyShow, removeEnd, removeStart, titleTemplate } from '@/shared';
import { nextTick, onBeforeMount, ref, watchEffect } from 'vue';
import type { RouteLocationNormalizedLoaded, RouterScrollBehavior } from 'vue-router';
import type { RouteLocationNormalized, RouterScrollBehavior } from 'vue-router';
import { onBeforeRouteUpdate, useRoute } from 'vue-router';
import { useHead } from '@unhead/vue';
import * as _ from 'lodash-es';
Expand All @@ -43,7 +43,7 @@ const isLoading = ref<boolean>(false);
const lastFetchError = ref<ApiError | null>(null);
const showPlaceholderPostList = ref<boolean>(false);
const fetchUsers = async (_route: RouteLocationNormalizedLoaded, isNewQuery: boolean) => {
const fetchUsers = async (_route: RouteLocationNormalized, isNewQuery: boolean) => {
const startTime = Date.now();
const queryString = { ..._route.params, ..._route.query };
lastFetchError.value = null;
Expand Down

0 comments on commit 27a0b19

Please sign in to comment.