2023-03-08 04:32:21 +09:00
|
|
|
import type { RouteLocationRaw } from 'vue-router'
|
|
|
|
import { useMagicSequence } from '~/composables/magickeys'
|
2024-02-25 04:28:56 +09:00
|
|
|
import { currentUser, getInstanceDomain } from '~/composables/users'
|
2023-03-08 04:32:21 +09:00
|
|
|
|
|
|
|
export default defineNuxtPlugin(({ $scrollToTop }) => {
|
|
|
|
const keys = useMagicKeys()
|
|
|
|
const router = useRouter()
|
2023-10-13 16:13:37 +09:00
|
|
|
const i18n = useNuxtApp().$i18n
|
2024-05-31 19:35:04 +09:00
|
|
|
const { y } = useWindowScroll({ behavior: 'instant' })
|
|
|
|
const virtualScroller = usePreferences('experimentalVirtualScroller')
|
2023-03-08 04:32:21 +09:00
|
|
|
|
|
|
|
// disable shortcuts when focused on inputs (https://vueuse.org/core/usemagickeys/#conditionally-disable)
|
|
|
|
const activeElement = useActiveElement()
|
|
|
|
|
|
|
|
const notUsingInput = computed(() =>
|
|
|
|
activeElement.value?.tagName !== 'INPUT'
|
|
|
|
&& activeElement.value?.tagName !== 'TEXTAREA'
|
|
|
|
&& !activeElement.value?.isContentEditable,
|
|
|
|
)
|
|
|
|
const isAuthenticated = currentUser.value !== undefined
|
|
|
|
|
|
|
|
const navigateTo = (to: string | RouteLocationRaw) => {
|
|
|
|
closeKeyboardShortcuts()
|
2023-03-21 13:20:36 +09:00
|
|
|
;($scrollToTop as () => void)() // is this really required?
|
2023-03-08 04:32:21 +09:00
|
|
|
router.push(to)
|
|
|
|
}
|
|
|
|
|
|
|
|
whenever(logicAnd(notUsingInput, keys['?']), toggleKeyboardShortcuts)
|
|
|
|
|
|
|
|
const defaultPublishDialog = () => {
|
|
|
|
const current = keys.current
|
|
|
|
// exclusive 'c' - not apply in combination
|
|
|
|
// TODO: bugfix -> create PR for vueuse, reset `current` ref on window focus|blur
|
|
|
|
if (!current.has('shift') && !current.has('meta') && !current.has('control') && !current.has('alt')) {
|
|
|
|
// TODO: is this the correct way of using openPublishDialog()?
|
2024-04-08 18:53:26 +09:00
|
|
|
openPublishDialog('dialog', getDefaultDraftItem())
|
2023-03-08 04:32:21 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, keys.c), defaultPublishDialog)
|
|
|
|
|
2024-02-25 04:28:56 +09:00
|
|
|
const instanceDomain = currentInstance.value ? getInstanceDomain(currentInstance.value) : 'm.webtoo.ls'
|
2023-03-08 04:32:21 +09:00
|
|
|
whenever(logicAnd(notUsingInput, useMagicSequence(['g', 'h'])), () => navigateTo('/home'))
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, useMagicSequence(['g', 'n'])), () => navigateTo('/notifications'))
|
2024-02-25 04:28:56 +09:00
|
|
|
// TODO: always overridden by 'c' (compose) shortcut
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, useMagicSequence(['g', 'c'])), () => navigateTo('/conversations'))
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, useMagicSequence(['g', 'f'])), () => navigateTo('/favourites'))
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, useMagicSequence(['g', 'b'])), () => navigateTo('/bookmarks'))
|
|
|
|
whenever(logicAnd(notUsingInput, useMagicSequence(['g', 'e'])), () => navigateTo(`/${instanceDomain}/explore`))
|
|
|
|
whenever(logicAnd(notUsingInput, useMagicSequence(['g', 'l'])), () => navigateTo(`/${instanceDomain}/public/local`))
|
|
|
|
whenever(logicAnd(notUsingInput, useMagicSequence(['g', 't'])), () => navigateTo(`/${instanceDomain}/public`))
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, useMagicSequence(['g', 'i'])), () => navigateTo('/lists'))
|
|
|
|
whenever(logicAnd(notUsingInput, useMagicSequence(['g', 's'])), () => navigateTo('/settings'))
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, useMagicSequence(['g', 'p'])), () => navigateTo(`/${instanceDomain}/@${currentUser.value?.account.username}`))
|
2024-03-30 13:15:58 +09:00
|
|
|
whenever(logicAnd(notUsingInput, computed(() => keys.current.size === 1), keys['/']), () => navigateTo('/search'))
|
2023-03-08 04:32:21 +09:00
|
|
|
|
|
|
|
const toggleFavouriteActiveStatus = () => {
|
|
|
|
// TODO: find a better solution than clicking buttons...
|
|
|
|
document
|
|
|
|
.querySelector<HTMLElement>('[aria-roledescription=status-details]')
|
2023-10-13 16:13:37 +09:00
|
|
|
?.querySelector<HTMLElement>(`button[aria-label=${i18n.t('action.favourite')}]`)
|
2023-03-08 04:32:21 +09:00
|
|
|
?.click()
|
|
|
|
}
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, keys.f), toggleFavouriteActiveStatus)
|
|
|
|
|
|
|
|
const toggleBoostActiveStatus = () => {
|
|
|
|
// TODO: find a better solution than clicking buttons...
|
|
|
|
document
|
|
|
|
.querySelector<HTMLElement>('[aria-roledescription=status-details]')
|
2023-10-13 16:13:37 +09:00
|
|
|
?.querySelector<HTMLElement>(`button[aria-label=${i18n.t('action.boost')}]`)
|
2023-03-08 04:32:21 +09:00
|
|
|
?.click()
|
|
|
|
}
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, keys.b), toggleBoostActiveStatus)
|
2024-02-24 23:46:54 +09:00
|
|
|
|
|
|
|
const showNewItems = () => {
|
|
|
|
// TODO: find a better solution than clicking buttons...
|
|
|
|
document
|
|
|
|
?.querySelector<HTMLElement>('button#elk_show_new_items')
|
|
|
|
?.click()
|
|
|
|
}
|
|
|
|
whenever(logicAnd(isAuthenticated, notUsingInput, keys['.']), showNewItems)
|
2024-05-31 19:35:04 +09:00
|
|
|
|
|
|
|
// TODO: virtual scroller cannot load off-screen post
|
|
|
|
// that prevents focusing next post properly
|
|
|
|
// we disabled this shortcut when enabled virtual scroller
|
|
|
|
if (!virtualScroller.value) {
|
|
|
|
const statusSelector = '[aria-roledescription="status-card"]'
|
|
|
|
|
|
|
|
// find the nearest status element id traversing up from the current active element
|
|
|
|
// `activeElement` can be some of an element within a status element
|
|
|
|
// otherwise, reach to the root `<html>`
|
|
|
|
function getActiveStatueId(element: HTMLElement): string | undefined {
|
|
|
|
if (element.nodeName === 'HTML')
|
|
|
|
return undefined
|
|
|
|
|
|
|
|
if (element.matches(statusSelector))
|
|
|
|
return element.id
|
|
|
|
|
|
|
|
return getActiveStatueId(element.parentNode as HTMLElement)
|
|
|
|
}
|
|
|
|
|
|
|
|
function focusNextOrPreviousStatus(direction: 'next' | 'previous') {
|
|
|
|
const activeStatusId = activeElement.value ? getActiveStatueId(activeElement.value) : undefined
|
|
|
|
const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction)
|
|
|
|
if (nextOrPreviousStatusId) {
|
|
|
|
const status = document.getElementById(nextOrPreviousStatusId)
|
|
|
|
if (status) {
|
|
|
|
status.focus({ preventScroll: true })
|
|
|
|
const topBarHeight = 58
|
|
|
|
y.value += status.getBoundingClientRect().top - topBarHeight
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNextOrPreviousStatusId(currentStatusId: string | undefined, direction: 'next' | 'previous'): string | undefined {
|
|
|
|
const statusIds = [...document.querySelectorAll(statusSelector)].map(s => s.id)
|
|
|
|
if (currentStatusId === undefined) {
|
|
|
|
// if there is no selection, always focus on the first status
|
|
|
|
return statusIds[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentIndex = statusIds.findIndex(id => id === currentStatusId)
|
|
|
|
const statusId = direction === 'next'
|
|
|
|
? statusIds[Math.min(currentIndex + 1, statusIds.length)]
|
|
|
|
: statusIds[Math.max(0, currentIndex - 1)]
|
|
|
|
return statusId
|
|
|
|
}
|
|
|
|
|
|
|
|
whenever(logicAnd(notUsingInput, keys.j), () => focusNextOrPreviousStatus('next'))
|
|
|
|
whenever(logicAnd(notUsingInput, keys.k), () => focusNextOrPreviousStatus('previous'))
|
|
|
|
}
|
2023-03-08 04:32:21 +09:00
|
|
|
})
|