mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
Merge branch 'feature/fold-code' into develop
This commit is contained in:
commit
ca0ccbfb01
7 changed files with 342 additions and 72 deletions
|
|
@ -35,7 +35,7 @@ export function SearchBar({ onSearch, onFocus, value }: Props) {
|
|||
/>
|
||||
<div className={`actions`}>
|
||||
<button
|
||||
className={`toggle-mode`}
|
||||
className={`toggle-search-mode`}
|
||||
title="Toggle search mode"
|
||||
onClick={() => {
|
||||
const newMode = searchMode === 'regex' ? 'fuzzy' : 'regex'
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { SideBarBodyWrapper } from 'components/SideBarBodyWrapper'
|
|||
import { ToggleShowButton } from 'components/ToggleShowButton'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import { platform } from 'platforms'
|
||||
import { useGitHubAttachCopyFileButton, useGitHubAttachCopySnippetButton } from 'platforms/GitHub'
|
||||
import {
|
||||
useGitHubAttachCopyFileButton,
|
||||
useGitHubAttachCopySnippetButton,
|
||||
useGitHubCodeFold,
|
||||
} from 'platforms/GitHub'
|
||||
import * as React from 'react'
|
||||
import { cx } from 'utils/cx'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
|
|
@ -98,6 +102,7 @@ export function SideBar() {
|
|||
|
||||
useGitHubAttachCopyFileButton(configContext.value.copyFileButton)
|
||||
useGitHubAttachCopySnippetButton(configContext.value.copySnippetButton)
|
||||
useGitHubCodeFold(configContext.value.codeFolding)
|
||||
|
||||
usePJAX()
|
||||
useProgressBar()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const WIKI_HOME_LINK = 'https://github.com/EnixCoda/Gitako/wiki'
|
|||
export const wikiLinks = {
|
||||
compressSingletonFolder: `${WIKI_HOME_LINK}/Compress-Singleton-Folder`,
|
||||
changeLog: `${WIKI_HOME_LINK}/Change-Log`,
|
||||
codeFolding: `${WIKI_HOME_LINK}/Code-folding`,
|
||||
copyFileButton: `${WIKI_HOME_LINK}/Copy-file-and-snippet`,
|
||||
copySnippet: `${WIKI_HOME_LINK}/Copy-file-and-snippet`,
|
||||
createAccessToken: `${WIKI_HOME_LINK}/Access-token-for-Gitako`,
|
||||
|
|
@ -29,18 +30,24 @@ function SettingsBarContent() {
|
|||
const useReloadHint = useStateIO<React.ReactNode>('')
|
||||
const { value: reloadHint } = useReloadHint
|
||||
|
||||
const moreFields: SimpleField<'copyFileButton' | 'copySnippetButton'>[] =
|
||||
const moreFields: SimpleField<'copyFileButton' | 'copySnippetButton'|'codeFolding'>[] =
|
||||
platform === GitHub
|
||||
? [
|
||||
{
|
||||
key: 'codeFolding',
|
||||
label: 'Fold source code button',
|
||||
wikiLink: wikiLinks.codeFolding,
|
||||
tooltip: `Read more in Gitako's Wiki`,
|
||||
},
|
||||
{
|
||||
key: 'copyFileButton',
|
||||
label: 'Copy file shortcut',
|
||||
label: 'Copy file button',
|
||||
wikiLink: wikiLinks.copyFileButton,
|
||||
tooltip: `Read more in Gitako's Wiki`,
|
||||
},
|
||||
{
|
||||
key: 'copySnippetButton',
|
||||
label: 'Copy snippet shortcut',
|
||||
label: 'Copy snippet button',
|
||||
wikiLink: wikiLinks.copySnippet,
|
||||
tooltip: `Read more in Gitako's Wiki`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { sortFoldersToFront } from 'utils/treeParser'
|
|||
import * as API from './API'
|
||||
import * as DOMHelper from './DOMHelper'
|
||||
import * as URLHelper from './URLHelper'
|
||||
export { useGitHubCodeFold } from './useGitHubCodeFold'
|
||||
|
||||
function processTree(tree: TreeNode[]): TreeNode {
|
||||
// nodes are created from items and put onto tree
|
||||
|
|
|
|||
162
src/platforms/GitHub/useGitHubCodeFold.tsx
Normal file
162
src/platforms/GitHub/useGitHubCodeFold.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { platform } from 'platforms'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useOnPJAXDone } from 'utils/hooks/usePJAX'
|
||||
import { GitHub } from '.'
|
||||
|
||||
const theCSSClassMark = 'gitako-code-fold-attached'
|
||||
const theCSSClassMarkWhenDisabled = 'gitako-code-fold-attached-disabled'
|
||||
const theCSSClassForHiddenLine = 'gitako-code-fold-hidden'
|
||||
const theCSSClassForToggleElement = 'gitako-code-fold-handler'
|
||||
const tableSelector = `.blob-wrapper table`
|
||||
const selectorOfLineNumber = `.blob-num`
|
||||
const selectorOfLineContent = `.blob-code`
|
||||
|
||||
const theCSSClassForToggleElementOnActive = 'active'
|
||||
function init() {
|
||||
type LineNumber = number // alias
|
||||
const blocks: LineNumber[] = [] // startLine -> exclusiveEndLine
|
||||
/**
|
||||
* For example, given such input
|
||||
*
|
||||
* 0 |∇ line1
|
||||
* 1 |∇ line2
|
||||
* 2 |
|
||||
* 3 |∇ line3
|
||||
* 4 | line4
|
||||
* 5 | line5
|
||||
*
|
||||
* blocks will be
|
||||
* 0 -> 5
|
||||
* 1 -> 3
|
||||
* 2
|
||||
* 3 -> 5
|
||||
* 4
|
||||
* 5
|
||||
*/
|
||||
|
||||
const table = document.querySelector(tableSelector)
|
||||
const lineElements = Array.from(document.querySelectorAll([tableSelector, 'tr'].join(' ')))
|
||||
if (!table || !lineElements.length) return
|
||||
if (table.classList.contains(theCSSClassMark)) {
|
||||
if (table.classList.contains(theCSSClassMarkWhenDisabled)) {
|
||||
// reactivate
|
||||
table.classList.remove(theCSSClassMarkWhenDisabled)
|
||||
}
|
||||
return
|
||||
}
|
||||
table.classList.add(theCSSClassMark)
|
||||
|
||||
// setup blocks
|
||||
{
|
||||
type Level = number // measured by leading whitespace amount
|
||||
const stack: [Level, LineNumber][] = []
|
||||
|
||||
function trySeal(lineNumber: number, level: number) {
|
||||
let ignoredTheHighestLevelItem = false
|
||||
while (stack.length) {
|
||||
const top = stack.pop()! // safe
|
||||
const [$LineNumber, $level] = top
|
||||
|
||||
if ($level < level) {
|
||||
stack.push(top)
|
||||
break
|
||||
} else if (!ignoredTheHighestLevelItem) {
|
||||
ignoredTheHighestLevelItem = true
|
||||
} else {
|
||||
blocks[$LineNumber] = lineNumber
|
||||
}
|
||||
}
|
||||
stack.push([lineNumber, level])
|
||||
}
|
||||
|
||||
lineElements.forEach((element, lineNumber) => {
|
||||
const text = element.querySelector(selectorOfLineContent)?.textContent || null
|
||||
if (text === null) return
|
||||
|
||||
const level = getTextLevel(text)
|
||||
if (level === -1) return
|
||||
|
||||
trySeal(lineNumber, level)
|
||||
})
|
||||
|
||||
trySeal(lineElements.length, -1)
|
||||
}
|
||||
|
||||
// attach toggle buttons
|
||||
blocks.forEach((end, line) => {
|
||||
if (!end) return
|
||||
|
||||
const toggleElement = document.createElement('div')
|
||||
toggleElement.setAttribute('role', 'button')
|
||||
toggleElement.classList.add(theCSSClassForToggleElement)
|
||||
|
||||
const lineNumberElement = lineElements[line].querySelector(selectorOfLineNumber)
|
||||
lineNumberElement?.appendChild(toggleElement)
|
||||
})
|
||||
|
||||
const foldLines = new Set<LineNumber>()
|
||||
function toggleLine(line: number) {
|
||||
// const lineElements = Array.from(document.querySelectorAll(lineSelector))
|
||||
const element = lineElements[line]
|
||||
element.classList.toggle(theCSSClassForToggleElementOnActive)
|
||||
if (foldLines.has(line)) {
|
||||
foldLines.delete(line)
|
||||
} else {
|
||||
foldLines.add(line)
|
||||
}
|
||||
|
||||
const linesToHide = new Set<LineNumber>()
|
||||
for (const line of foldLines.values()) {
|
||||
const end = blocks[line]
|
||||
for (let $line = line + 1; $line < end; $line++) linesToHide.add($line)
|
||||
}
|
||||
|
||||
lineElements.forEach((element, i) => {
|
||||
if (linesToHide.has(i)) element.classList.add(theCSSClassForHiddenLine)
|
||||
else element.classList.remove(theCSSClassForHiddenLine)
|
||||
})
|
||||
}
|
||||
|
||||
table.addEventListener('click', e => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (e.target.classList.contains(theCSSClassForToggleElement)) {
|
||||
const tr = e.target.parentElement?.parentElement
|
||||
if (tr) {
|
||||
toggleLine(lineElements.indexOf(tr))
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cancelToggleFeature() {
|
||||
const table = document.querySelector(tableSelector)
|
||||
table?.classList.add(theCSSClassMarkWhenDisabled)
|
||||
}
|
||||
|
||||
function getTextLevel(text: string) {
|
||||
const leading = countLeadingWhitespace(text)
|
||||
return leading === text.length ? -1 : leading
|
||||
}
|
||||
|
||||
function countLeadingWhitespace(text: string) {
|
||||
let i = 0
|
||||
for (; i < text.length; i++) {
|
||||
// Mixed usage of space and table indentation? ¯\_(ツ)_/¯
|
||||
if (!(text[i] === ' ' || text[i] === '\t' || text[i] === '\n')) break
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
export function useGitHubCodeFold(active: boolean) {
|
||||
const effect = useCallback(() => {
|
||||
if (platform !== GitHub) return
|
||||
if (active) {
|
||||
init()
|
||||
return () => cancelToggleFeature()
|
||||
}
|
||||
}, [active])
|
||||
useEffect(effect, [effect])
|
||||
useOnPJAXDone(effect)
|
||||
}
|
||||
|
|
@ -13,6 +13,33 @@ $github-header-z-index: 32;
|
|||
$github-pull-request-float-header-z-index: 110;
|
||||
$minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-z-index) + 1;
|
||||
|
||||
@mixin interactive-background($default, $hover, $active) {
|
||||
background-color: $default;
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: $hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: $active;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin interactive-background-on-before($default, $hover, $active) {
|
||||
&::before {
|
||||
background-color: $default;
|
||||
}
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: $hover;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
&::before {
|
||||
background-color: $active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--gitako-width: #{$side-bar-base-width};
|
||||
}
|
||||
|
|
@ -54,6 +81,117 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
}
|
||||
}
|
||||
|
||||
// code folding start
|
||||
.blob-wrapper table .blob-num {
|
||||
position: relative; // for positioning
|
||||
min-width: 60px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
// cancel code fold if not wrapped with .gitako-code-fold-mark
|
||||
.gitako-code-fold-handler {
|
||||
display: none;
|
||||
}
|
||||
.gitako-code-fold-hidden {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.gitako-code-fold-attached {
|
||||
tr {
|
||||
.gitako-code-fold-handler {
|
||||
display: initial;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: 0.25s ease;
|
||||
-webkit-mask-image: url('~@primer/octicons/build/svg/chevron-down.svg?inline');
|
||||
mask-image: url('~@primer/octicons/build/svg/chevron-down.svg?inline');
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
@include interactive-background-on-before(
|
||||
var(--gitako-icon-tertiary),
|
||||
var(--gitako-icon-primary),
|
||||
var(--gitako-icon-secondary)
|
||||
);
|
||||
}
|
||||
|
||||
&.active {
|
||||
.gitako-code-fold-handler {
|
||||
&::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
@include interactive-background-on-before(
|
||||
var(--gitako-icon-secondary),
|
||||
var(--gitako-icon-tertiary),
|
||||
var(--gitako-icon-primary)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gitako-code-fold-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// code folding end
|
||||
|
||||
// clippy button
|
||||
.markdown-body {
|
||||
.clippy-wrapper {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 8px;
|
||||
left: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
|
||||
.clippy {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--gitako-border-tertiary);
|
||||
border-radius: 4px;
|
||||
|
||||
@include interactive-background(
|
||||
var(--gitako-btn-bg),
|
||||
var(--gitako-btn-hover-bg),
|
||||
var(--gitako-btn-focus-bg)
|
||||
);
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background-image: url('~@primer/octicons-react/build/svg/clippy-16.svg?inline');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
&.success {
|
||||
background-image: url('~@primer/octicons-react/build/svg/check-16.svg?inline');
|
||||
}
|
||||
&.fail {
|
||||
background-image: url('~@primer/octicons-react/build/svg/x-16.svg?inline');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gitee
|
||||
&.git-project {
|
||||
#git-header-nav {
|
||||
|
|
@ -97,45 +235,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
.clippy-wrapper {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 8px;
|
||||
left: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
|
||||
.clippy {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--gitako-border-tertiary);
|
||||
border-radius: 4px;
|
||||
background: var(--gitako-bg-secondary);
|
||||
&:hover {
|
||||
background: var(--gitako-bg-secondary);
|
||||
}
|
||||
&:active {
|
||||
background: var(--gitako-bg-secondary);
|
||||
}
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background-image: url('~@primer/octicons-react/build/svg/clippy-16.svg?inline');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
&.success {
|
||||
background-image: url('~@primer/octicons-react/build/svg/check-16.svg?inline');
|
||||
}
|
||||
&.fail {
|
||||
background-image: url('~@primer/octicons-react/build/svg/x-16.svg?inline');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-pjax-loader.is-loading {
|
||||
left: 0; /* reposition progress bar of GitHub */
|
||||
}
|
||||
|
|
@ -154,17 +253,13 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
@include flex-center();
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@mixin button-color {
|
||||
border: none;
|
||||
background: transparent;
|
||||
&:hover {
|
||||
background: var(--gitako-btn-hover-bg);
|
||||
}
|
||||
&:active {
|
||||
background: var(--gitako-btn-focus-bg);
|
||||
}
|
||||
|
||||
@include interactive-background(
|
||||
transparent,
|
||||
var(--gitako-btn-hover-bg),
|
||||
var(--gitako-btn-focus-bg)
|
||||
);
|
||||
}
|
||||
|
||||
// Why does not TextInput get theme properly?
|
||||
|
|
@ -354,7 +449,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
|
||||
.close-side-bar-button {
|
||||
@include icon-button;
|
||||
@include button-color;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
border-radius: $button-size;
|
||||
|
|
@ -493,25 +587,23 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
align-items: center;
|
||||
padding: 0 4px;
|
||||
|
||||
.toggle-mode {
|
||||
.toggle-search-mode {
|
||||
margin: 0;
|
||||
padding: 2px 4px;
|
||||
min-width: 32px;
|
||||
border: 1px solid var(--gitako-border-secondary);
|
||||
border-radius: 8px;
|
||||
background: var(--gitako-bg-primary);
|
||||
color: var(--gitako-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
outline: none;
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: var(--gitako-bg-secondary);
|
||||
}
|
||||
&:active {
|
||||
background: var(--gitako-bg-tertiary);
|
||||
}
|
||||
|
||||
@include interactive-background(
|
||||
var(--gitako-bg-primary),
|
||||
var(--gitako-bg-secondary),
|
||||
var(--gitako-bg-tertiary)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -523,20 +615,22 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
}
|
||||
|
||||
.node-item {
|
||||
background: var(--gitako-bg-primary);
|
||||
&:hover {
|
||||
text-decoration: initial; // revert underline from .gitako-side-bar a:hover
|
||||
.node-item-label {
|
||||
text-decoration: underline; // apply underline like .gitako-side-bar a:hover
|
||||
}
|
||||
}
|
||||
&.focused,
|
||||
&:hover {
|
||||
|
||||
&.focused {
|
||||
background: var(--gitako-bg-tertiary);
|
||||
}
|
||||
&:active {
|
||||
background: var(--gitako-bg-secondary);
|
||||
}
|
||||
|
||||
@include interactive-background(
|
||||
var(--gitako-bg-primary),
|
||||
var(--gitako-bg-secondary),
|
||||
var(--gitako-bg-tertiary)
|
||||
);
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
|
|
@ -618,7 +712,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
.go-to-button,
|
||||
.find-in-folder-button {
|
||||
@include icon-button();
|
||||
@include button-color();
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 28px;
|
||||
|
|
@ -758,7 +851,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
|
||||
.settings-button {
|
||||
@include icon-button();
|
||||
@include button-color();
|
||||
$size: 32px;
|
||||
width: $size;
|
||||
height: $size;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type Config = {
|
|||
searchMode: SearchMode
|
||||
sidebarToggleMode: 'persistent' | 'float'
|
||||
commentToggle: boolean
|
||||
codeFolding: boolean
|
||||
}
|
||||
|
||||
enum configKeys {
|
||||
|
|
@ -35,6 +36,7 @@ enum configKeys {
|
|||
searchMode = 'searchMode',
|
||||
sidebarToggleMode = 'sidebarToggleMode',
|
||||
commentToggle = 'commentToggle',
|
||||
codeFolding = 'codeFolding',
|
||||
}
|
||||
|
||||
export const defaultConfigs: Config = {
|
||||
|
|
@ -52,6 +54,7 @@ export const defaultConfigs: Config = {
|
|||
searchMode: 'fuzzy',
|
||||
sidebarToggleMode: 'float',
|
||||
commentToggle: true,
|
||||
codeFolding: true,
|
||||
}
|
||||
|
||||
const configKeyArray = Object.values(configKeys)
|
||||
|
|
|
|||
Loading…
Reference in a new issue