From 3bbae187d8931ba3400747b87add46355732bfa0 Mon Sep 17 00:00:00 2001 From: n0099 Date: Wed, 11 Sep 2024 13:25:02 +0000 Subject: [PATCH] * fix firefox compatibility: https://bugzilla.mozilla.org/show_bug.cgi?id=1918017 by watching the event listener on `window.resize` to re-calc the `window.height - topOffset` for the `rootMargin.bottom` of intersection observer @ stores/viewportTopmostPost.ts * fix firefox compatibility by feature detecting on `Element.computedStyleMap()` with fallbacking to `.getComputedStyle()`, and replace all `Element.attributeStyleMap.set()` with old-style `.style.setProperty()` @ utils/post/renderer/list/index.ts * replace the event listener on `window.resize` with `useResizeableEcharts()` that only observer resize events of given element(s) and replace `_.throttle` with `_.debounce` with a larger waiting window @ utils/echarts.ts @ fe --- fe/src/pages/bilibiliVote.vue | 1 + fe/src/stores/viewportTopmostPost.ts | 36 +++++++++++--------- fe/src/utils/echarts.ts | 12 +++---- fe/src/utils/post/renderer/list/index.ts | 42 +++++++++++++++++------- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/fe/src/pages/bilibiliVote.vue b/fe/src/pages/bilibiliVote.vue index efdd5ffc..95c9266f 100644 --- a/fe/src/pages/bilibiliVote.vue +++ b/fe/src/pages/bilibiliVote.vue @@ -132,6 +132,7 @@ const chartElementRefs = { top5CandidateCountGroupByTime: ref(), allVoteCountGroupByTime: ref() }; +useResizeableEcharts(Object.values(chartElementRefs)); type ChartName = keyof typeof chartElementRefs; const { top50CandidateCount: top50CandidateCountRef, diff --git a/fe/src/stores/viewportTopmostPost.ts b/fe/src/stores/viewportTopmostPost.ts index 038b870b..28839ee0 100644 --- a/fe/src/stores/viewportTopmostPost.ts +++ b/fe/src/stores/viewportTopmostPost.ts @@ -3,24 +3,30 @@ import _ from 'lodash'; export const useViewportTopmostPostStore = defineStore('viewportTopmostPost', () => { interface TopmostPost { cursor: Cursor, tid: Tid, pid?: Pid } const viewportTopmostPost = ref(); - + const { height: windowHeight } = useWindowSize(); const intersectionObserver = (newTopmostPost: TopmostPost, topOffset = 0) => { const stickyTitleEl = ref(); - useIntersectionObserver(stickyTitleEl, entries => { - _.orderBy(entries, 'time').forEach(e => { // https://github.com/vueuse/vueuse/issues/4197 - if (e.isIntersecting - && !(newTopmostPost.pid === undefined // prevent thread overwrite its reply - && viewportTopmostPost.value?.tid === newTopmostPost.tid)) - viewportTopmostPost.value = newTopmostPost; - }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + let stopExistingIntersectionObserver = () => {}; + watchDebounced(windowHeight, () => { + stopExistingIntersectionObserver(); + const { stop } = useIntersectionObserver(stickyTitleEl, entries => { + _.orderBy(entries, 'time').forEach(e => { // https://github.com/vueuse/vueuse/issues/4197 + if (e.isIntersecting + && !(newTopmostPost.pid === undefined // prevent thread overwrite its reply + && viewportTopmostPost.value?.tid === newTopmostPost.tid)) + viewportTopmostPost.value = newTopmostPost; + }); - // bottom: -100% will only trigger when reaching the top border of root that defaults to viewport - // top: -(topOffset + 1)px will move down the trigger line below the top border to match with its offset - // the additional +1px will allow they sharing an intersection with 1px height when the stickyTitleEl is stucking - // https://stackoverflow.com/questions/16302483/event-to-detect-when-positionsticky-is-triggered - // https://stackoverflow.com/questions/54807535/intersection-observer-api-observe-the-center-of-the-viewport - // https://web.archive.org/web/20240111160426/https://wilsotobianco.com/experiments/intersection-observer-playground/ - }, { rootMargin: `-${topOffset}px 0px -100% 0px` }); + // bottom: -windowHeight will only trigger when reaching the top border of root that defaults to viewport + // additional +topOffset and not using -100% to fix https://bugzilla.mozilla.org/show_bug.cgi?id=1918017 + // top: -topOffset will move down the trigger line below the top border to match with its offset + // https://stackoverflow.com/questions/16302483/event-to-detect-when-positionsticky-is-triggered + // https://stackoverflow.com/questions/54807535/intersection-observer-api-observe-the-center-of-the-viewport + // https://web.archive.org/web/20240111160426/https://wilsotobianco.com/experiments/intersection-observer-playground/ + }, { rootMargin: `${-topOffset}px 0px ${-windowHeight.value + topOffset}px 0px` }); + stopExistingIntersectionObserver = stop; + }, { debounce: 5000, immediate: true }); return { stickyTitleEl }; }; diff --git a/fe/src/utils/echarts.ts b/fe/src/utils/echarts.ts index a95108cb..5ac3a7d2 100644 --- a/fe/src/utils/echarts.ts +++ b/fe/src/utils/echarts.ts @@ -4,12 +4,12 @@ import * as echarts from 'echarts/core'; // eslint-disable-next-line import-x/extensions import type { ColorPaletteOptionMixin } from 'echarts/types/src/util/types.d.ts'; -if (import.meta.client) { - addEventListener('resize', _.throttle(() => { - document.querySelectorAll('.echarts') - .forEach(el => { echarts.getInstanceByDom(el)?.resize() }); - }, 200, { leading: false })); -} +export const useResizeableEcharts = (el: Parameters[0]) => + useResizeObserver(el, _.debounce((entries: Parameters[1]>[0]) => { // https://github.com/vueuse/vueuse/issues/4216 + entries.forEach(entry => { + echarts.getInstanceByDom(entry.target as HTMLElement)?.resize(); + }); + }, 1000)); export const echarts4ColorTheme: ColorPaletteOptionMixin = { color: [ diff --git a/fe/src/utils/post/renderer/list/index.ts b/fe/src/utils/post/renderer/list/index.ts index b7915074..beda14b9 100644 --- a/fe/src/utils/post/renderer/list/index.ts +++ b/fe/src/utils/post/renderer/list/index.ts @@ -22,20 +22,37 @@ export const guessReplyContainIntrinsicBlockSize = (replyElements: HTMLElement[] if (contentEl === null) return; const contentStyles = ((el: HTMLElement) => { - const getCSSPropertyInPixels = (el: HTMLElement, property: string) => - (el.computedStyleMap().get(property) as CSSNumericValue).to('px').value; + // https://caniuse.com/mdn-api_element_computedstylemap + // https://bugzilla.mozilla.org/show_bug.cgi?id=1857849 + // https://github.com/surma/ishoudinireadyyet.com + // https://github.com/tylergaw/css-typed-om/ + const isCSMSupported = 'computedStyleMap' in el; + const pixelStringToNumber = (s: string) => { + if (!s.endsWith('px')) + throw new Error(`Unit of '${s}' is not in pixels`); + + return Number(removeEnd(s, 'px')); // parseInt() will also remove any suffix + }; + + const getCSSPropertyInPixels = (el: HTMLElement, property: string) => (isCSMSupported + ? (el.computedStyleMap().get(property) as CSSNumericValue).to('px').value + : pixelStringToNumber(getComputedStyle(el).getPropertyValue(property))); const getInnerWidth = (el: HTMLElement | null) => (el === null ? 0 : el.clientWidth - getCSSPropertyInPixels(el, 'padding-left') - getCSSPropertyInPixels(el, 'padding-right')); - const contentLineHeightUnitValue = el.computedStyleMap().get('line-height') as CSSUnitValue; + + const convertRemUnitValueToPixels = (unitValue: CSSUnitValue) => (unitValue.unit === 'number' + ? convertRemToPixels(unitValue.value) + : unitValue.to('px').value); + const lineHeight = isCSMSupported + ? convertRemUnitValueToPixels(el.computedStyleMap().get('line-height') as CSSUnitValue) + : convertRemToPixels(pixelStringToNumber(getComputedStyle(el).lineHeight)); return { width: getInnerWidth(el), subReply: { width: getInnerWidth(document.querySelector('.sub-reply-content')) }, - lineHeight: contentLineHeightUnitValue.unit === 'number' - ? convertRemToPixels(contentLineHeightUnitValue.value) - : contentLineHeightUnitValue.to('px').value + lineHeight }; })(contentEl); @@ -80,20 +97,23 @@ export const guessReplyContainIntrinsicBlockSize = (replyElements: HTMLElement[] return Math.round(Math.ceil(lineCount) * contentStyles.lineHeight); }; replyElements.forEach(el => { - el.attributeStyleMap.set('--sub-reply-group-count', el.querySelectorAll('.sub-reply-group').length); + // https://caniuse.com/mdn-api_htmlelement_attributestylemap + el.style.setProperty('--sub-reply-group-count', + el.querySelectorAll('.sub-reply-group').length.toString()); - const imageLineCount = (el.querySelectorAll('.tieba-ugc-image').length * tiebaUGCImageMaxSize.px) / contentStyles.width; - el.attributeStyleMap.set('--predicted-image-height', + const imageLineCount = (el.querySelectorAll('.tieba-ugc-image') + .length * tiebaUGCImageMaxSize.px) / contentStyles.width; + el.style.setProperty('--predicted-image-height', `${Math.ceil(imageLineCount) * tiebaUGCImageMaxSize.px}px`); const replyContentHeight = predictPostContentHeight(contentStyles.width)(el.querySelector('.reply-content')); - el.attributeStyleMap.set('--predicted-reply-content-height', `${replyContentHeight}px`); + el.style.setProperty('--predicted-reply-content-height', `${replyContentHeight}px`); const subReplyContentHeight = _.sum( [...el.querySelectorAll('.sub-reply-content')] .map(predictPostContentHeight(contentStyles.subReply.width)) ); - el.attributeStyleMap.set('--predicted-sub-reply-content-height', `${subReplyContentHeight}px`); + el.style.setProperty('--predicted-sub-reply-content-height', `${subReplyContentHeight}px`); }); // show diff between predicted height and actual height of each `.reply` after complete scroll over whole page