feat: fold source code button

This commit is contained in:
EnixCoda 2021-06-07 17:22:00 +08:00
parent d5df98f684
commit 246f794442
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
6 changed files with 277 additions and 4 deletions

View file

@ -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()

View file

@ -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`,
},

View file

@ -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

View 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)
}

View file

@ -13,6 +13,32 @@ $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;
&: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};
}
@ -53,6 +79,75 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
}
}
}
.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;
}
}
// gitee
&.git-project {

View file

@ -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)