diff --git a/src/components/MetaBar.tsx b/src/components/MetaBar.tsx index 8691180..2f96728 100644 --- a/src/components/MetaBar.tsx +++ b/src/components/MetaBar.tsx @@ -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 || '...'} diff --git a/src/components/Node.tsx b/src/components/Node.tsx index 45d0f58..1b8b2b0 100644 --- a/src/components/Node.tsx +++ b/src/components/Node.tsx @@ -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 ( { - 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 })} >
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index e0d4380..3061845 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -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()} /> diff --git a/src/driver/core/FileExplorer.ts b/src/driver/core/FileExplorer.ts index 7b3619a..2bb9ab6 100644 --- a/src/driver/core/FileExplorer.ts +++ b/src/driver/core/FileExplorer.ts @@ -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: (fn: () => T) => Promise } @@ -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]> = + 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, 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) { diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts index e30acd4..42483c0 100644 --- a/src/platforms/GitHub/index.ts +++ b/src/platforms/GitHub/index.ts @@ -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) { diff --git a/src/platforms/platform.d.ts b/src/platforms/platform.d.ts index 4f91d95..e555d7a 100644 --- a/src/platforms/platform.d.ts +++ b/src/platforms/platform.d.ts @@ -23,5 +23,9 @@ type Platform = { getCurrentPath(branchName: string): string[] | null setOAuth(code: string): Promise getOAuthLink(): string + delegatePJAXProps?(options?: { + node?: TreeNode + }): void | (React.DOMAttributes & Record) // support data-* attributes + loadWithPJAX?(url: string, element: HTMLElement): void usePlatformHooks?(): void } diff --git a/src/utils/DOMHelper.ts b/src/utils/DOMHelper.ts index 8651b7a..f117275 100644 --- a/src/utils/DOMHelper.ts +++ b/src/utils/DOMHelper.ts @@ -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 +} diff --git a/src/utils/general.ts b/src/utils/general.ts index 3c48402..553318d 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -193,7 +193,7 @@ export function createPromiseQueue() { } } -export function isOpenInNewWindowClick(event: React.MouseEvent) { +export function isOpenInNewWindowClick(event: React.MouseEvent) { return ( (os === OperatingSystems.macOS && event.metaKey) || (os === OperatingSystems.Linux && event.ctrlKey) || diff --git a/src/utils/hooks/usePJAX.ts b/src/utils/hooks/usePJAX.ts index f3c26fa..396dcaf 100644 --- a/src/utils/hooks/usePJAX.ts +++ b/src/utils/hooks/usePJAX.ts @@ -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) {