feat: customize pjax behavior by platforms

This commit is contained in:
EnixCoda 2021-10-28 11:56:06 +08:00
parent 9ac855c153
commit 0de422ffb7
9 changed files with 138 additions and 123 deletions

View file

@ -1,6 +1,6 @@
import { BranchName, Breadcrumb, Flex, Text } from '@primer/components'
import { GitBranchIcon } from '@primer/octicons-react'
import { platform, platformName } from 'platforms'
import { platform } from 'platforms'
import * as React from 'react'
import { isOpenInNewWindowClick } from 'utils/general'
import { loadWithPJAX } from 'utils/hooks/usePJAX'
@ -28,16 +28,13 @@ export function MetaBar({ metaData }: Props) {
href={branchUrl}
as="a"
className={'branch-name'}
{...(platformName === 'GitHub'
? { 'data-pjax': '#repo-content-pjax-container' }
: {
onClick: e => {
if (isOpenInNewWindowClick(e)) return
onClick={e => {
if (isOpenInNewWindowClick(e)) return
e.preventDefault()
loadWithPJAX(branchUrl)
},
})}
e.preventDefault()
loadWithPJAX(branchUrl, e.currentTarget)
}}
{...platform.delegatePJAXProps?.()}
>
{branchName || '...'}
</BranchName>

View file

@ -1,8 +1,7 @@
import { useConfigs } from 'containers/ConfigsContext'
import { platformName } from 'platforms'
import { platform } from 'platforms'
import * as React from 'react'
import { cx } from 'utils/cx'
import { isOpenInNewWindowClick } from 'utils/general'
import { getFileIconURL, getFolderIconURL } from 'utils/parseIconMapCSV'
import { Icon } from './Icon'
@ -43,18 +42,11 @@ export function Node({
return (
<a
href={node.url}
{...(platformName === 'GitHub' && node.type === 'blob'
? { 'data-pjax': '#repo-content-pjax-container' }
: {
onClick: event => {
if (isOpenInNewWindowClick(event)) return
onClick(event, node)
},
})}
onClick={event => onClick(event, node)}
className={cx(`node-item`, { focused, disabled: node.accessDenied, expanded, compact })}
style={{ ...style, paddingLeft: `${10 + (compact ? 10 : 20) * depth}px` }}
title={node.path}
{...platform.delegatePJAXProps?.({ node })}
>
<div className={'node-item-label'}>
<NodeItemIcon node={node} open={expanded} loading={loading} />

View file

@ -13,7 +13,7 @@ import * as DOMHelper from 'utils/DOMHelper'
import { run } from 'utils/general'
import { useCatchNetworkError } from 'utils/hooks/useCatchNetworkError'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { loadWithPJAX, useOnPJAXDone, usePJAX } from 'utils/hooks/usePJAX'
import { useOnPJAXDone, usePJAX } from 'utils/hooks/usePJAX'
import { useProgressBar } from 'utils/hooks/useProgressBar'
import { useStateIO } from 'utils/hooks/useStateIO'
import { SideBarErrorContext } from '../containers/ErrorContext'
@ -181,7 +181,6 @@ export function SideBar() {
metaData={metaData}
freeze={showSettings}
accessToken={accessToken}
loadWithPJAX={loadWithPJAX}
config={configContext.value}
catchNetworkErrors={useCatchNetworkError()}
/>

View file

@ -3,7 +3,8 @@ import { GetCreatedMethod, MethodCreator } from 'driver/connect'
import { platform } from 'platforms'
import { Config } from 'utils/config/helper'
import * as DOMHelper from 'utils/DOMHelper'
import { OperatingSystems, os } from 'utils/general'
import { isOpenInNewWindowClick, OperatingSystems, os } from 'utils/general'
import { loadWithPJAX } from 'utils/hooks/usePJAX'
import { VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
export type Props = {
@ -11,7 +12,6 @@ export type Props = {
freeze: boolean
accessToken: string | undefined
config: Config
loadWithPJAX(url: string): void
catchNetworkErrors: <T>(fn: () => T) => Promise<T | undefined>
}
@ -101,108 +101,94 @@ export const setUpTree: BoundMethodCreator<
})
}
export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => event => {
const {
state: { searched, visibleNodes },
props: { loadWithPJAX },
} = dispatch.get()
if (!visibleNodes) return
const { nodes, focusedNode, expandedNodes } = visibleNodes
function handleVerticalMove(index: number) {
if (0 <= index && index < nodes.length) {
DOMHelper.focusFileExplorer()
dispatch.call(focusNode, nodes[index])
} else {
DOMHelper.focusSearchInput()
dispatch.call(focusNode, null)
export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent<HTMLElement>]> =
dispatch => event => {
const {
state: { searched, visibleNodes },
} = dispatch.get()
if (!visibleNodes) return
const { nodes, focusedNode, expandedNodes } = visibleNodes
function handleVerticalMove(index: number) {
if (0 <= index && index < nodes.length) {
DOMHelper.focusFileExplorer()
dispatch.call(focusNode, nodes[index])
} else {
DOMHelper.focusSearchInput()
dispatch.call(focusNode, null)
}
}
}
const { key } = event
// prevent document body scrolling if the keypress results in Gitako action
let muteEvent = true
if (focusedNode) {
const focusedNodeIndex = nodes.findIndex(node => node.path === focusedNode.path)
switch (key) {
case 'ArrowUp':
// focus on previous node
handleVerticalMove(focusedNodeIndex - 1)
break
case 'ArrowDown':
// focus on next node
handleVerticalMove(focusedNodeIndex + 1)
break
case 'ArrowLeft':
if (wouldBlockHistoryNavigation(event)) {
muteEvent = false
const { key } = event
// prevent document body scrolling if the keypress results in Gitako action
let muteEvent = true
if (focusedNode) {
const focusedNodeIndex = nodes.findIndex(node => node.path === focusedNode.path)
switch (key) {
case 'ArrowUp':
// focus on previous node
handleVerticalMove(focusedNodeIndex - 1)
break
}
if (expandedNodes.has(focusedNode.path)) {
dispatch.call(toggleNodeExpansion, focusedNode, { recursive: event.altKey })
} else {
// go forward to the start of the list, find the closest node with lower depth
const parentNode = getVisibleParentNode(nodes, focusedNode)
if (parentNode) {
dispatch.call(focusNode, parentNode)
case 'ArrowDown':
// focus on next node
handleVerticalMove(focusedNodeIndex + 1)
break
case 'ArrowLeft':
if (wouldBlockHistoryNavigation(event)) {
muteEvent = false
break
}
}
break
// consider the two keys as 'confirm' key
case 'ArrowRight':
if (wouldBlockHistoryNavigation(event)) {
muteEvent = false
break
}
// expand node or focus on first content node or redirect to file page
if (focusedNode.type === 'tree') {
if (expandedNodes.has(focusedNode.path)) {
const nextNode = nodes[focusedNodeIndex + 1]
if (focusedNode.contents?.includes(nextNode)) {
dispatch.call(focusNode, nextNode)
}
dispatch.call(toggleNodeExpansion, focusedNode, { recursive: event.altKey })
} else {
dispatch.call(toggleNodeExpansion, focusedNode, { recursive: event.altKey })
// go forward to the start of the list, find the closest node with lower depth
const parentNode = getVisibleParentNode(nodes, focusedNode)
if (parentNode) {
dispatch.call(focusNode, parentNode)
}
}
} else if (focusedNode.type === 'blob') {
if (focusedNode.url) loadWithPJAX(focusedNode.url)
} else if (focusedNode.type === 'commit') {
window.open(focusedNode.url)
}
break
case 'Enter':
// expand node or redirect to file page
if (searched) {
dispatch.call(goTo, focusedNode.path.split('/'))
} else {
break
// consider the two keys as 'confirm' key
case 'ArrowRight':
if (wouldBlockHistoryNavigation(event)) {
muteEvent = false
break
}
// expand node or focus on first content node or redirect to file page
if (focusedNode.type === 'tree') {
dispatch.call(toggleNodeExpansion, focusedNode, { recursive: event.altKey })
if (expandedNodes.has(focusedNode.path)) {
const nextNode = nodes[focusedNodeIndex + 1]
if (focusedNode.contents?.includes(nextNode)) {
dispatch.call(focusNode, nextNode)
}
} else {
dispatch.call(toggleNodeExpansion, focusedNode, { recursive: event.altKey })
}
} else if (focusedNode.type === 'blob') {
if (focusedNode.url) loadWithPJAX(focusedNode.url)
const focusedNodeElement = DOMHelper.findNodeElement(focusedNode, event.currentTarget)
if (focusedNodeElement && focusedNode.url)
loadWithPJAX(focusedNode.url, focusedNodeElement)
} else if (focusedNode.type === 'commit') {
window.open(focusedNode.url)
}
}
break
default:
muteEvent = false
}
if (muteEvent) {
event.preventDefault()
}
} else {
// now search input is focused
if (nodes.length) {
switch (key) {
case 'ArrowDown':
DOMHelper.focusFileExplorer()
dispatch.call(focusNode, nodes[0])
break
case 'ArrowUp':
DOMHelper.focusFileExplorer()
dispatch.call(focusNode, nodes[nodes.length - 1])
case 'Enter':
// expand node or redirect to file page
if (searched) {
dispatch.call(goTo, focusedNode.path.split('/'))
} else {
if (focusedNode.type === 'tree') {
dispatch.call(toggleNodeExpansion, focusedNode, { recursive: event.altKey })
} else if (focusedNode.type === 'blob') {
const focusedNodeElement = DOMHelper.findNodeElement(focusedNode, event.currentTarget)
if (focusedNodeElement && focusedNode.url)
loadWithPJAX(focusedNode.url, focusedNodeElement)
} else if (focusedNode.type === 'commit') {
window.open(focusedNode.url)
}
}
break
default:
muteEvent = false
@ -210,9 +196,27 @@ export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch
if (muteEvent) {
event.preventDefault()
}
} else {
// now search input is focused
if (nodes.length) {
switch (key) {
case 'ArrowDown':
DOMHelper.focusFileExplorer()
dispatch.call(focusNode, nodes[0])
break
case 'ArrowUp':
DOMHelper.focusFileExplorer()
dispatch.call(focusNode, nodes[nodes.length - 1])
break
default:
muteEvent = false
}
if (muteEvent) {
event.preventDefault()
}
}
}
}
}
export const onFocusSearchBar: BoundMethodCreator = dispatch => () => dispatch.call(focusNode, null)
@ -277,6 +281,8 @@ export const focusNode: BoundMethodCreator<[TreeNode | null]> =
export const onNodeClick: BoundMethodCreator<
[React.MouseEvent<HTMLElement, MouseEvent>, TreeNode]
> = dispatch => (event, node) => {
if (isOpenInNewWindowClick(event)) return
const preventDefault = !(node.type === 'blob' && node.url?.includes('#'))
if (preventDefault) event.preventDefault()
@ -291,12 +297,9 @@ export const onNodeClick: BoundMethodCreator<
(recursiveToggleFolder === 'alt' && event.altKey)
dispatch.call(toggleNodeExpansion, node, { recursive })
} else if (node.type === 'blob') {
const {
props: { loadWithPJAX },
} = dispatch.get()
dispatch.call(focusNode, node)
if (node.url && !node.url.includes('#')) {
loadWithPJAX(node.url)
loadWithPJAX(node.url, event.currentTarget)
}
} else if (node.type === 'commit') {
if (node.url) {

View file

@ -197,6 +197,18 @@ export const GitHub: Platform = {
useGitHubAttachCopySnippetButton(copySnippetButton)
useGitHubCodeFold(codeFolding)
},
delegatePJAXProps(options) {
if (!options?.node || options.node.type === 'blob')
return {
'data-pjax': '#repo-content-pjax-container',
onClick() {
/* Overwriting default onClick */
},
}
},
loadWithPJAX(url, element) {
element.click()
},
}
function sanitizePath(path: string) {

View file

@ -23,5 +23,9 @@ type Platform = {
getCurrentPath(branchName: string): string[] | null
setOAuth(code: string): Promise<string | null>
getOAuthLink(): string
delegatePJAXProps?(options?: {
node?: TreeNode
}): void | (React.DOMAttributes<HTMLElement> & Record<string, any>) // support data-* attributes
loadWithPJAX?(url: string, element: HTMLElement): void
usePlatformHooks?(): void
}

View file

@ -129,3 +129,9 @@ export function setResizingState(on: boolean) {
if (on) target.classList.add('resizing')
else target.classList.remove('resizing')
}
export function findNodeElement(node:TreeNode, rootElement: HTMLElement): HTMLElement | null {
const nodeElement = rootElement.querySelector(`a[href="${node.url}"]`)
if (nodeElement instanceof HTMLElement) return nodeElement
return null
}

View file

@ -193,7 +193,7 @@ export function createPromiseQueue() {
}
}
export function isOpenInNewWindowClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
export function isOpenInNewWindowClick(event: React.MouseEvent<HTMLElement, MouseEvent>) {
return (
(os === OperatingSystems.macOS && event.metaKey) ||
(os === OperatingSystems.Linux && event.ctrlKey) ||

View file

@ -1,4 +1,5 @@
import { Config, Pjax } from 'pjax-api'
import { platform } from 'platforms'
import * as React from 'react'
import { useEvent } from 'react-use'
@ -9,7 +10,7 @@ const config: Config = {
// gitee
'#git-project-content',
// gitea
'.repository > .ui.container'
'.repository > .ui.container',
],
update: {
css: false,
@ -42,8 +43,9 @@ export function usePJAX() {
useRedirectedEvents(document, 'pjax:ready', 'pjax:end')
}
export const loadWithPJAX = (url: string) => {
Pjax.assign(url, config)
export const loadWithPJAX = (url: string, element: HTMLElement) => {
if (platform.loadWithPJAX) platform.loadWithPJAX(url, element)
else Pjax.assign(url, config)
}
export function useOnPJAXDone(callback: () => void) {