feat: Agenda URL #hash scrolls to 'now' or specific day (#7772)
* feat: Agenda URL #now scroll to current event * fix: agendaData date offset for dev purposes * feat: scroll to day * fix: Showing correct hostname in agenda modify log * feat: mobile menu scroll to hash and general bug fixes * fix: agenda mobile menu formatting and Playwright selectors * fix: removing spurious ? mark * chore: removing redundant agenda time setter in favour of agenda settings panel for debugging * style: Update AgendaMobileBar.vue * style: Update AgendaScheduleList.vue --------- Co-authored-by: Nicolas Giard <github@ngpixel.com>
This commit is contained in:
parent
af21347b67
commit
bece8fd71b
client/agenda
playwright/tests/meeting
|
@ -434,7 +434,7 @@ function reconnectScrollObservers () {
|
||||||
scrollObserver.disconnect()
|
scrollObserver.disconnect()
|
||||||
visibleDays.length = 0
|
visibleDays.length = 0
|
||||||
for (const mDay of agendaStore.meetingDays) {
|
for (const mDay of agendaStore.meetingDays) {
|
||||||
const el = document.getElementById(`agenda-day-${mDay.slug}`)
|
const el = document.getElementById(mDay.slug)
|
||||||
el.dataset.dayId = mDay.slug.toString()
|
el.dataset.dayId = mDay.slug.toString()
|
||||||
el.dataset.dayTs = mDay.ts
|
el.dataset.dayTs = mDay.ts
|
||||||
scrollObserver.observe(el)
|
scrollObserver.observe(el)
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NBadge,
|
NBadge,
|
||||||
NDropdown,
|
NDropdown,
|
||||||
|
@ -51,21 +50,48 @@ const siteStore = useSiteStore()
|
||||||
|
|
||||||
// Meeting Days
|
// Meeting Days
|
||||||
|
|
||||||
|
function optionToLink(opts){
|
||||||
|
const { key, label, icon } = opts
|
||||||
|
|
||||||
|
return {
|
||||||
|
...opts,
|
||||||
|
type: 'render',
|
||||||
|
render: () => h(
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: 'dropdown-link',
|
||||||
|
'data-testid': 'mobile-link',
|
||||||
|
href: `#${key}`
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
icon()
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
label
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const jumpToDayOptions = computed(() => {
|
const jumpToDayOptions = computed(() => {
|
||||||
const days = []
|
const days = []
|
||||||
if (agendaStore.isMeetingLive) {
|
if (agendaStore.isMeetingLive) {
|
||||||
days.push({
|
days.push(optionToLink({
|
||||||
label: 'Jump to Now',
|
label: 'Jump to Now',
|
||||||
key: 'now',
|
key: 'now',
|
||||||
icon: () => h('i', { class: 'bi bi-arrow-down-right-square text-red' })
|
icon: () => h('i', { class: 'bi bi-arrow-down-right-square text-red' })
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
for (const day of agendaStore.meetingDays) {
|
for (const day of agendaStore.meetingDays) {
|
||||||
days.push({
|
days.push(optionToLink({
|
||||||
label: `Jump to ${day.label}`,
|
label: `Jump to ${day.label}`,
|
||||||
key: day.slug,
|
key: day.slug,
|
||||||
icon: () => h('i', { class: 'bi bi-arrow-down-right-square' })
|
icon: () => h('i', { class: 'bi bi-arrow-down-right-square' })
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
return days
|
return days
|
||||||
})
|
})
|
||||||
|
@ -90,14 +116,13 @@ const downloadIcsOptions = [
|
||||||
function jumpToDay (dayId) {
|
function jumpToDay (dayId) {
|
||||||
if (dayId === 'now') {
|
if (dayId === 'now') {
|
||||||
const lastEventId = agendaStore.findCurrentEventId()
|
const lastEventId = agendaStore.findCurrentEventId()
|
||||||
|
|
||||||
if (lastEventId) {
|
if (lastEventId) {
|
||||||
document.getElementById(`agenda-rowid-${lastEventId}`)?.scrollIntoView(true)
|
document.getElementById(`agenda-rowid-${lastEventId}`)?.scrollIntoView(true)
|
||||||
} else {
|
} else {
|
||||||
message.warning('There is no event happening right now.')
|
message.warning('There is no event happening right now.')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(`agenda-day-${dayId}`)?.scrollIntoView(true)
|
document.getElementById(dayId)?.scrollIntoView(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,4 +187,19 @@ function downloadIcs (key) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-link {
|
||||||
|
display: flex;
|
||||||
|
text-decoration:none;
|
||||||
|
gap: 0.2rem 0.5rem;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--bs-dark-bg-subtle);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
li.nav-item(v-for='day of agendaStore.meetingDays')
|
li.nav-item(v-for='day of agendaStore.meetingDays')
|
||||||
a.nav-link(
|
a.nav-link(
|
||||||
:class='agendaStore.dayIntersectId === day.slug ? `active` : ``'
|
:class='agendaStore.dayIntersectId === day.slug ? `active` : ``'
|
||||||
:href='`#slot-` + day.slug'
|
:href='`#${day.slug}`'
|
||||||
@click='scrollToDay(day.slug, $event)'
|
@click='scrollToDay(day.slug, $event)'
|
||||||
)
|
)
|
||||||
i.bi.bi-arrow-right-short.d-none.d-xxl-inline.me-2
|
i.bi.bi-arrow-right-short.d-none.d-xxl-inline.me-2
|
||||||
|
@ -109,7 +109,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
import {
|
import {
|
||||||
NAffix,
|
NAffix,
|
||||||
NBadge,
|
NBadge,
|
||||||
|
@ -200,14 +199,11 @@ function pickerDiscard () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToDay (dayId, ev) {
|
function scrollToDay (daySlug, ev) {
|
||||||
ev.preventDefault()
|
document.getElementById(daySlug)?.scrollIntoView(true)
|
||||||
document.getElementById(`agenda-day-${dayId}`)?.scrollIntoView(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToNow (ev) {
|
function scrollToNow (ev) {
|
||||||
ev.preventDefault()
|
|
||||||
|
|
||||||
const lastEventId = agendaStore.findCurrentEventId()
|
const lastEventId = agendaStore.findCurrentEventId()
|
||||||
|
|
||||||
if (lastEventId) {
|
if (lastEventId) {
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
)
|
)
|
||||||
//- ROW - DAY HEADING -----------------------
|
//- ROW - DAY HEADING -----------------------
|
||||||
template(v-if='item.displayType === `day`')
|
template(v-if='item.displayType === `day`')
|
||||||
td(:id='`agenda-day-` + item.id', :colspan='pickerModeActive ? 6 : 5') {{item.date}}
|
td(:id='item.slug', :colspan='pickerModeActive ? 6 : 5') {{item.date}}
|
||||||
//- ROW - SESSION HEADING -------------------
|
//- ROW - SESSION HEADING -------------------
|
||||||
template(v-else-if='item.displayType === `session-head`')
|
template(v-else-if='item.displayType === `session-head`')
|
||||||
td.agenda-table-cell-check(v-if='pickerModeActive')
|
td.agenda-table-cell-check(v-if='pickerModeActive')
|
||||||
|
@ -200,7 +200,7 @@ import {
|
||||||
|
|
||||||
import AgendaDetailsModal from './AgendaDetailsModal.vue'
|
import AgendaDetailsModal from './AgendaDetailsModal.vue'
|
||||||
|
|
||||||
import { useAgendaStore } from './store'
|
import { useAgendaStore, daySlugPrefix, daySlug } from './store'
|
||||||
import { useSiteStore } from '../shared/store'
|
import { useSiteStore } from '../shared/store'
|
||||||
import { getUrl } from '../shared/urls'
|
import { getUrl } from '../shared/urls'
|
||||||
|
|
||||||
|
@ -248,6 +248,7 @@ const meetingEvents = computed(() => {
|
||||||
if (itemDate.toISODate() !== acc.lastDate) {
|
if (itemDate.toISODate() !== acc.lastDate) {
|
||||||
acc.result.push({
|
acc.result.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
slug: daySlug(item),
|
||||||
key: `day-${itemDate.toISODate()}`,
|
key: `day-${itemDate.toISODate()}`,
|
||||||
displayType: 'day',
|
displayType: 'day',
|
||||||
date: itemDate.toLocaleString(DateTime.DATE_HUGE),
|
date: itemDate.toLocaleString(DateTime.DATE_HUGE),
|
||||||
|
@ -575,6 +576,30 @@ function recalculateRedLine () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On page load when browser location hash contains '#now' or '#agenda-day-*' then scroll accordingly
|
||||||
|
*/
|
||||||
|
;(function scrollToHashInit() {
|
||||||
|
if (!window.location.hash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!(window.location.hash === "#now" || window.location.hash.startsWith(`#${daySlugPrefix}`))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const unsubscribe = agendaStore.$subscribe((_mutation, agendaStoreState) => {
|
||||||
|
if (agendaStoreState.schedule.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unsubscribe() // we only need to scroll once, so unsubscribe from future updates
|
||||||
|
if(window.location.hash === "#now") {
|
||||||
|
const lastEventId = agendaStore.findCurrentEventId()
|
||||||
|
document.getElementById(`agenda-rowid-${lastEventId}`)?.scrollIntoView(true)
|
||||||
|
} else if(window.location.hash.startsWith(`#${daySlugPrefix}`)) {
|
||||||
|
document.getElementById(window.location.hash.substring(1))?.scrollIntoView(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
// MOUNTED
|
// MOUNTED
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -121,7 +121,7 @@ export const useAgendaStore = defineStore('agenda', {
|
||||||
meetingDays () {
|
meetingDays () {
|
||||||
const siteStore = useSiteStore()
|
const siteStore = useSiteStore()
|
||||||
return uniqBy(this.scheduleAdjusted, 'adjustedStartDate').sort().map(s => ({
|
return uniqBy(this.scheduleAdjusted, 'adjustedStartDate').sort().map(s => ({
|
||||||
slug: s.id.toString(),
|
slug: daySlug(s),
|
||||||
ts: s.adjustedStartDate,
|
ts: s.adjustedStartDate,
|
||||||
label: siteStore.viewport < 1350 ? DateTime.fromISO(s.adjustedStartDate).toFormat('ccc LLL d') : DateTime.fromISO(s.adjustedStartDate).toLocaleString(DateTime.DATE_HUGE)
|
label: siteStore.viewport < 1350 ? DateTime.fromISO(s.adjustedStartDate).toFormat('ccc LLL d') : DateTime.fromISO(s.adjustedStartDate).toLocaleString(DateTime.DATE_HUGE)
|
||||||
}))
|
}))
|
||||||
|
@ -292,3 +292,8 @@ function findFirstConferenceUrl (txt) {
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const daySlugPrefix = 'agenda-day-'
|
||||||
|
export function daySlug(s) {
|
||||||
|
return `${daySlugPrefix}${s.adjustedStartDate}` // eg 'agenda-day-2024-08-13'
|
||||||
|
}
|
||||||
|
|
|
@ -1431,7 +1431,7 @@ test.describe('past - small screens', () => {
|
||||||
|
|
||||||
// can open the jump to day dropdown
|
// can open the jump to day dropdown
|
||||||
await barBtnLocator.first().click()
|
await barBtnLocator.first().click()
|
||||||
const jumpDayDdnLocator = page.locator('.n-dropdown-menu > .n-dropdown-option')
|
const jumpDayDdnLocator = page.locator('.n-dropdown-menu [data-testid=mobile-link]')
|
||||||
await expect(jumpDayDdnLocator).toHaveCount(7)
|
await expect(jumpDayDdnLocator).toHaveCount(7)
|
||||||
for (let idx = 0; idx < 7; idx++) {
|
for (let idx = 0; idx < 7; idx++) {
|
||||||
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate, { zone: meetingData.meeting.timezone })
|
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate, { zone: meetingData.meeting.timezone })
|
||||||
|
|
Loading…
Reference in a new issue