<div class="timeline" role="feed" aria-label="{{label}}" on:focusWithCapture="saveFocus(event)" on:blurWithCapture="clearFocus(event)" > {{#if !$initialized}} <LoadingPage /> {{/if}} {{#if timelineType === 'notifications'}} <VirtualList component="{{NotificationVirtualListItem}}" :makeProps items="{{$timelineItemIds}}" on:scrollToBottom="onScrollToBottom()" shown="{{$initialized}}" footerComponent="{{LoadingFooter}}" showFooter="{{$initialized && $runningUpdate}}" realm="{{$currentInstance + '/' + timeline}}" on:initializedVisibleItems="initialize()" /> {{elseif virtual}} <VirtualList component="{{StatusVirtualListItem}}" :makeProps items="{{$timelineItemIds}}" on:scrollToBottom="onScrollToBottom()" shown="{{$initialized}}" footerComponent="{{LoadingFooter}}" showFooter="{{$initialized && $runningUpdate}}" realm="{{$currentInstance + '/' + timeline}}" on:initializedVisibleItems="initialize()" /> {{else}} <!-- if this is a status thread, it's easier to just render the whole thing rather than use a virtual list --> <PseudoVirtualList component="{{StatusVirtualListItem}}" :makeProps items="{{$timelineItemIds}}" shown="{{$initialized}}" on:initializedVisibleItems="initialize()" scrollToItem="{{scrollToItem}}" realm="{{$currentInstance + '/' + timeline}}" /> {{/if}} </div> <style> .timeline { min-height: 60vh; position: relative; } </style> <script> import { store } from '../../_store/store' import StatusVirtualListItem from './StatusVirtualListItem.html' import NotificationVirtualListItem from './NotificationVirtualListItem.html' import Status from '../status/Status.html' import PseudoVirtualList from '../pseudoVirtualList/PseudoVirtualList.html' import LoadingFooter from './LoadingFooter.html' import VirtualList from '../virtualList/VirtualList.html' import { timelines } from '../../_static/timelines' import { database } from '../../_database/database' import { initializeTimeline, fetchTimelineItemsOnScrollToBottom, setupTimeline } from '../../_actions/timeline' import LoadingPage from '../LoadingPage.html' import { focusWithCapture, blurWithCapture } from '../../_utils/events' export default { oncreate() { console.log('timeline oncreate()') this.onPushState = this.onPushState.bind(this) this.store.setForCurrentTimeline({ignoreBlurEvents: false}) window.addEventListener('pushState', this.onPushState) setupTimeline() if (this.store.get('initialized')) { this.restoreFocus() } }, ondestroy() { console.log('ondestroy') window.removeEventListener('pushState', this.onPushState) }, data: () => ({ StatusVirtualListItem, NotificationVirtualListItem, LoadingFooter, Status }), computed: { makeProps: ($currentInstance, timelineType, timelineValue) => async (itemId) => { let res = { timelineType, timelineValue } if (timelineType === 'notifications') { res.notification = await database.getNotification($currentInstance, itemId) } else { res.status = await database.getStatus($currentInstance, itemId) } return res }, label: (timeline, $currentInstance, timelineType, timelineValue) => { if (timelines[timeline]) { return `${timelines[timeline].label} timeline for ${$currentInstance}` } switch (timelineType) { case 'tag': return `#${timelineValue} timeline for ${$currentInstance}` case 'status': return 'Status context' case 'account': return `Account #${timelineValue} on ${$currentInstance}` case 'list': return `List #${timelineValue} on ${$currentInstance}` } }, timelineType: (timeline) => { return timeline.split('/')[0] }, timelineValue: (timeline) => { return timeline.split('/').slice(-1)[0] }, // for threads, it's simpler to just render all items as a pseudo-virtual list // due to need to scroll to the right item and thus calculate all item heights up-front virtual: (timelineType) => timelineType !=='status', scrollToItem: (timelineType, timelineValue, $firstTimelineItemId) => { // Scroll to the first item if this is a "status in own thread" timeline. // Don't scroll to the first item because it obscures the "back" button. return timelineType === 'status' && $firstTimelineItemId && timelineValue !== $firstTimelineItemId && timelineValue } }, store: () => store, components: { VirtualList, PseudoVirtualList, LoadingPage }, events: { focusWithCapture, blurWithCapture }, methods: { initialize() { if (this.store.get('initialized') || !this.store.get('timelineItemIds')) { return } console.log('timeline initialize()') initializeTimeline() }, onPushState() { this.store.setForCurrentTimeline({ ignoreBlurEvents: true }) }, onScrollToBottom() { if (!this.store.get('initialized') || this.store.get('runningUpdate') || this.get('timelineType') === 'status') { // for status contexts, we've already fetched the whole thread return } fetchTimelineItemsOnScrollToBottom() }, saveFocus(e) { let instanceName = this.store.get('currentInstance') let timelineName = this.get('timeline') let lastFocusedElementSelector let activeElement = e.target if (activeElement) { let focusKey = activeElement.getAttribute('focus-key') if (focusKey) { lastFocusedElementSelector = `[focus-key=${focusKey}]` } } console.log('saving focus to ', lastFocusedElementSelector) this.store.setForTimeline(instanceName, timelineName, { lastFocusedElementSelector }) }, clearFocus() { if (this.store.get('ignoreBlurEvents')) { return } console.log('clearing focus') let instanceName = this.store.get('currentInstance') let timelineName = this.get('timeline') this.store.setForTimeline(instanceName, timelineName, { lastFocusedElementSelector: null }) }, restoreFocus() { let lastFocusedElementSelector = this.store.get('lastFocusedElementSelector') console.log('lastFocused', lastFocusedElementSelector) if (lastFocusedElementSelector) { requestAnimationFrame(() => { requestAnimationFrame(() => { let element = document.querySelector(lastFocusedElementSelector) console.log('el', element) if (element) { element.focus() } }) }) } }, } } </script>