mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
initial commit
This commit is contained in:
parent
1a0272e3bf
commit
608345dc10
26 changed files with 5762 additions and 0 deletions
17
.babelrc
Normal file
17
.babelrc
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"babel-preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "60"
|
||||
}
|
||||
}
|
||||
],
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
["transform-object-rest-spread", { "useBuiltIns": true }]
|
||||
]
|
||||
}
|
||||
26
.eslintrc.json
Normal file
26
.eslintrc.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-const-assign": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-unreachable": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"constructor-super": "warn",
|
||||
"valid-typeof": "warn"
|
||||
},
|
||||
"plugins": [
|
||||
"class-property"
|
||||
]
|
||||
}
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.vscode
|
||||
node_modules
|
||||
tmp
|
||||
dist
|
||||
41
package.json
Normal file
41
package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "gitako",
|
||||
"version": "0.1.0",
|
||||
"description": "yet another extension for GitHub",
|
||||
"repository": "https://github.com/EnixCoda/Gitako",
|
||||
"author": "EnixCoda",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "webpack --watch",
|
||||
"dev": "webpack",
|
||||
"prod": "NODE_ENV=production webpack && cd dist && rm ./gitako.zip && zip gitako.zip *"
|
||||
},
|
||||
"dependencies": {
|
||||
"pjax": "^0.2.4",
|
||||
"preact": "^8.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-react-require": "^3.0.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"copy-webpack-plugin": "^4.2.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"file-loader": "^1.1.5",
|
||||
"less": "^2.7.3",
|
||||
"less-loader": "^4.0.5",
|
||||
"style-loader": "^0.19.0",
|
||||
"uglifyjs-webpack-plugin": "^1.1.1",
|
||||
"url-loader": "^0.6.2",
|
||||
"webpack": "^3.8.1"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
}
|
||||
BIN
src/assets/font/octicons.woff2
Executable file
BIN
src/assets/font/octicons.woff2
Executable file
Binary file not shown.
192
src/components/FileExplorer.js
Normal file
192
src/components/FileExplorer.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
import SearchBar from './SearchBar'
|
||||
import Node from './Node'
|
||||
|
||||
import DOMHelper from '../utils/DOMHelper'
|
||||
import treeParser from '../utils/treeParser'
|
||||
import VisibleNodesGenerator from '../utils/VisibleNodesGenerator'
|
||||
|
||||
export default class List extends preact.Component {
|
||||
state = {
|
||||
// generated by this.visibleNodesGenerator
|
||||
visibleNodes: null,
|
||||
}
|
||||
|
||||
props = {
|
||||
treeData: null,
|
||||
metaData: null,
|
||||
}
|
||||
|
||||
tasksAfterRender = []
|
||||
visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
componentWillMount() {
|
||||
const { treeData, metaData } = this.props
|
||||
const { root, nodes } = treeParser.parse(treeData, metaData)
|
||||
this.visibleNodesGenerator.plantTree(root, nodes)
|
||||
this.updateVisibleNodes()
|
||||
this.tasksAfterRender.push(DOMHelper.focusFileExplorer)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.execAfterRender()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.execAfterRender()
|
||||
}
|
||||
|
||||
execAfterRender() {
|
||||
for (const task of this.tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
this.tasksAfterRender.length = 0
|
||||
}
|
||||
|
||||
updateVisibleNodes() {
|
||||
const { visibleNodes } = this.visibleNodesGenerator
|
||||
this.setState({ visibleNodes })
|
||||
this.tasksAfterRender.push(() => DOMHelper.attachPJAX('gitako'))
|
||||
}
|
||||
|
||||
handleKeyDown = event => {
|
||||
const { key } = event
|
||||
const { visibleNodes: { nodes, focusedNode, expandedNodes, depths } } = this.state
|
||||
let shouldStopPropagation = true // prevent body scrolling
|
||||
if (focusedNode) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
switch (key) {
|
||||
case 'ArrowUp':
|
||||
// focus on previous node
|
||||
if (focusedNodeIndex === 0) {
|
||||
this.focusNode(null)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
} else {
|
||||
this.focusNode(nodes[focusedNodeIndex - 1])
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrowDown':
|
||||
// focus on next node
|
||||
if (focusedNodeIndex + 1 < nodes.length) {
|
||||
this.focusNode(nodes[focusedNodeIndex + 1])
|
||||
} else {
|
||||
this.focusNode(null)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrowLeft':
|
||||
// collapse node or go to parent node
|
||||
if (expandedNodes.has(focusedNode)) {
|
||||
this.setExpand(focusedNode, false)
|
||||
} else {
|
||||
// go forward to the start of the list, find the closest node with lower depth
|
||||
let indexOfParentNode = focusedNodeIndex
|
||||
const focusedNodeDepth = depths.get(nodes[focusedNodeIndex])
|
||||
while (
|
||||
indexOfParentNode !== -1 &&
|
||||
depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth
|
||||
) {
|
||||
--indexOfParentNode
|
||||
}
|
||||
if (indexOfParentNode !== -1) {
|
||||
this.focusNode(nodes[indexOfParentNode])
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
// consider the two keys as 'confirm' key
|
||||
case 'ArrowRight':
|
||||
case 'Enter':
|
||||
// expand node or redirect to file page
|
||||
if (focusedNode.type === 'tree') {
|
||||
this.setExpand(focusedNode, true)
|
||||
} else {
|
||||
// simulate click to trigger pjax
|
||||
DOMHelper.clickOnNodeElement(focusedNodeIndex)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
this.focusNode(nodes[0])
|
||||
break
|
||||
case 'ArrowUp':
|
||||
this.focusNode(nodes[nodes.length - 1])
|
||||
break
|
||||
default:
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
} else {
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
}
|
||||
if (shouldStopPropagation) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchKeyChange = async event => {
|
||||
const searchKey = event.target.value
|
||||
await this.visibleNodesGenerator.search(searchKey)
|
||||
this.updateVisibleNodes()
|
||||
}
|
||||
|
||||
setExpand = (node, expand) => {
|
||||
this.visibleNodesGenerator.setExpand(node, expand)
|
||||
this.focusNode(node)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
|
||||
toggleNodeExpand = node => {
|
||||
this.visibleNodesGenerator.toggleExpand(node)
|
||||
this.focusNode(node)
|
||||
this.tasksAfterRender.push(DOMHelper.focusFileExplorer)
|
||||
}
|
||||
|
||||
focusNode = node => {
|
||||
this.visibleNodesGenerator.focusNode(node)
|
||||
if (node) {
|
||||
// when focus a node not in viewport(by keyboard), scroll to it
|
||||
const { visibleNodes: { nodes } } = this.state
|
||||
const indexOfToBeFocusedNode = nodes.indexOf(node)
|
||||
this.tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(indexOfToBeFocusedNode))
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
this.updateVisibleNodes()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visibleNodes: { nodes, depths, focusedNode, expandedNodes } } = this.state
|
||||
return (
|
||||
<div className={`file-explorer`} tabIndex={-1} onKeyDown={this.handleKeyDown}>
|
||||
<SearchBar onSearchKeyChange={this.handleSearchKeyChange} />
|
||||
{nodes.length === 0 ? (
|
||||
<label className={'no-results'}>No results found.</label>
|
||||
) : (
|
||||
nodes.map(node => (
|
||||
<Node
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={depths.get(node)}
|
||||
focused={focusedNode === node}
|
||||
expanded={expandedNodes.has(node)}
|
||||
toggleExpand={this.toggleNodeExpand.bind(null, node)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
135
src/components/GitakoSideBar.js
Normal file
135
src/components/GitakoSideBar.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
import FileExplorer from './FileExplorer'
|
||||
import Logo from './Logo'
|
||||
import MetaBar from './MetaBar'
|
||||
import SettingsBar from './SettingsBar'
|
||||
|
||||
import cx from '../utils/cx'
|
||||
import DOMHelper, { REPO_TYPE_PRIVATE } from '../utils/DOMHelper'
|
||||
import GitHubHelper, { NOT_FOUND } from '../utils/GitHubHelper'
|
||||
import storageHelper from '../utils/storageHelper'
|
||||
import URLHelper from '../utils/URLHelper'
|
||||
|
||||
export default class GitakoSideBar extends preact.Component {
|
||||
state = {
|
||||
// whether Gitako side bar should be shown
|
||||
shouldShow: false,
|
||||
// whether show settings pane
|
||||
showSettings: false,
|
||||
// whether pending for network request
|
||||
loading: true,
|
||||
// whether failed loading the repo due to it is private
|
||||
errorDueToPrivateRepo: false,
|
||||
// got access token for GitHub
|
||||
hasAccessToken: null,
|
||||
// meta data for the repository
|
||||
metaData: null,
|
||||
// file tree data
|
||||
treeData: null,
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
try {
|
||||
const metaDataFromUrl = URLHelper.parse()
|
||||
this.setState({ metaData: metaDataFromUrl })
|
||||
this.decorateGitHubPageContent()
|
||||
const accessToken = await storageHelper.getAccessToken()
|
||||
this.setState({ hasAccessToken: !!accessToken })
|
||||
const metaDataFromAPI = await GitHubHelper.getRepoMeta({ ...metaDataFromUrl, accessToken })
|
||||
const branchName = metaDataFromUrl.branchName || metaDataFromAPI['default_branch']
|
||||
const metaData = { ...metaDataFromUrl, branchName, api: metaDataFromAPI }
|
||||
this.setState({ metaData })
|
||||
this.setShouldShow(URLHelper.detectShouldShow(metaData))
|
||||
const treeData = await GitHubHelper.getTreeData({ ...metaData, accessToken })
|
||||
this.setState({ treeData, loading: false })
|
||||
|
||||
window.addEventListener('pjax:send', this.onPJAXStart)
|
||||
window.addEventListener('pjax:complete', this.onPJAXEnd)
|
||||
} catch (err) {
|
||||
// TODO: detect request time exceeds limit
|
||||
if (err.message === NOT_FOUND) {
|
||||
const repoPageType = await DOMHelper.getRepoPageType()
|
||||
const errorDueToPrivateRepo = repoPageType === REPO_TYPE_PRIVATE
|
||||
this.setState({
|
||||
showSettings: repoPageType !== null,
|
||||
errorDueToPrivateRepo,
|
||||
})
|
||||
this.setShouldShow(errorDueToPrivateRepo)
|
||||
} else {
|
||||
console.error(err)
|
||||
this.setShouldShow(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPJAXStart = () => {
|
||||
this.setState({ loading: true })
|
||||
}
|
||||
|
||||
onPJAXEnd = () => {
|
||||
this.setState({ loading: false })
|
||||
this.setShouldShow(URLHelper.detectShouldShow())
|
||||
this.decorateGitHubPageContent()
|
||||
DOMHelper.scrollToRepoContent()
|
||||
DOMHelper.focusSearchInput()
|
||||
}
|
||||
|
||||
decorateGitHubPageContent() {
|
||||
DOMHelper.attachCopyFileBtn()
|
||||
DOMHelper.attachCopySnippet()
|
||||
DOMHelper.attachPJAX('github')
|
||||
}
|
||||
|
||||
setShouldShow = shouldShow => {
|
||||
this.setState({ shouldShow })
|
||||
DOMHelper.setBodyIndent(shouldShow)
|
||||
}
|
||||
|
||||
toggleShowSettings = () => {
|
||||
const { showSettings } = this.state
|
||||
this.setState({ showSettings: !showSettings })
|
||||
}
|
||||
|
||||
renderPrivateRepoError() {
|
||||
return (
|
||||
<div className={'description'}>
|
||||
<h5>Access Denied</h5>
|
||||
<p>
|
||||
Gitako need access token with proper scopes (recommended: repo) to read this repository's
|
||||
data.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
shouldShow,
|
||||
loading,
|
||||
metaData,
|
||||
treeData,
|
||||
showSettings,
|
||||
hasAccessToken,
|
||||
errorDueToPrivateRepo,
|
||||
} = this.state
|
||||
return (
|
||||
<div className={cx('gitako', { hidden: !shouldShow })}>
|
||||
<div className={'gitako-side-bar'}>
|
||||
<Logo loading={loading} />
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
{metaData && <MetaBar metaData={metaData} />}
|
||||
{errorDueToPrivateRepo && this.renderPrivateRepoError()}
|
||||
{metaData && treeData && <FileExplorer metaData={metaData} treeData={treeData} />}
|
||||
</div>
|
||||
<SettingsBar
|
||||
toggleShowSettings={this.toggleShowSettings}
|
||||
activated={showSettings}
|
||||
hasAccessToken={hasAccessToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
46
src/components/Icon.js
Normal file
46
src/components/Icon.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
function getIconClassName(type) {
|
||||
switch (type) {
|
||||
case 'folder':
|
||||
return 'triangle-right'
|
||||
case '.pdf':
|
||||
return 'file-pdf'
|
||||
case '.txt':
|
||||
return 'file-text'
|
||||
case '.zip':
|
||||
case '.rar':
|
||||
case '.7z':
|
||||
return 'file-zip'
|
||||
case '.md':
|
||||
return 'markdown'
|
||||
case '.png':
|
||||
case '.jpg':
|
||||
case '.gif':
|
||||
case '.bmp':
|
||||
return 'file-media'
|
||||
case '.js':
|
||||
case '.jsx':
|
||||
case '.ts':
|
||||
case '.tsx':
|
||||
case '.es6':
|
||||
case '.coffee':
|
||||
case '.css':
|
||||
case '.less':
|
||||
case '.scss':
|
||||
case '.sass':
|
||||
return 'file-code'
|
||||
// TODO: adapt to more file types
|
||||
// case '': return 'file-binary'
|
||||
// case '': return 'file-submodule'
|
||||
// case '': return 'file-symlink-directory'
|
||||
// case '': return 'file-symlink-file'
|
||||
default:
|
||||
return 'file'
|
||||
}
|
||||
}
|
||||
|
||||
export default function Icon({ type }) {
|
||||
return <span className={`octicon octicon-${getIconClassName(type)} octicon-color`} />
|
||||
}
|
||||
14
src/components/Logo.js
Normal file
14
src/components/Logo.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
import cx from '../utils/cx'
|
||||
|
||||
export default function Logo({ loading }) {
|
||||
return (
|
||||
<div className={'gitako-logo-wrapper Header'}>
|
||||
<h3> </h3>
|
||||
<h3 className={cx('gitako-logo', { invisible: loading })}>Gitako</h3>
|
||||
<h3 className={cx('gitako-logo breath', { invisible: !loading })}>Gitako</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
18
src/components/MetaBar.js
Normal file
18
src/components/MetaBar.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
export default function MetaBar({ metaData }) {
|
||||
const userUrl = metaData ? metaData.api && metaData.api.owner.html_url : undefined
|
||||
const repoUrl = metaData ? metaData.api && metaData.api.html_url : undefined
|
||||
return (
|
||||
<div className={'meta-bar'}>
|
||||
<a className={'username'} href={userUrl}>
|
||||
{metaData.userName}
|
||||
</a>
|
||||
/
|
||||
<a className={'repo-name pjax-link'} href={repoUrl}>
|
||||
{metaData.repoName}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
42
src/components/Node.js
Normal file
42
src/components/Node.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
import Icon from './Icon'
|
||||
|
||||
import cx from '../utils/cx'
|
||||
|
||||
function getIconType(node) {
|
||||
switch (node.type) {
|
||||
case 'tree':
|
||||
return 'folder'
|
||||
default:
|
||||
return node.name.replace(/.*\./, '.')
|
||||
}
|
||||
}
|
||||
|
||||
export default function Node({ node, depth, expanded, focused, toggleExpand }) {
|
||||
const { name, url, type } = node
|
||||
const item = (
|
||||
<p
|
||||
className={cx('node-item', { expanded })}
|
||||
style={{ paddingLeft: `${10 + 20 * depth}px` }}
|
||||
onClick={node.type === 'tree' ? toggleExpand : undefined}
|
||||
>
|
||||
<Icon type={getIconType(node)} />
|
||||
<span className={'node-item-name'}>{name}</span>
|
||||
</p>
|
||||
)
|
||||
return (
|
||||
<div className={cx(`node-item-row`, { focused })}>
|
||||
{
|
||||
type !== 'tree'
|
||||
? (
|
||||
<a className={'pjax-link'} href={url} tabIndex={-1}>
|
||||
{ item }
|
||||
</a>
|
||||
)
|
||||
: item
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
src/components/SearchBar.js
Normal file
17
src/components/SearchBar.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
export default function SearchBar({ onSearchKeyChange }) {
|
||||
return (
|
||||
<div className={'search-input-wrapper'}>
|
||||
<input
|
||||
tabIndex={0}
|
||||
className="form-control search-input"
|
||||
aria-label="search files"
|
||||
placeholder="Search files (regexp)"
|
||||
type="text"
|
||||
onInput={onSearchKeyChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
src/components/SettingsBar.js
Normal file
102
src/components/SettingsBar.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
import storageHelper from '../utils/storageHelper'
|
||||
|
||||
const ACCESS_TOKEN_REGEXP = /^[0-9a-f]{40}$/
|
||||
|
||||
export default class SettingsBar extends preact.Component {
|
||||
state = {
|
||||
hint: null,
|
||||
tokenCleared: false,
|
||||
}
|
||||
|
||||
handleAccessTokenChange = event => {
|
||||
const value = event.target.value
|
||||
const { hasAccessToken } = this.props
|
||||
if (value === '') {
|
||||
if (hasAccessToken) {
|
||||
storageHelper.setAccessToken('')
|
||||
this.setState({ tokenCleared: true })
|
||||
} else {
|
||||
this.setState({ hint: '' })
|
||||
}
|
||||
} else if (ACCESS_TOKEN_REGEXP.test(value)) {
|
||||
storageHelper.setAccessToken(value)
|
||||
this.setState({ hint: 'Your token is saved, refresh the page to make it work!' })
|
||||
} else {
|
||||
this.setState({ hint: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hint, tokenCleared } = this.state
|
||||
const { toggleShowSettings, activated, hasAccessToken } = this.props
|
||||
return (
|
||||
<div className={'gitako-settings-bar'}>
|
||||
<div className={'placeholder-row'}>
|
||||
<h3>{activated ? 'Settings' : ''}</h3>
|
||||
<span
|
||||
className={`settings-icon octicon octicon-${activated ? 'x' : 'gear'} octicon-color`}
|
||||
onClick={toggleShowSettings}
|
||||
/>
|
||||
</div>
|
||||
{activated && (
|
||||
<div className={'gitako-settings-bar-content'}>
|
||||
<div className={'gitako-settings-bar-content-section access-token'}>
|
||||
<h4>Access Token</h4>
|
||||
<span>
|
||||
With access token, Gitako will be able to browse your private repositories with no
|
||||
API request time limit.
|
||||
</span>
|
||||
<br />
|
||||
<a href="https://github.com/blog/1509-personal-api-tokens" target="_blank">
|
||||
How to create access token?
|
||||
</a>
|
||||
<br />
|
||||
<span>
|
||||
Gitako stores the token in{' '}
|
||||
<a href="https://developer.chrome.com/apps/storage" target="_blank">
|
||||
chrome local storage
|
||||
</a>{' '}
|
||||
locally and safely.
|
||||
</span>
|
||||
<br />
|
||||
<input
|
||||
className={'access-token-input form-control'}
|
||||
placeholder={
|
||||
hasAccessToken
|
||||
? tokenCleared ? 'Your token is cleared' : 'Your token is saved'
|
||||
: 'Input your token here'
|
||||
}
|
||||
onInput={this.handleAccessTokenChange}
|
||||
/>
|
||||
{hint && <span className={'hint'}>{hint}</span>}
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section position'}>
|
||||
<h5>Position of Gitako (WIP)</h5>
|
||||
<select value={'next to'} disabled>
|
||||
<option value="next to">next to main content</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section TOC'}>
|
||||
<h5>Table of Markdown Content (WIP)</h5>
|
||||
<label htmlFor={'toc'}>
|
||||
<input name={'toc'} type={'checkbox'} disabled />
|
||||
enable
|
||||
</label>
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section issue'}>
|
||||
<h4>Issue</h4>
|
||||
<a href="https://github.com/EnixCoda/Gitako/issues" target="_blank">
|
||||
Draft a issue on Github.
|
||||
</a>
|
||||
<br />
|
||||
<span>Report BUG or request feature.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
10
src/content.js
Normal file
10
src/content.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import preact from 'preact'
|
||||
/** @jsx preact.h */
|
||||
|
||||
import './content.less'
|
||||
|
||||
import GitakoSideBar from './components/GitakoSideBar'
|
||||
|
||||
const GitakoSideBarElement = document.createElement('div')
|
||||
document.body.appendChild(GitakoSideBarElement)
|
||||
preact.render(<GitakoSideBar />, GitakoSideBarElement)
|
||||
258
src/content.less
Normal file
258
src/content.less
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
@import './octicons.css';
|
||||
@side-bar-width: 260px;
|
||||
@github-content-width: 1280px;
|
||||
@content-width: (@side-bar-width + @github-content-width);
|
||||
|
||||
.with-gitako-spacing {
|
||||
margin-left: @side-bar-width;
|
||||
@media (min-width: @github-content-width) and (max-width: @content-width) {
|
||||
margin-left: ~'calc(' @content-width ~' - 100vw)';
|
||||
}
|
||||
@media (min-width: @content-width) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
position: relative;
|
||||
.clippy {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
&:hover {
|
||||
background: #fff;
|
||||
}
|
||||
&:active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&.success {
|
||||
i::before {
|
||||
content: '\F03A'; // .octicon-check
|
||||
}
|
||||
}
|
||||
|
||||
&.fail {
|
||||
i::before {
|
||||
content: '\F081'; // .octicon-x
|
||||
}
|
||||
}
|
||||
|
||||
i::before {
|
||||
margin-left: 2px; // make icon centered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.gitako {
|
||||
.gitako-side-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: @side-bar-width;
|
||||
height: 100vh;
|
||||
z-index: 2;
|
||||
background: #fafbfc;
|
||||
border-right: 1px solid #e1e4e8;
|
||||
|
||||
@media screen and (min-width: @content-width) {
|
||||
border-left: 1px solid #e1e4e8;
|
||||
left: ~'calc((100vw - ' @content-width ~') / 2)';
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
.hidden;
|
||||
}
|
||||
|
||||
.gitako-logo-wrapper.Header {
|
||||
position: relative;
|
||||
|
||||
.gitako-logo {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&.breath {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#ff2400,
|
||||
#e81d1d,
|
||||
#e8b71d,
|
||||
#e3e81d,
|
||||
#1de840,
|
||||
#1ddde8,
|
||||
#2b1de8,
|
||||
#dd00f3,
|
||||
#dd00f3
|
||||
);
|
||||
background-size: 450% 450%;
|
||||
animation: rainbow 9s ease infinite;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
|
||||
// https://stackoverflow.com/questions/10814178/css-performance-relative-to-translatez0
|
||||
-webkit-transform: rotateZ(360deg);
|
||||
transform: rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.octicon {
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.octicon-color {
|
||||
color: rgba(3, 47, 98, 0.55);
|
||||
}
|
||||
|
||||
.gitako-side-bar-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.meta-bar {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #586069;
|
||||
background-color: #f1f8ff;
|
||||
border-bottom: 1px solid #c8e1ff;
|
||||
|
||||
a {
|
||||
// fix a weird bug:
|
||||
// when gitako failed loading repo, cursor hovering <a> in meta bar will be 'text'
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-name {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
/* search input */
|
||||
.search-input-wrapper {
|
||||
input[type='text'].form-control {
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 0px 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.file-explorer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
|
||||
.node-item-row {
|
||||
background: #fff;
|
||||
&:hover {
|
||||
background: #f6f8fa;
|
||||
}
|
||||
&.focused {
|
||||
background: #f0f0f6;
|
||||
}
|
||||
|
||||
.node-item {
|
||||
word-wrap: normal;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
color: #0366d6;
|
||||
line-height: 20px;
|
||||
padding: 6px 0;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid #eaecef;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
// folder icon rotate when expand
|
||||
&.expanded .octicon.octicon-triangle-right {
|
||||
transform: rotate(90deg) translate3d(25%, 0, 0);
|
||||
}
|
||||
|
||||
.node-item-name {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.node-item:hover .node-item-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gitako-settings-bar {
|
||||
border-top: 1px solid #eaecef;
|
||||
&-content {
|
||||
padding: 0 10px;
|
||||
&-section {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.access-token {
|
||||
border-bottom: none; // prevent overwrite by github style
|
||||
}
|
||||
.access-token-input {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.placeholder-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
|
||||
.settings-icon {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% {
|
||||
background-position: 9% 91%;
|
||||
}
|
||||
50% {
|
||||
background-position: 91% 9%;
|
||||
}
|
||||
100% {
|
||||
background-position: 9% 91%;
|
||||
}
|
||||
}
|
||||
15
src/manifest.json
Normal file
15
src/manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Gitako",
|
||||
"version": "0.1.0",
|
||||
"author": "EnixCoda",
|
||||
"description": "yet another GitHub extension.",
|
||||
"homepage_url": "https://github.com/EnixCoda/Gitako",
|
||||
"permissions": ["tabs", "storage", "*://github.com/*"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://github.com/*"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
537
src/octicons.css
vendored
Executable file
537
src/octicons.css
vendored
Executable file
|
|
@ -0,0 +1,537 @@
|
|||
@font-face {
|
||||
font-family: 'Octicons';
|
||||
src: url('assets/font/octicons.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
.octicon is optimized for 16px.
|
||||
.mega-octicon is optimized for 32px but can be used larger.
|
||||
|
||||
*/
|
||||
.octicon,
|
||||
.mega-octicon {
|
||||
font: normal normal normal 16px/1 Octicons;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
fill: currentColor;
|
||||
}
|
||||
.mega-octicon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.octicon-alert:before {
|
||||
content: '\f02d';
|
||||
}
|
||||
.octicon-arrow-down:before {
|
||||
content: '\f03f';
|
||||
}
|
||||
.octicon-arrow-left:before {
|
||||
content: '\f040';
|
||||
}
|
||||
.octicon-arrow-right:before {
|
||||
content: '\f03e';
|
||||
}
|
||||
.octicon-arrow-small-down:before {
|
||||
content: '\f0a0';
|
||||
}
|
||||
.octicon-arrow-small-left:before {
|
||||
content: '\f0a1';
|
||||
}
|
||||
.octicon-arrow-small-right:before {
|
||||
content: '\f071';
|
||||
}
|
||||
.octicon-arrow-small-up:before {
|
||||
content: '\f09f';
|
||||
}
|
||||
.octicon-arrow-up:before {
|
||||
content: '\f03d';
|
||||
}
|
||||
.octicon-beaker:before {
|
||||
content: '\f0dd';
|
||||
}
|
||||
.octicon-bell:before {
|
||||
content: '\f0de';
|
||||
}
|
||||
.octicon-bold:before {
|
||||
content: '\f0e2';
|
||||
}
|
||||
.octicon-book:before {
|
||||
content: '\f007';
|
||||
}
|
||||
.octicon-bookmark:before {
|
||||
content: '\f07b';
|
||||
}
|
||||
.octicon-briefcase:before {
|
||||
content: '\f0d3';
|
||||
}
|
||||
.octicon-broadcast:before {
|
||||
content: '\f048';
|
||||
}
|
||||
.octicon-browser:before {
|
||||
content: '\f0c5';
|
||||
}
|
||||
.octicon-bug:before {
|
||||
content: '\f091';
|
||||
}
|
||||
.octicon-calendar:before {
|
||||
content: '\f068';
|
||||
}
|
||||
.octicon-check:before {
|
||||
content: '\f03a';
|
||||
}
|
||||
.octicon-checklist:before {
|
||||
content: '\f076';
|
||||
}
|
||||
.octicon-chevron-down:before {
|
||||
content: '\f0a3';
|
||||
}
|
||||
.octicon-chevron-left:before {
|
||||
content: '\f0a4';
|
||||
}
|
||||
.octicon-chevron-right:before {
|
||||
content: '\f078';
|
||||
}
|
||||
.octicon-chevron-up:before {
|
||||
content: '\f0a2';
|
||||
}
|
||||
.octicon-circle-slash:before {
|
||||
content: '\f084';
|
||||
}
|
||||
.octicon-circuit-board:before {
|
||||
content: '\f0d6';
|
||||
}
|
||||
.octicon-clippy:before {
|
||||
content: '\f035';
|
||||
}
|
||||
.octicon-clock:before {
|
||||
content: '\f046';
|
||||
}
|
||||
.octicon-cloud-download:before {
|
||||
content: '\f00b';
|
||||
}
|
||||
.octicon-cloud-upload:before {
|
||||
content: '\f00c';
|
||||
}
|
||||
.octicon-code:before {
|
||||
content: '\f05f';
|
||||
}
|
||||
.octicon-comment-discussion:before {
|
||||
content: '\f04f';
|
||||
}
|
||||
.octicon-comment:before {
|
||||
content: '\f02b';
|
||||
}
|
||||
.octicon-credit-card:before {
|
||||
content: '\f045';
|
||||
}
|
||||
.octicon-dash:before {
|
||||
content: '\f0ca';
|
||||
}
|
||||
.octicon-dashboard:before {
|
||||
content: '\f07d';
|
||||
}
|
||||
.octicon-database:before {
|
||||
content: '\f096';
|
||||
}
|
||||
.octicon-desktop-download:before {
|
||||
content: '\f0dc';
|
||||
}
|
||||
.octicon-device-camera-video:before {
|
||||
content: '\f057';
|
||||
}
|
||||
.octicon-device-camera:before {
|
||||
content: '\f056';
|
||||
}
|
||||
.octicon-device-desktop:before {
|
||||
content: '\f27c';
|
||||
}
|
||||
.octicon-device-mobile:before {
|
||||
content: '\f038';
|
||||
}
|
||||
.octicon-diff-added:before {
|
||||
content: '\f06b';
|
||||
}
|
||||
.octicon-diff-ignored:before {
|
||||
content: '\f099';
|
||||
}
|
||||
.octicon-diff-modified:before {
|
||||
content: '\f06d';
|
||||
}
|
||||
.octicon-diff-removed:before {
|
||||
content: '\f06c';
|
||||
}
|
||||
.octicon-diff-renamed:before {
|
||||
content: '\f06e';
|
||||
}
|
||||
.octicon-diff:before {
|
||||
content: '\f04d';
|
||||
}
|
||||
.octicon-ellipses:before {
|
||||
content: '\f101';
|
||||
}
|
||||
.octicon-ellipsis:before {
|
||||
content: '\f09a';
|
||||
}
|
||||
.octicon-eye:before {
|
||||
content: '\f04e';
|
||||
}
|
||||
.octicon-file-binary:before {
|
||||
content: '\f094';
|
||||
}
|
||||
.octicon-file-code:before {
|
||||
content: '\f010';
|
||||
}
|
||||
.octicon-file-directory:before {
|
||||
content: '\f016';
|
||||
}
|
||||
.octicon-file-media:before {
|
||||
content: '\f012';
|
||||
}
|
||||
.octicon-file-pdf:before {
|
||||
content: '\f014';
|
||||
}
|
||||
.octicon-file-submodule:before {
|
||||
content: '\f017';
|
||||
}
|
||||
.octicon-file-symlink-directory:before {
|
||||
content: '\f0b1';
|
||||
}
|
||||
.octicon-file-symlink-file:before {
|
||||
content: '\f0b0';
|
||||
}
|
||||
.octicon-file-text:before {
|
||||
content: '\f011';
|
||||
}
|
||||
.octicon-file-zip:before {
|
||||
content: '\f013';
|
||||
}
|
||||
.octicon-file:before {
|
||||
content: '\f102';
|
||||
}
|
||||
.octicon-flame:before {
|
||||
content: '\f0d2';
|
||||
}
|
||||
.octicon-fold:before {
|
||||
content: '\f0cc';
|
||||
}
|
||||
.octicon-gear:before {
|
||||
content: '\f02f';
|
||||
}
|
||||
.octicon-gift:before {
|
||||
content: '\f042';
|
||||
}
|
||||
.octicon-gist-secret:before {
|
||||
content: '\f08c';
|
||||
}
|
||||
.octicon-gist:before {
|
||||
content: '\f00e';
|
||||
}
|
||||
.octicon-git-branch:before {
|
||||
content: '\f020';
|
||||
}
|
||||
.octicon-git-commit:before {
|
||||
content: '\f01f';
|
||||
}
|
||||
.octicon-git-compare:before {
|
||||
content: '\f0ac';
|
||||
}
|
||||
.octicon-git-merge:before {
|
||||
content: '\f023';
|
||||
}
|
||||
.octicon-git-pull-request:before {
|
||||
content: '\f009';
|
||||
}
|
||||
.octicon-globe:before {
|
||||
content: '\f0b6';
|
||||
}
|
||||
.octicon-graph:before {
|
||||
content: '\f043';
|
||||
}
|
||||
.octicon-heart:before {
|
||||
content: '\2665';
|
||||
}
|
||||
.octicon-history:before {
|
||||
content: '\f07e';
|
||||
}
|
||||
.octicon-home:before {
|
||||
content: '\f08d';
|
||||
}
|
||||
.octicon-horizontal-rule:before {
|
||||
content: '\f070';
|
||||
}
|
||||
.octicon-hubot:before {
|
||||
content: '\f09d';
|
||||
}
|
||||
.octicon-inbox:before {
|
||||
content: '\f0cf';
|
||||
}
|
||||
.octicon-info:before {
|
||||
content: '\f059';
|
||||
}
|
||||
.octicon-issue-closed:before {
|
||||
content: '\f028';
|
||||
}
|
||||
.octicon-issue-opened:before {
|
||||
content: '\f026';
|
||||
}
|
||||
.octicon-issue-reopened:before {
|
||||
content: '\f027';
|
||||
}
|
||||
.octicon-italic:before {
|
||||
content: '\f0e4';
|
||||
}
|
||||
.octicon-jersey:before {
|
||||
content: '\f019';
|
||||
}
|
||||
.octicon-key:before {
|
||||
content: '\f049';
|
||||
}
|
||||
.octicon-keyboard:before {
|
||||
content: '\f00d';
|
||||
}
|
||||
.octicon-law:before {
|
||||
content: '\f0d8';
|
||||
}
|
||||
.octicon-light-bulb:before {
|
||||
content: '\f000';
|
||||
}
|
||||
.octicon-link-external:before {
|
||||
content: '\f07f';
|
||||
}
|
||||
.octicon-link:before {
|
||||
content: '\f05c';
|
||||
}
|
||||
.octicon-list-ordered:before {
|
||||
content: '\f062';
|
||||
}
|
||||
.octicon-list-unordered:before {
|
||||
content: '\f061';
|
||||
}
|
||||
.octicon-location:before {
|
||||
content: '\f060';
|
||||
}
|
||||
.octicon-lock:before {
|
||||
content: '\f06a';
|
||||
}
|
||||
.octicon-logo-gist:before {
|
||||
content: '\f0ad';
|
||||
}
|
||||
.octicon-logo-github:before {
|
||||
content: '\f092';
|
||||
}
|
||||
.octicon-mail-read:before {
|
||||
content: '\f03c';
|
||||
}
|
||||
.octicon-mail-reply:before {
|
||||
content: '\f051';
|
||||
}
|
||||
.octicon-mail:before {
|
||||
content: '\f03b';
|
||||
}
|
||||
.octicon-mark-github:before {
|
||||
content: '\f00a';
|
||||
}
|
||||
.octicon-markdown:before {
|
||||
content: '\f0c9';
|
||||
}
|
||||
.octicon-megaphone:before {
|
||||
content: '\f077';
|
||||
}
|
||||
.octicon-mention:before {
|
||||
content: '\f0be';
|
||||
}
|
||||
.octicon-milestone:before {
|
||||
content: '\f075';
|
||||
}
|
||||
.octicon-mirror:before {
|
||||
content: '\f024';
|
||||
}
|
||||
.octicon-mortar-board:before {
|
||||
content: '\f0d7';
|
||||
}
|
||||
.octicon-mute:before {
|
||||
content: '\f080';
|
||||
}
|
||||
.octicon-no-newline:before {
|
||||
content: '\f09c';
|
||||
}
|
||||
.octicon-octoface:before {
|
||||
content: '\f008';
|
||||
}
|
||||
.octicon-organization:before {
|
||||
content: '\f037';
|
||||
}
|
||||
.octicon-package:before {
|
||||
content: '\f0c4';
|
||||
}
|
||||
.octicon-paintcan:before {
|
||||
content: '\f0d1';
|
||||
}
|
||||
.octicon-pencil:before {
|
||||
content: '\f058';
|
||||
}
|
||||
.octicon-person:before {
|
||||
content: '\f018';
|
||||
}
|
||||
.octicon-pin:before {
|
||||
content: '\f041';
|
||||
}
|
||||
.octicon-plug:before {
|
||||
content: '\f0d4';
|
||||
}
|
||||
.octicon-plus:before {
|
||||
content: '\f05d';
|
||||
}
|
||||
.octicon-primitive-dot:before {
|
||||
content: '\f052';
|
||||
}
|
||||
.octicon-primitive-square:before {
|
||||
content: '\f053';
|
||||
}
|
||||
.octicon-pulse:before {
|
||||
content: '\f085';
|
||||
}
|
||||
.octicon-question:before {
|
||||
content: '\f02c';
|
||||
}
|
||||
.octicon-quote:before {
|
||||
content: '\f063';
|
||||
}
|
||||
.octicon-radio-tower:before {
|
||||
content: '\f030';
|
||||
}
|
||||
.octicon-repo-clone:before {
|
||||
content: '\f04c';
|
||||
}
|
||||
.octicon-repo-force-push:before {
|
||||
content: '\f04a';
|
||||
}
|
||||
.octicon-repo-forked:before {
|
||||
content: '\f002';
|
||||
}
|
||||
.octicon-repo-pull:before {
|
||||
content: '\f006';
|
||||
}
|
||||
.octicon-repo-push:before {
|
||||
content: '\f005';
|
||||
}
|
||||
.octicon-repo:before {
|
||||
content: '\f001';
|
||||
}
|
||||
.octicon-rocket:before {
|
||||
content: '\f033';
|
||||
}
|
||||
.octicon-rss:before {
|
||||
content: '\f034';
|
||||
}
|
||||
.octicon-ruby:before {
|
||||
content: '\f047';
|
||||
}
|
||||
.octicon-search:before {
|
||||
content: '\f02e';
|
||||
}
|
||||
.octicon-server:before {
|
||||
content: '\f097';
|
||||
}
|
||||
.octicon-settings:before {
|
||||
content: '\f07c';
|
||||
}
|
||||
.octicon-shield:before {
|
||||
content: '\f0e1';
|
||||
}
|
||||
.octicon-sign-in:before {
|
||||
content: '\f036';
|
||||
}
|
||||
.octicon-sign-out:before {
|
||||
content: '\f032';
|
||||
}
|
||||
.octicon-smiley:before {
|
||||
content: '\f0e7';
|
||||
}
|
||||
.octicon-squirrel:before {
|
||||
content: '\f0b2';
|
||||
}
|
||||
.octicon-star:before {
|
||||
content: '\f02a';
|
||||
}
|
||||
.octicon-stop:before {
|
||||
content: '\f08f';
|
||||
}
|
||||
.octicon-sync:before {
|
||||
content: '\f087';
|
||||
}
|
||||
.octicon-tag:before {
|
||||
content: '\f015';
|
||||
}
|
||||
.octicon-tasklist:before {
|
||||
content: '\f0e5';
|
||||
}
|
||||
.octicon-telescope:before {
|
||||
content: '\f088';
|
||||
}
|
||||
.octicon-terminal:before {
|
||||
content: '\f0c8';
|
||||
}
|
||||
.octicon-text-size:before {
|
||||
content: '\f0e3';
|
||||
}
|
||||
.octicon-three-bars:before {
|
||||
content: '\f05e';
|
||||
}
|
||||
.octicon-thumbsdown:before {
|
||||
content: '\f0db';
|
||||
}
|
||||
.octicon-thumbsup:before {
|
||||
content: '\f0da';
|
||||
}
|
||||
.octicon-tools:before {
|
||||
content: '\f031';
|
||||
}
|
||||
.octicon-trashcan:before {
|
||||
content: '\f0d0';
|
||||
}
|
||||
.octicon-triangle-down:before {
|
||||
content: '\f05b';
|
||||
}
|
||||
.octicon-triangle-left:before {
|
||||
content: '\f044';
|
||||
}
|
||||
.octicon-triangle-right:before {
|
||||
content: '\f05a';
|
||||
}
|
||||
.octicon-triangle-up:before {
|
||||
content: '\f0aa';
|
||||
}
|
||||
.octicon-unfold:before {
|
||||
content: '\f039';
|
||||
}
|
||||
.octicon-unmute:before {
|
||||
content: '\f0ba';
|
||||
}
|
||||
.octicon-unverified:before {
|
||||
content: '\f0e8';
|
||||
}
|
||||
.octicon-verified:before {
|
||||
content: '\f0e6';
|
||||
}
|
||||
.octicon-versions:before {
|
||||
content: '\f064';
|
||||
}
|
||||
.octicon-watch:before {
|
||||
content: '\f0e0';
|
||||
}
|
||||
.octicon-x:before {
|
||||
content: '\f081';
|
||||
}
|
||||
.octicon-zap:before {
|
||||
content: '\26a1';
|
||||
}
|
||||
290
src/utils/DOMHelper.js
Normal file
290
src/utils/DOMHelper.js
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
/**
|
||||
* this helper helps manipulating DOM
|
||||
*/
|
||||
|
||||
import pjax from 'pjax'
|
||||
|
||||
/**
|
||||
* if should show gitako, then move body right to make space for showing gitako
|
||||
* otherwise, hide the space
|
||||
*/
|
||||
function setBodyIndent(shouldShowGitako) {
|
||||
const spacingClassName = 'with-gitako-spacing'
|
||||
if (shouldShowGitako) {
|
||||
document.body.classList.add(spacingClassName)
|
||||
} else {
|
||||
document.body.classList.remove(spacingClassName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* content above the file navigation bar is same for all pages of the repo
|
||||
* use this function to scroll down a bit to hide them
|
||||
*/
|
||||
function scrollToRepoContent() {
|
||||
const fileNavigationSelector = '.file-navigation.js-zeroclipboard-container'
|
||||
const fileNavigationElement = document.querySelector(fileNavigationSelector)
|
||||
// cannot to use behavior: smooth here as it will scroll horizontally
|
||||
if (fileNavigationElement) {
|
||||
fileNavigationElement.scrollIntoView()
|
||||
} else {
|
||||
document.body.scrollIntoView()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* scroll to index-th element in the list
|
||||
* @param {number} index index of node item in the list
|
||||
*/
|
||||
function scrollToNodeElement(index) {
|
||||
const nodeElementSelector = '.node-item'
|
||||
const nodeElements = document.querySelectorAll(nodeElementSelector)
|
||||
nodeElements[index].scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* add pjax listeners
|
||||
* call this when pjax redirected or page loaded
|
||||
*/
|
||||
function attachPJAX(fields) {
|
||||
// TODO: switch for fields
|
||||
const elements = [
|
||||
'.gitako a.pjax-link', // links in Gitako file tree & list
|
||||
'.js-path-segment a', // links in the file navigation bar
|
||||
].join()
|
||||
new pjax({
|
||||
elements,
|
||||
selectors: ['.repository-content'],
|
||||
scrollTo: false,
|
||||
analytics: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* there are few types of pages on GitHub, mainly
|
||||
* 1. raw text: code
|
||||
* 2. rendered content: like Markdown
|
||||
* 3. preview: like image
|
||||
*/
|
||||
const PAGE_TYPES = {
|
||||
RAW_TEXT: 'raw_text',
|
||||
RENDERED: 'rendered',
|
||||
// PREVIEW: 'preview',
|
||||
OTHERS: 'others',
|
||||
}
|
||||
|
||||
/**
|
||||
* this function tries to tell which type current page is of
|
||||
*
|
||||
* note: not determining through file extension here
|
||||
* becasuse there might be files using wrong extension name
|
||||
*
|
||||
* TODO: distinguish type 'preview'
|
||||
*/
|
||||
function getCurrentPageType() {
|
||||
const blobWrapperSelector = '.repository-content .file .blob-wrapper'
|
||||
const blobWrapperElement = document.querySelector(blobWrapperSelector)
|
||||
if (blobWrapperElement) {
|
||||
if (blobWrapperElement.querySelector('table')) {
|
||||
return PAGE_TYPES.RAW_TEXT
|
||||
}
|
||||
} else {
|
||||
const readmeSelector = '.repository-content .readme'
|
||||
const readmeElement = document.querySelector(readmeSelector)
|
||||
if (readmeElement) {
|
||||
return PAGE_TYPES.RENDERED
|
||||
}
|
||||
}
|
||||
return PAGE_TYPES.OTHERS
|
||||
}
|
||||
|
||||
export const REPO_TYPE_PRIVATE = 'private'
|
||||
export const REPO_TYPE_PUBLIC = 'public'
|
||||
function getRepoPageType() {
|
||||
const headerSelector = `#js-repo-pjax-container .pagehead.repohead h1`
|
||||
const header = document.querySelector(headerSelector)
|
||||
if (header) {
|
||||
const repoPageTypes = [REPO_TYPE_PRIVATE, REPO_TYPE_PUBLIC]
|
||||
for (const repoPageType of repoPageTypes) {
|
||||
if (header.classList.contains(repoPageType)) {
|
||||
return repoPageType
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* add copy file content buttons to button groups
|
||||
* click these buttons will copy file content to clipboard
|
||||
*/
|
||||
function attachCopyFileBtn() {
|
||||
/**
|
||||
* get text content of raw text content
|
||||
*/
|
||||
function getCodeElement() {
|
||||
if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) {
|
||||
const codeContentSelector = '.repository-content .file .data table'
|
||||
return document.querySelector(codeContentSelector)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change inner text of copy file button to give feedback
|
||||
* @param {element} copyFileBtn
|
||||
* @param {string} text
|
||||
*/
|
||||
function setTempCopyFileBtnText(copyFileBtn, text) {
|
||||
copyFileBtn.innerText = text
|
||||
window.setTimeout(() => (copyFileBtn.innerText = 'Copy file'), 1000)
|
||||
}
|
||||
|
||||
if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) {
|
||||
const btnGroupSelector = [
|
||||
// the button group next to navigation bar
|
||||
'.repository-content .file-navigation.js-zeroclipboard-container .BtnGroup',
|
||||
// the button group in file content header
|
||||
'.repository-content .file .file-header .file-actions .BtnGroup',
|
||||
].join(', ')
|
||||
const btnGroups = document.querySelectorAll(btnGroupSelector)
|
||||
|
||||
btnGroups.forEach(btnGroup => {
|
||||
const copyFileBtn = document.createElement('button')
|
||||
copyFileBtn.classList.add('btn', 'btn-sm', 'BtnGroup-item', 'copy-file-btn')
|
||||
copyFileBtn.innerText = 'Copy file'
|
||||
copyFileBtn.addEventListener('click', () => {
|
||||
const codeElement = getCodeElement()
|
||||
if (copyElementContent(codeElement)) {
|
||||
setTempCopyFileBtnText(copyFileBtn, 'Success!')
|
||||
} else {
|
||||
setTempCopyFileBtnText(copyFileBtn, 'Copy failed!')
|
||||
}
|
||||
})
|
||||
btnGroup.insertBefore(copyFileBtn, btnGroups.lastChild)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* copy content of a DOM element to clipboard
|
||||
* @param {element} element
|
||||
* @returns {boolean} whether copy is successful
|
||||
*/
|
||||
function copyElementContent(element) {
|
||||
window.getSelection().removeAllRanges()
|
||||
const range = document.createRange()
|
||||
range.selectNode(element)
|
||||
window.getSelection().addRange(range)
|
||||
const isCopySuccessful = document.execCommand('copy')
|
||||
window.getSelection().removeAllRanges()
|
||||
return isCopySuccessful
|
||||
}
|
||||
|
||||
/**
|
||||
* create a copy file content button `clippy`
|
||||
* once mouse enters a code snippet of markdown, move clippy into it
|
||||
* user can copy the snippet's content by click it
|
||||
*
|
||||
* TODO: 'reactify' it
|
||||
*/
|
||||
function createClippy() {
|
||||
function setTempClippyIconFeedback(clippy, type) {
|
||||
const tempIconClassName = type === 'success' ? 'success' : 'fail'
|
||||
clippy.classList.add(tempIconClassName)
|
||||
window.setTimeout(() => {
|
||||
clippy.classList.remove(tempIconClassName)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* <button class="clippy">
|
||||
* <i class="octicon octicon-clippy" />
|
||||
* </button>
|
||||
*/
|
||||
const clippy = document.createElement('button')
|
||||
clippy.classList.add('clippy')
|
||||
const clippyIcon = document.createElement('i')
|
||||
clippyIcon.classList.add('octicon', 'octicon-clippy')
|
||||
clippy.appendChild(clippyIcon)
|
||||
|
||||
// set clipboard with current code snippet element's content
|
||||
clippy.addEventListener('click', function onClippyClick() {
|
||||
if (copyElementContent(currentCodeSnippetElement)) {
|
||||
setTempClippyIconFeedback(clippy, 'success')
|
||||
} else {
|
||||
setTempClippyIconFeedback(clippy, 'fail')
|
||||
}
|
||||
})
|
||||
|
||||
return clippy
|
||||
}
|
||||
|
||||
const clippy = createClippy()
|
||||
|
||||
let currentCodeSnippetElement
|
||||
function attachCopySnippet() {
|
||||
const readmeSelector = '.repository-content .readme'
|
||||
const readmeElement = document.querySelector(readmeSelector)
|
||||
if (readmeElement) {
|
||||
const snippetSelector = '.repository-content .readme pre'
|
||||
const snippetElements = readmeElement.querySelectorAll(snippetSelector)
|
||||
readmeElement.addEventListener('mouseover', ({ target }) => {
|
||||
// only move clippy when mouse is over a new snippet
|
||||
if (
|
||||
Array.from(snippetElements).indexOf(target) !== -1 &&
|
||||
currentCodeSnippetElement !== target
|
||||
) {
|
||||
currentCodeSnippetElement = target
|
||||
currentCodeSnippetElement.insertAdjacentElement('afterbegin', clippy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* focus to side bar, user will be able to manipulate it with keyboard
|
||||
*/
|
||||
function focusFileExplorer() {
|
||||
const sideBarContentSelector = '.gitako .file-explorer'
|
||||
const sideBarElement = document.querySelector(sideBarContentSelector)
|
||||
if (sideBarElement) {
|
||||
sideBarElement.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function focusSearchInput() {
|
||||
const searchInputSelector = '.search-input'
|
||||
const searchInputElement = document.querySelector(searchInputSelector)
|
||||
if (searchInputElement) {
|
||||
if (document.activeElement !== searchInputElement) {
|
||||
searchInputElement.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate click on node item, for triggering pjax
|
||||
* @param {number} index
|
||||
*/
|
||||
function clickOnNodeElement(index = 0) {
|
||||
const nodeElementSelector = '.node-item'
|
||||
const nodeElements = document.querySelectorAll(nodeElementSelector)
|
||||
nodeElements[index].click()
|
||||
}
|
||||
|
||||
export default {
|
||||
attachPJAX,
|
||||
attachCopyFileBtn,
|
||||
attachCopySnippet,
|
||||
clickOnNodeElement,
|
||||
focusSearchInput,
|
||||
focusFileExplorer,
|
||||
getCurrentPageType,
|
||||
getRepoPageType,
|
||||
setBodyIndent,
|
||||
scrollToNodeElement,
|
||||
scrollToRepoContent,
|
||||
}
|
||||
33
src/utils/GitHubHelper.js
Normal file
33
src/utils/GitHubHelper.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export const NOT_FOUND = 'Repo Not Found'
|
||||
|
||||
async function getRepoMeta({ userName, repoName, accessToken }) {
|
||||
const headers = {}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `token ${accessToken}`
|
||||
}
|
||||
const res = await fetch(`https://api.github.com/repos/${userName}/${repoName}`, { headers })
|
||||
if (res.status === 200) return res.json()
|
||||
// for private repo, GitHub api also responses with 404 when unauthorized
|
||||
throw new Error(NOT_FOUND)
|
||||
}
|
||||
|
||||
async function getTreeData({ userName, repoName, branchName, accessToken }) {
|
||||
const headers = {}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `token ${accessToken}`
|
||||
}
|
||||
return (await fetch(
|
||||
`https://api.github.com/repos/${userName}/${repoName}/git/trees/${branchName}?recursive=1`,
|
||||
{ headers }
|
||||
)).json()
|
||||
}
|
||||
|
||||
function getUrlForRedirect({ userName, repoName, branchName }, path) {
|
||||
return `https://github.com/${userName}/${repoName}/tree/${branchName}/${path}`
|
||||
}
|
||||
|
||||
export default {
|
||||
getRepoMeta,
|
||||
getTreeData,
|
||||
getUrlForRedirect,
|
||||
}
|
||||
12
src/utils/cx.js
Normal file
12
src/utils/cx.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* cx('class1', { class2: true, class3: false }) --> 'class1 class2'
|
||||
* @param {string} baseClassNames
|
||||
* @param {object} optionalClassNames
|
||||
*/
|
||||
export default function cx(baseClassNames = '', optionalClassNames = {}) {
|
||||
return Object.entries(optionalClassNames)
|
||||
.map(([key, value]) => value ? key : '')
|
||||
.filter(_ => _)
|
||||
.concat([baseClassNames])
|
||||
.join(' ')
|
||||
}
|
||||
25
src/utils/storageHelper.js
Normal file
25
src/utils/storageHelper.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const localStorage = chrome.storage.local
|
||||
const ACCESS_TOKEN_KEY = 'access_token'
|
||||
|
||||
function get(key) {
|
||||
return new Promise(resolve => localStorage.get(key, items => resolve(items[key])))
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
return new Promise(resolve => localStorage.set({ [key]: value }, resolve))
|
||||
}
|
||||
|
||||
function getAccessToken() {
|
||||
return get(ACCESS_TOKEN_KEY)
|
||||
}
|
||||
|
||||
function setAccessToken(accessToken) {
|
||||
return set(ACCESS_TOKEN_KEY, accessToken)
|
||||
}
|
||||
|
||||
export default {
|
||||
get,
|
||||
set,
|
||||
getAccessToken,
|
||||
setAccessToken,
|
||||
}
|
||||
72
src/utils/treeParser.js
Normal file
72
src/utils/treeParser.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import GitHubHelper from './GitHubHelper'
|
||||
|
||||
const nodeTemplate = {
|
||||
name: null,
|
||||
path: null,
|
||||
mode: null,
|
||||
type: null,
|
||||
sha: null,
|
||||
url: null,
|
||||
}
|
||||
|
||||
function sortFoldersToFront(root) {
|
||||
const isFolder = node => node.type === 'tree'
|
||||
const isNotFolder = (...args) => !isFolder(...args)
|
||||
function DFS(root) {
|
||||
const nodes = root.contents
|
||||
if (nodes) {
|
||||
nodes.splice(0, Infinity, ...nodes.filter(isFolder), ...nodes.filter(isNotFolder))
|
||||
nodes.forEach(DFS)
|
||||
}
|
||||
return root
|
||||
}
|
||||
return DFS(root)
|
||||
}
|
||||
|
||||
function parse(treeData, metaData) {
|
||||
const { tree } = treeData
|
||||
|
||||
// nodes are created from items and put onto tree
|
||||
const pathToNode = new Map()
|
||||
const pathToItem = new Map()
|
||||
|
||||
const root = { ...nodeTemplate, name: '', path: '', contents: [] }
|
||||
pathToNode.set('', root)
|
||||
|
||||
tree.forEach(item => pathToItem.set(item.path, item))
|
||||
tree.forEach(item => {
|
||||
// bottom-up search for the deepest node created
|
||||
let path = item.path
|
||||
const itemsToCreateTreeNode = []
|
||||
while (path !== '' && !pathToNode.has(path)) {
|
||||
itemsToCreateTreeNode.push(pathToItem.get(path))
|
||||
// 'a/b' -> 'a'
|
||||
// 'a' -> ''
|
||||
path = path.substring(0, path.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
// top-down create nodes
|
||||
while (itemsToCreateTreeNode.length) {
|
||||
const item = itemsToCreateTreeNode.pop()
|
||||
const node = {
|
||||
...nodeTemplate,
|
||||
...item,
|
||||
name: item.path.replace(/^.*\//, ''),
|
||||
url: GitHubHelper.getUrlForRedirect(metaData, item.path),
|
||||
contents: item.type === 'tree' ? [] : null,
|
||||
}
|
||||
pathToNode.get(path).contents.push(node)
|
||||
pathToNode.set(node.path, node)
|
||||
path = node.path
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
root: sortFoldersToFront(root),
|
||||
nodes: Array.from(pathToNode.values()),
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
parse,
|
||||
}
|
||||
39
src/utils/urlHelper.js
Normal file
39
src/utils/urlHelper.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
function parse() {
|
||||
const { pathname } = window.location
|
||||
const [
|
||||
,
|
||||
// ignore content before the first '/'
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
branchName,
|
||||
] = pathname.split('/')
|
||||
return {
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
branchName,
|
||||
}
|
||||
}
|
||||
|
||||
const RESERVED_NAME = ['blog']
|
||||
function isInCodePage() {
|
||||
const { userName, repoName, type, branchName } = parse()
|
||||
return !!(
|
||||
userName &&
|
||||
!RESERVED_NAME.find(_ => _ === userName) &&
|
||||
repoName &&
|
||||
(!type || type === 'tree' || type === 'blob') &&
|
||||
((type && branchName) || !(type || branchName))
|
||||
)
|
||||
}
|
||||
|
||||
function detectShouldShow(metaData) {
|
||||
return isInCodePage() && (!metaData || metaData.repoName)
|
||||
}
|
||||
|
||||
export default {
|
||||
detectShouldShow,
|
||||
isInCodePage,
|
||||
parse,
|
||||
}
|
||||
124
src/utils/visibleNodesGenerator.js
Normal file
124
src/utils/visibleNodesGenerator.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* This is the stack for generating an array of nodes for rendering
|
||||
*
|
||||
* when lower layer changes, higher layers would reset
|
||||
* when higher layer changes, lower layers would not notice
|
||||
*
|
||||
* render stack | when will change | on change callback
|
||||
*
|
||||
* ^ changes frequently
|
||||
* |
|
||||
* |4 focus | when hover/focus move | onFocusChange
|
||||
* | | | expandedNodes + focusNode -> visibleNodes
|
||||
* |3 expansion | when fold/unfold | onExpansionChange
|
||||
* | | | searchedNodes + toggleNode -> expandedNodes
|
||||
* |2 search key | when search | onSearch
|
||||
* | | | treeNodes + searchKey -> searchedNodes
|
||||
* |1 tree: { root <-> nodes } | when tree init | treeHelper.parse
|
||||
* | | tree data from api -> { root, nodes }
|
||||
* v stable
|
||||
*/
|
||||
|
||||
function getFilterFunc(keyRegex) {
|
||||
return function filterFunc({ name }) {
|
||||
return keyRegex.test(name)
|
||||
}
|
||||
}
|
||||
|
||||
function search(treeNodes, searchKey) {
|
||||
if (!searchKey) return
|
||||
/**
|
||||
* if searchKey is 'abcd'
|
||||
* then keyRegex will be /a.*?b.*?c.*?d/i
|
||||
*/
|
||||
const keyRegex = new RegExp(
|
||||
searchKey
|
||||
.replace(/\//, '')
|
||||
.split('')
|
||||
.join('.*?'),
|
||||
'i'
|
||||
)
|
||||
return treeNodes.filter(getFilterFunc(keyRegex))
|
||||
}
|
||||
|
||||
function debounce(func, delay) {
|
||||
let timer
|
||||
return (...args) => {
|
||||
return new Promise(resolve => {
|
||||
window.clearTimeout(timer)
|
||||
timer = window.setTimeout(() => resolve(func(...args)), delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const debouncedSearch = debounce(search, 250)
|
||||
|
||||
export default class VisibleNodesGenerator {
|
||||
// LEVEL 1
|
||||
root = null
|
||||
nodes = null
|
||||
plantTree(root, nodes) {
|
||||
this.root = root
|
||||
this.nodes = nodes
|
||||
|
||||
// a simplified sync 'search'
|
||||
this.searchedNodes = this.root.contents
|
||||
this.generateVisibleNodes()
|
||||
}
|
||||
|
||||
// LEVEL 2
|
||||
searchedNodes = null
|
||||
async search(searchKey) {
|
||||
this.searchedNodes = (await debouncedSearch(this.nodes, searchKey)) || this.root.contents
|
||||
this.expandedNodes.clear()
|
||||
this.generateVisibleNodes()
|
||||
}
|
||||
|
||||
// LEVEL 3
|
||||
expandedNodes = new Set()
|
||||
depths = new Map()
|
||||
toggleExpand(node) {
|
||||
this.setExpand(node, !this.expandedNodes.has(node))
|
||||
}
|
||||
|
||||
setExpand(node, expand) {
|
||||
if (expand && node.contents) {
|
||||
// only node with contents is expandable
|
||||
this.expandedNodes.add(node)
|
||||
} else {
|
||||
this.expandedNodes.delete(node)
|
||||
}
|
||||
this.generateVisibleNodes()
|
||||
}
|
||||
|
||||
visibleNodes = null
|
||||
generateVisibleNodes() {
|
||||
this.focusedNode = null
|
||||
this.depths.clear()
|
||||
const nodesSet = new Set() // prevent duplication
|
||||
const get = (nodes, depth = 0) => {
|
||||
return [].concat(
|
||||
...nodes.map(node => {
|
||||
if (nodesSet.has(node)) return []
|
||||
this.depths.set(node, depth)
|
||||
nodesSet.add(node)
|
||||
const children = this.expandedNodes.has(node) ? get(node.contents, depth + 1) : []
|
||||
return [node, ...children]
|
||||
})
|
||||
)
|
||||
}
|
||||
this.visibleNodes = {
|
||||
nodes: get(this.searchedNodes),
|
||||
depths: this.depths,
|
||||
expandedNodes: this.expandedNodes,
|
||||
}
|
||||
this.focusNode(null)
|
||||
}
|
||||
|
||||
// LEVEL 4
|
||||
focusedNode = null
|
||||
focusNode(node) {
|
||||
this.focusedNode = node
|
||||
this.visibleNodes.focusedNode = node
|
||||
}
|
||||
}
|
||||
67
webpack.config.js
Normal file
67
webpack.config.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const UglifyJSWebpackPlugin = require('uglifyjs-webpack-plugin')
|
||||
|
||||
const srcPath = path.resolve(__dirname, 'src')
|
||||
|
||||
const plugins = [
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json',
|
||||
},
|
||||
]),
|
||||
new webpack.SourceMapDevToolPlugin({}),
|
||||
]
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
plugins.push(
|
||||
new UglifyJSWebpackPlugin({
|
||||
cache: true,
|
||||
uglifyOptions: {
|
||||
ecma: 6,
|
||||
},
|
||||
})
|
||||
)
|
||||
plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
content: './src/content.js',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
},
|
||||
include: [srcPath],
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
loader: ['style-loader', 'css-loader', 'less-loader'],
|
||||
include: [srcPath],
|
||||
},
|
||||
{
|
||||
test: /\.(png|woff|woff2|eot|ttf|svg)(\?[a-z0-9=.]+)?$/,
|
||||
loader: 'url-loader?limit=100000',
|
||||
include: [srcPath],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins,
|
||||
}
|
||||
Loading…
Reference in a new issue