mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: customize pjax behavior by platforms
This commit is contained in:
parent
9ac855c153
commit
0de422ffb7
9 changed files with 138 additions and 123 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
4
src/platforms/platform.d.ts
vendored
4
src/platforms/platform.d.ts
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) ||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue