scroll to element for linear view
This commit is contained in:
parent
97e691b290
commit
7753a94a2f
6 changed files with 77 additions and 49 deletions
|
|
@ -80,6 +80,19 @@ export default {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tryScrollTo = async (id) => {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isPage.value) {
|
||||||
|
router.push({ name: 'conversation', params: { statusId: id } })
|
||||||
|
}
|
||||||
|
setFocused(id)
|
||||||
|
const target = document.querySelector(`.Status[data-status-id=${id}]`)
|
||||||
|
await nextTick()
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
|
||||||
const { statusId } = toRefs(props)
|
const { statusId } = toRefs(props)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -105,6 +118,7 @@ export default {
|
||||||
}
|
}
|
||||||
provide('isExpanded', isExpanded)
|
provide('isExpanded', isExpanded)
|
||||||
provide('isPage', isPage)
|
provide('isPage', isPage)
|
||||||
|
provide('expandable', true)
|
||||||
|
|
||||||
// # Focus
|
// # Focus
|
||||||
const focusedId = ref(statusId.value)
|
const focusedId = ref(statusId.value)
|
||||||
|
|
@ -133,12 +147,13 @@ export default {
|
||||||
} = useConversation(focusedId, isExpanded)
|
} = useConversation(focusedId, isExpanded)
|
||||||
|
|
||||||
watch(expanded, (value) => {
|
watch(expanded, (value) => {
|
||||||
|
tryScrollTo(currentStatus.value.id)
|
||||||
if (value) {
|
if (value) {
|
||||||
fetchConversation()
|
fetchConversation()
|
||||||
} else {
|
} else {
|
||||||
resetDisplayState()
|
resetDisplayState()
|
||||||
}
|
}
|
||||||
})
|
}, { flush: 'post' })
|
||||||
|
|
||||||
const resetDisplayState = () => {
|
const resetDisplayState = () => {
|
||||||
setFocused(statusId.value)
|
setFocused(statusId.value)
|
||||||
|
|
@ -181,7 +196,7 @@ export default {
|
||||||
heightChart: heightChartLinear,
|
heightChart: heightChartLinear,
|
||||||
changeSuspendState: changeSuspendStateLinear,
|
changeSuspendState: changeSuspendStateLinear,
|
||||||
updateVirtualHeight: updateVirtualHeightLinear,
|
updateVirtualHeight: updateVirtualHeightLinear,
|
||||||
} = useVirtualScrolling(conversation, linearElement)
|
} = useVirtualScrolling(conversation, linearElement, currentStatus)
|
||||||
|
|
||||||
// # Tree style stuff
|
// # Tree style stuff
|
||||||
const isTreeView = computed(() => displayStyle.value === 'tree')
|
const isTreeView = computed(() => displayStyle.value === 'tree')
|
||||||
|
|
@ -205,9 +220,10 @@ export default {
|
||||||
const currentLevelElement = useTemplateRef('currentLevel')
|
const currentLevelElement = useTemplateRef('currentLevel')
|
||||||
const {
|
const {
|
||||||
heightChart: heightChartCurrentLevel,
|
heightChart: heightChartCurrentLevel,
|
||||||
|
totalHeight: totalHeightCurrentLevel,
|
||||||
changeSuspendState: changeSuspendStateCurrentLevel,
|
changeSuspendState: changeSuspendStateCurrentLevel,
|
||||||
updateVirtualHeight: updateVirtualHeightCurrentLevel,
|
updateVirtualHeight: updateVirtualHeightCurrentLevel,
|
||||||
} = useVirtualScrolling(currentLevel, currentLevelElement)
|
} = useVirtualScrolling(currentLevel, currentLevelElement, currentStatus)
|
||||||
|
|
||||||
const treeViewIsSimple = computed(
|
const treeViewIsSimple = computed(
|
||||||
() => !mergedConfig.value.conversationTreeAdvanced,
|
() => !mergedConfig.value.conversationTreeAdvanced,
|
||||||
|
|
@ -223,32 +239,6 @@ export default {
|
||||||
)
|
)
|
||||||
|
|
||||||
// # Scrolling
|
// # Scrolling
|
||||||
const tryScrollTo = (id) => {
|
|
||||||
if (!id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isPage.value) {
|
|
||||||
router.push({ name: 'conversation', params: { statusId: id } })
|
|
||||||
}
|
|
||||||
// Because the conversation can be unmounted when out of sight
|
|
||||||
// and mounted again when it comes into sight,
|
|
||||||
// the `mounted` or `created` function in `status` should not
|
|
||||||
// contain scrolling calls, as we do not want the page to jump
|
|
||||||
// when we scroll with an expanded conversation.
|
|
||||||
//
|
|
||||||
// Now the method is to rely solely on the `focused` watcher
|
|
||||||
// in `status` components.
|
|
||||||
// In linear views, all statuses are rendered at all times, but
|
|
||||||
// in tree views, it is possible that a change in active status
|
|
||||||
// removes and adds status components (e.g. an originally child
|
|
||||||
// status becomes an ancestor status, and thus they will be
|
|
||||||
// different).
|
|
||||||
// Here, let the components be rendered first, in order to trigger
|
|
||||||
// the `focused` watcher.
|
|
||||||
nextTick(() => {
|
|
||||||
setFocused(id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const diveIntoStatus = (id) => {
|
const diveIntoStatus = (id) => {
|
||||||
tryScrollTo(id)
|
tryScrollTo(id)
|
||||||
}
|
}
|
||||||
|
|
@ -261,6 +251,11 @@ export default {
|
||||||
loadError,
|
loadError,
|
||||||
mobileLayout,
|
mobileLayout,
|
||||||
|
|
||||||
|
// # Conversation Expansion
|
||||||
|
isPage,
|
||||||
|
isExpanded,
|
||||||
|
toggleExpanded,
|
||||||
|
|
||||||
// # Focus
|
// # Focus
|
||||||
focused,
|
focused,
|
||||||
setFocused,
|
setFocused,
|
||||||
|
|
@ -270,11 +265,6 @@ export default {
|
||||||
currentStatus,
|
currentStatus,
|
||||||
getReplies,
|
getReplies,
|
||||||
|
|
||||||
// # Conversation Expansion
|
|
||||||
isPage,
|
|
||||||
isExpanded,
|
|
||||||
toggleExpanded,
|
|
||||||
|
|
||||||
// # Misc UI things
|
// # Misc UI things
|
||||||
getStatusClasses,
|
getStatusClasses,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@
|
||||||
|
|
||||||
:focused="focused === element.status.id"
|
:focused="focused === element.status.id"
|
||||||
conversation-rank="ancestor"
|
conversation-rank="ancestor"
|
||||||
|
:data-status-id="element.id"
|
||||||
|
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@dive="diveIntoStatus(element.status.id)"
|
@dive="diveIntoStatus(element.status.id)"
|
||||||
|
|
@ -165,6 +166,7 @@
|
||||||
|
|
||||||
:focused="focused === element.id || focused === element.status.retweeted_status?.id"
|
:focused="focused === element.id || focused === element.status.retweeted_status?.id"
|
||||||
|
|
||||||
|
:data-status-id="element.id"
|
||||||
@goto="setFocused"
|
@goto="setFocused"
|
||||||
@toggle-expanded="toggleExpanded"
|
@toggle-expanded="toggleExpanded"
|
||||||
@suspendable-state-change="changeSuspendStateLinear"
|
@suspendable-state-change="changeSuspendStateLinear"
|
||||||
|
|
|
||||||
|
|
@ -121,19 +121,15 @@ const Status = {
|
||||||
],
|
],
|
||||||
inject: {
|
inject: {
|
||||||
profileUserId: {
|
profileUserId: {
|
||||||
type: String,
|
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
isPage: {
|
isPage: {
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isExpanded: {
|
isExpanded: {
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
expandable: {
|
expandable: {
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="!isExpanded && !isPreview"
|
v-if="expandable && !isExpanded && !isPreview"
|
||||||
class="button-unstyled"
|
class="button-unstyled"
|
||||||
:title="$t('status.expand')"
|
:title="$t('status.expand')"
|
||||||
@click.prevent="toggleExpanded"
|
@click.prevent="toggleExpanded"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
:replies="getReplies(statusId)"
|
:replies="getReplies(statusId)"
|
||||||
:focused="focused === statusId"
|
:focused="focused === statusId"
|
||||||
|
|
||||||
|
:data-status-id="statusId"
|
||||||
:conversation-rank="depth === 0 ? 'current' : 'child'"
|
:conversation-rank="depth === 0 ? 'current' : 'child'"
|
||||||
:thread-display-state="threadDisplay.get(statusId)"
|
:thread-display-state="threadDisplay.get(statusId)"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
import { useStatusesStore } from 'src/stores/statuses.js'
|
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||||
|
|
@ -7,10 +7,11 @@ import { useStatusesStore } from 'src/stores/statuses.js'
|
||||||
import { useScrollPosition } from 'src/composables/useScrollPosition.js'
|
import { useScrollPosition } from 'src/composables/useScrollPosition.js'
|
||||||
import { useWindowSize } from 'src/composables/useWindowSize.js'
|
import { useWindowSize } from 'src/composables/useWindowSize.js'
|
||||||
|
|
||||||
export function useVirtualScrolling(conversation, body) {
|
export function useVirtualScrolling(conversation, body, anchorStatus) {
|
||||||
const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
|
const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
|
||||||
|
|
||||||
const { mergedConfig } = storeToRefs(useMergedConfigStore())
|
const { mergedConfig } = storeToRefs(useMergedConfigStore())
|
||||||
|
const anchor = computed(() => anchorStatus?.value.id)
|
||||||
|
|
||||||
const unsuspendibleIds = ref(new Set())
|
const unsuspendibleIds = ref(new Set())
|
||||||
const changeSuspendState = ({ id, suspend }) => {
|
const changeSuspendState = ({ id, suspend }) => {
|
||||||
|
|
@ -32,7 +33,7 @@ export function useVirtualScrolling(conversation, body) {
|
||||||
fontSize.value = Number.parseInt(string.slice(0, -2), 10) // remove the 'px'
|
fontSize.value = Number.parseInt(string.slice(0, -2), 10) // remove the 'px'
|
||||||
}
|
}
|
||||||
// Update font size if user changed UI scale
|
// Update font size if user changed UI scale
|
||||||
watch(fontSizeSetting, updateFontSize)
|
watch(fontSizeSetting, updateFontSize, { immediate: true })
|
||||||
|
|
||||||
// Placeholder heights.
|
// Placeholder heights.
|
||||||
const mutedStatusHeight = computed(() => {
|
const mutedStatusHeight = computed(() => {
|
||||||
|
|
@ -79,10 +80,22 @@ export function useVirtualScrolling(conversation, body) {
|
||||||
topScrollBoundary.value = distanceItemTopToWindowTop
|
topScrollBoundary.value = distanceItemTopToWindowTop
|
||||||
bottomScrollBoundary.value = distanceItemTopToWindowBottom
|
bottomScrollBoundary.value = distanceItemTopToWindowBottom
|
||||||
}
|
}
|
||||||
watch(windowHeight, updateBoundaries)
|
const windowWatcher = watch(windowHeight, updateBoundaries)
|
||||||
watch(scrollY, updateBoundaries)
|
const scrollWatcher = watch(scrollY, updateBoundaries)
|
||||||
watch(totalHeight, updateBoundaries)
|
const heightWatcher = watch(totalHeight, updateBoundaries)
|
||||||
watch(body, updateBoundaries)
|
const bodyWatcher = watch(body, updateBoundaries)
|
||||||
|
const pauseWatchers = () => {
|
||||||
|
windowWatcher.pause()
|
||||||
|
scrollWatcher.pause()
|
||||||
|
heightWatcher.pause()
|
||||||
|
bodyWatcher.pause()
|
||||||
|
}
|
||||||
|
const resumeWatchers = () => {
|
||||||
|
windowWatcher.resume()
|
||||||
|
scrollWatcher.resume()
|
||||||
|
heightWatcher.resume()
|
||||||
|
bodyWatcher.resume()
|
||||||
|
}
|
||||||
|
|
||||||
const heightChart = computed(() => {
|
const heightChart = computed(() => {
|
||||||
// Map every height and suspendable state
|
// Map every height and suspendable state
|
||||||
|
|
@ -108,8 +121,34 @@ export function useVirtualScrolling(conversation, body) {
|
||||||
return sum + item.height
|
return sum + item.height
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
|
return chart
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(heightChart, async (newVal, oldVal) => {
|
||||||
|
pauseWatchers()
|
||||||
|
const getAnchoredEl = (list) => anchor.value
|
||||||
|
? list.find(({ id }) => id === anchor.value)
|
||||||
|
: list[list.length - 1]
|
||||||
|
const oldElement = getAnchoredEl(oldVal)
|
||||||
|
const newElement = getAnchoredEl(newVal)
|
||||||
|
const oldOffset = oldElement?.top ?? 0
|
||||||
|
const newOffset = newElement?.top ?? 0
|
||||||
|
|
||||||
|
const diff = newOffset - oldOffset // Positive = down, Negative = up
|
||||||
|
|
||||||
|
console.log(diff, oldElement, newOffset)
|
||||||
|
topScrollBoundary.value += diff
|
||||||
|
bottomScrollBoundary.value += diff
|
||||||
|
await nextTick()
|
||||||
|
window.scrollBy(0, diff)
|
||||||
|
|
||||||
|
updateBoundaries()
|
||||||
|
resumeWatchers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const heightChartGrouped = computed(() => {
|
||||||
// Determine visibility state
|
// Determine visibility state
|
||||||
chart.forEach((heightChartItem) => {
|
const chart = heightChart.value.map((heightChartItem) => {
|
||||||
const itemBottomBoundary = heightChartItem.top + heightChartItem.height
|
const itemBottomBoundary = heightChartItem.top + heightChartItem.height
|
||||||
const itemTopBoundary = heightChartItem.top
|
const itemTopBoundary = heightChartItem.top
|
||||||
|
|
||||||
|
|
@ -123,7 +162,7 @@ export function useVirtualScrolling(conversation, body) {
|
||||||
const isAboveBottomBoundary = itemTopBoundary < finalBottomScrollBoundary
|
const isAboveBottomBoundary = itemTopBoundary < finalBottomScrollBoundary
|
||||||
// This accounts for the case where item's boundaries exceed scroll boundary
|
// This accounts for the case where item's boundaries exceed scroll boundary
|
||||||
|
|
||||||
heightChartItem.visible = isBelowTopBoundary && isAboveBottomBoundary
|
return { ...heightChartItem, visible: isBelowTopBoundary && isAboveBottomBoundary }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Group invisible statuses into spacers
|
// Group invisible statuses into spacers
|
||||||
|
|
@ -162,7 +201,7 @@ export function useVirtualScrolling(conversation, body) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
heightChart,
|
heightChart: heightChartGrouped,
|
||||||
changeSuspendState,
|
changeSuspendState,
|
||||||
updateVirtualHeight,
|
updateVirtualHeight,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue