Merge branch 'feature/fold-code' into develop

This commit is contained in:
EnixCoda 2021-06-07 18:03:34 +08:00
commit ca0ccbfb01
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
7 changed files with 342 additions and 72 deletions

View file

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

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,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;

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)