refactor: migrate utils to TS

This commit is contained in:
EnixCoda 2019-01-07 23:40:36 +08:00
parent 6f6740f658
commit 8954327aa9
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
14 changed files with 197 additions and 92 deletions

View file

@ -22,6 +22,8 @@
"react-dom": "^16.4.0"
},
"devDependencies": {
"@types/chrome": "^0.0.77",
"@types/nprogress": "^0.0.29",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"awesome-typescript-loader": "^5.2.1",

View file

@ -76,7 +76,7 @@ declare class Pjax {
* @param {string} href
* @param {Pjax.IOptions} options
*/
loadUrl(href: string, options?: Pjax.IOptions): void;
loadUrl(href: string, options?: Partial<Pjax.IOptions>): void;
/**
* Called after all switches complete (even async).
@ -179,6 +179,11 @@ declare namespace Pjax {
requestParams?: IRequestParams[];
formData?: FormData;
}
/**
* Cache accessed pages
*/
forceCache?: boolean
}
export type Switch = (oldEl: Element, newEl: Element, options?: IOptions, switchesOptions?: StringKeyedObject) => void;

View file

@ -2,8 +2,8 @@
* this helper helps manipulating DOM
*/
import PJAX from 'pjax'
import NProgress from 'nprogress'
import * as PJAX from 'pjax'
import * as NProgress from 'nprogress'
NProgress.configure({ showSpinner: false })
@ -11,7 +11,7 @@ NProgress.configure({ showSpinner: false })
* if should show gitako, then move body right to make space for showing gitako
* otherwise, hide the space
*/
function setBodyIndent(shouldShowGitako) {
function setBodyIndent(shouldShowGitako: boolean) {
const spacingClassName = 'with-gitako-spacing'
if (shouldShowGitako) {
document.body.classList.add(spacingClassName)
@ -20,7 +20,7 @@ function setBodyIndent(shouldShowGitako) {
}
}
function $(selector, existCallback, otherwise) {
function $(selector: string, existCallback?: (element: Element) => any, otherwise?: () => any) {
const element = document.querySelector(selector)
if (element) {
return existCallback ? existCallback(element) : element
@ -40,12 +40,19 @@ function getBranches() {
}
function getCurrentBranch() {
const selectedBranchButtonSelector = '.repository-content > .file-navigation > .branch-select-menu > button'
const branchNameFromButtonElement = $(selectedBranchButtonSelector, element => element.title.trim())
const selectedBranchButtonSelector =
'.repository-content > .file-navigation > .branch-select-menu > button'
const branchNameFromButtonElement = $(
selectedBranchButtonSelector,
(element: HTMLButtonElement) => element.title.trim()
)
if (branchNameFromButtonElement) return branchNameFromButtonElement
const selectedBranchSelector = '.select-menu.branch-select-menu .select-menu-modal .select-menu-list .select-menu-item.selected svg.select-menu-item-icon + span'
const branchNameFromSelectElement = $(selectedBranchSelector, element => element.textContent.trim())
const selectedBranchSelector =
'.select-menu.branch-select-menu .select-menu-modal .select-menu-list .select-menu-item.selected svg.select-menu-item-icon + span'
const branchNameFromSelectElement = $(selectedBranchSelector, element =>
element.textContent.trim()
)
if (branchNameFromSelectElement) return branchNameFromSelectElement
}
@ -78,9 +85,8 @@ function scrollToRepoContent() {
/**
* scroll to index-th element in the list
* @param {number} index index of node item in the list
*/
function scrollToNodeElement(index) {
function scrollToNodeElement(index: number) {
const nodeElementSelector = '.node-item'
const nodeElements = document.querySelectorAll(nodeElementSelector)
const targetElement = nodeElements[index]
@ -98,10 +104,10 @@ const pjax = new PJAX({
scrollTo: false,
analytics: false,
cacheBust: false,
forceCache: true,
})
forceCache: true, // TODO: merge namespace, add forceCache
} as any)
function loadWithPJAX(URL) {
function loadWithPJAX(URL: string) {
NProgress.start()
pjax.loadUrl(URL, { scrollTo: 0 })
}
@ -130,9 +136,11 @@ const PAGE_TYPES = {
function getCurrentPageType() {
const blobWrapperSelector = '.repository-content .file .blob-wrapper table'
const readmeSelector = '.repository-content .readme'
return $(blobWrapperSelector, () => PAGE_TYPES.RAW_TEXT)
|| $(readmeSelector, () => PAGE_TYPES.RENDERED)
|| PAGE_TYPES.OTHERS
return (
$(blobWrapperSelector, () => PAGE_TYPES.RAW_TEXT) ||
$(readmeSelector, () => PAGE_TYPES.RENDERED) ||
PAGE_TYPES.OTHERS
)
}
export const REPO_TYPE_PRIVATE = 'private'
@ -169,7 +177,7 @@ function attachCopyFileBtn() {
* @param {element} copyFileBtn
* @param {string} text
*/
function setTempCopyFileBtnText(copyFileBtn, text) {
function setTempCopyFileBtnText(copyFileBtn: HTMLButtonElement, text: string) {
copyFileBtn.innerText = text
window.setTimeout(() => (copyFileBtn.innerText = 'Copy file'), 1000)
}
@ -195,7 +203,7 @@ function attachCopyFileBtn() {
setTempCopyFileBtnText(copyFileBtn, 'Copy failed!')
}
})
btnGroup.insertBefore(copyFileBtn, btnGroups.lastChild)
btnGroup.insertBefore(copyFileBtn, btnGroup.lastChild)
})
}
}
@ -205,7 +213,7 @@ function attachCopyFileBtn() {
* @param {element} element
* @returns {boolean} whether copy is successful
*/
function copyElementContent(element) {
function copyElementContent(element: Element) {
window.getSelection().removeAllRanges()
const range = document.createRange()
range.selectNode(element)
@ -223,7 +231,7 @@ function copyElementContent(element) {
* TODO: 'reactify' it
*/
function createClippy() {
function setTempClippyIconFeedback(clippy, type) {
function setTempClippyIconFeedback(clippy: Element, type: 'success' | 'fail') {
const tempIconClassName = type === 'success' ? 'success' : 'fail'
clippy.classList.add(tempIconClassName)
window.setTimeout(() => {
@ -262,12 +270,13 @@ function createClippy() {
const clippy = createClippy()
let currentCodeSnippetElement
let currentCodeSnippetElement: Element
function attachCopySnippet() {
const readmeSelector = '.repository-content #readme article'
return $(readmeSelector, readmeElement =>
readmeElement.addEventListener('mouseover', ({ target }) => {
readmeElement.addEventListener('mouseover', e => {
// only move clippy when mouse is over a new snippet(<pre>)
const target = e.target as Element
if (target.nodeName === 'PRE') {
if (currentCodeSnippetElement !== target) {
currentCodeSnippetElement = target
@ -291,12 +300,12 @@ function attachCopySnippet() {
*/
function focusFileExplorer() {
const sideBarContentSelector = '.gitako-side-bar .file-explorer'
$(sideBarContentSelector, sideBarElement => sideBarElement.focus())
$(sideBarContentSelector, (sideBarElement: HTMLElement) => sideBarElement.focus())
}
function focusSearchInput() {
const searchInputSelector = '.search-input'
$(searchInputSelector, searchInputElement => {
$(searchInputSelector, (searchInputElement: HTMLElement) => {
if (document.activeElement !== searchInputElement) {
searchInputElement.focus()
}
@ -309,14 +318,20 @@ function focusSearchInput() {
*/
function clickOnNodeElement(index = 0) {
const nodeElementSelector = '.node-item'
const nodeElements = document.querySelectorAll(nodeElementSelector)
const nodeElements: NodeListOf<HTMLElement> = document.querySelectorAll(nodeElementSelector)
nodeElements[index].click()
}
/**
* a combination of few above functions
*/
function decorateGitHubPageContent({ copyFileButton, copySnippetButton }) {
function decorateGitHubPageContent({
copyFileButton,
copySnippetButton,
}: {
copyFileButton: boolean
copySnippetButton: boolean
}) {
if (copyFileButton) attachCopyFileBtn()
if (copySnippetButton) attachCopySnippet()
}

View file

@ -4,16 +4,24 @@ export const BAD_CREDENTIALS = 'Bad credentials'
export const API_RATE_LIMIT = `API rate limit`
export const EMPTY_PROJECT = `Empty project`
function apiRateLimitExceeded(content) {
return content && content['documentation_url'] === 'https://developer.github.com/v3/#rate-limiting'
function apiRateLimitExceeded(content: any) {
return (
content && content['documentation_url'] === 'https://developer.github.com/v3/#rate-limiting'
)
}
function isEmptyProject(content) {
function isEmptyProject(content: any) {
return content && content['message'] === 'Git Repository is empty.'
}
async function request(url, { accessToken } = {}) {
const headers = {}
type Options = {
accessToken?: string
}
async function request(url: string, { accessToken }: Options = {}) {
const headers = {} as {
Authorization?: string
}
if (accessToken) {
headers.Authorization = `token ${accessToken}`
}
@ -30,22 +38,56 @@ async function request(url, { accessToken } = {}) {
}
}
async function getRepoMeta({ userName, repoName, accessToken }) {
export type MetaData = {
userName?: string
repoName?: string
branchName?: string
accessToken?: string
}
async function getRepoMeta({ userName, repoName, accessToken }: MetaData) {
const url = `https://api.github.com/repos/${userName}/${repoName}`
return await request(url, { accessToken })
}
async function getTreeData({ userName, repoName, branchName, accessToken }) {
export type TreeItem = {
path: string
}
export type TreeData = {
userName: string
repoName: string
branchName: string
accessToken: string
tree: TreeItem[]
}
async function getTreeData({ userName, repoName, branchName, accessToken }: TreeData) {
const url = `https://api.github.com/repos/${userName}/${repoName}/git/trees/${branchName}?recursive=1`
return await request(url, { accessToken })
}
async function getBlobData({ userName, repoName, accessToken, fileSHA }) {
export type ItemData = {
userName: string
repoName: string
branchName: string
accessToken: string
}
export type BlobData = {
fileSHA: string
} & ItemData
async function getBlobData({ userName, repoName, accessToken, fileSHA }: BlobData) {
const url = `https://api.github.com/repos/${userName}/${repoName}/git/blobs/${fileSHA}`
return await request(url, { accessToken })
}
function getUrlForRedirect({ userName, repoName, branchName }, type = 'blob', path) {
function getUrlForRedirect(
{ userName, repoName, branchName }: MetaData,
type = 'blob',
path?: string
) {
return `https://github.com/${userName}/${repoName}/${type}/${branchName}/${path}`
}

View file

@ -1,4 +1,5 @@
import { raiseError } from 'analytics'
import { MetaData } from './GitHubHelper'
function parse() {
const { pathname } = window.location
@ -20,7 +21,7 @@ function parse() {
function parseSHA() {
const { type, path } = parse()
return (type === 'blob' || type === 'tree') ? path[0] : false
return type === 'blob' || type === 'tree' ? path[0] : false
}
function isInRepoPage() {
@ -37,18 +38,18 @@ const TYPES = {
// TODO: record more types
}
function isInCodePage(metaData = {}) {
function isInCodePage(metaData: MetaData = {}) {
const mergedRepo = { ...parse(), ...metaData }
const { type, branchName } = mergedRepo
return Boolean(
isInRepoPage(mergedRepo) &&
isInRepoPage() &&
(!type || type === TYPES.TREE || type === TYPES.BLOB) &&
type !== TYPES.COMMIT &&
(branchName || (!type && !branchName))
)
}
function isCommitPath(path) {
function isCommitPath(path: string[]) {
return /^[a-z0-9]{40}$/.test(path[0])
}

View file

@ -1,6 +1,14 @@
import storageHelper from 'utils/storageHelper'
import { pick } from 'utils/general'
type Config = {
shortcut: string
accessToken: string | null
compressSingletonFolder: boolean
copyFileButton: boolean
copySnippetButton: boolean
}
export const config = {
shortcut: 'shortcut',
accessToken: 'access_token',
@ -11,19 +19,19 @@ export const config = {
const configKeys = Object.values(config)
function get() {
return storageHelper.get(configKeys)
function get(): any {
return storageHelper.get(configKeys) || {}
}
function getOne(key) {
function getOne(key: keyof Config) {
return get()[key]
}
function set(partialConfig) {
function set(partialConfig: Partial<Config>) {
return storageHelper.set(pick(partialConfig, configKeys))
}
function setOne(key, value) {
function setOne<K extends keyof Config>(key: K, value: Config[K]) {
return set({
[key]: value,
})

View file

@ -1,9 +1,7 @@
/**
* cx('class1', { class2: true, class3: false }) --> 'class1 class2'
* @param {string} baseClassNames
* @param {object} optionalClassNames
*/
export default function cx(...classNames) {
export default function cx(...classNames: (string | object)[]): string {
return classNames
.filter(Boolean)
.map(className => {

View file

@ -1,17 +0,0 @@
/**
* @param {Object} source
* @param {Object|Array} keys
* @returns
*/
export function pick(source, keys) {
if (keys && typeof keys === 'object') {
return (
Array.isArray(keys) ? keys : Object.keys(keys)
).reduce((copy, key) => {
copy[key] = source[key]
return copy
}, {})
}
return {}
}

12
src/utils/general.ts Normal file
View file

@ -0,0 +1,12 @@
export function pick<T extends any>(source: T, keys: string[]): T {
if (keys && typeof keys === 'object') {
return (Array.isArray(keys) ? keys : Object.keys(keys)).reduce(
(copy, key) => {
copy[key] = source[key]
return copy
},
{} as T
)
}
return {} as T
}

View file

@ -1,6 +1,6 @@
const keyCodeArray = [
...'1234567890abcdefghijklmnopqrstuvwxyz'.split(''),
...'`[]\\;\',./'.split(''),
..."`[]\\;',./".split(''),
'alt',
'shift',
'ctrl',
@ -8,7 +8,7 @@ const keyCodeArray = [
]
const validKeyCodes = new Set(keyCodeArray)
function isValidKey(key) {
function isValidKey(key: string) {
return validKeyCodes.has(key)
}
@ -18,7 +18,7 @@ function isValidKey(key) {
* @param {string} keysString
* @returns {string}
*/
function parse(keysString) {
function parse(keysString: string) {
return (
keysString
.split('+')
@ -32,13 +32,11 @@ function parse(keysString) {
)
}
function parseKeyCode(code) {
return code
.toLowerCase()
.replace(/^control$/, 'ctrl')
function parseKeyCode(code: string) {
return code.toLowerCase().replace(/^control$/, 'ctrl')
}
function parseEvent(e) {
function parseEvent(e: KeyboardEvent) {
const { altKey: alt, shiftKey: shift, metaKey: meta, ctrlKey: ctrl } = e
const code = parseKeyCode(e.key)
const keys = { meta, ctrl, shift, alt, [code]: true }

View file

@ -1,10 +1,10 @@
const localStorage = chrome.storage.local
function get(mapping) {
function get(mapping: string | string[] | object) {
return new Promise(resolve => localStorage.get(mapping, resolve))
}
function set(value) {
function set(value: any) {
return new Promise(resolve => localStorage.set(value, resolve))
}

View file

@ -1,6 +1,25 @@
import GitHubHelper from 'utils/GitHubHelper'
import GitHubHelper, { TreeData, MetaData } from 'utils/GitHubHelper'
const nodeTemplate = {
interface BasicItem {
name: string | null
path: string | null
parent?: BasicItem | null
mode: null
type: string | null
url: string | null
}
interface Folder extends BasicItem {
contents?: Item[]
}
interface Blob extends BasicItem {
sha: string | null
}
type Item = Folder & Blob
const nodeTemplate: Blob = {
name: null,
path: null,
mode: null,
@ -9,10 +28,10 @@ const nodeTemplate = {
url: null,
}
function sortFoldersToFront(root) {
const isFolder = node => node.type === 'tree'
const isNotFolder = node => !isFolder(node)
function depthFirstSearch(root) {
const isFolder = (node: BasicItem) => node.type === 'tree'
const isNotFolder = (node: BasicItem) => !isFolder(node)
function sortFoldersToFront(root: Item) {
function depthFirstSearch(root: Item) {
const nodes = root.contents
if (nodes) {
nodes.splice(0, Infinity, ...nodes.filter(isFolder), ...nodes.filter(isNotFolder))
@ -23,14 +42,14 @@ function sortFoldersToFront(root) {
return depthFirstSearch(root)
}
function setParentNode(root, parent = null) {
function setParentNode(root: Item, parent: Item | null = null) {
root.parent = parent
if (root.contents) {
root.contents.forEach(node => setParentNode(node, root))
}
}
function findGitModules(root) {
function findGitModules(root: Item) {
if (root.contents) {
const modulesFile = root.contents.find(content => content.name === '.gitmodules')
if (modulesFile) {
@ -40,14 +59,14 @@ function findGitModules(root) {
return null
}
function parse(treeData, metaData) {
function parse(treeData: TreeData, metaData: 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: [] }
const root: Item = { ...nodeTemplate, name: '', path: '', contents: [] }
pathToNode.set('', root)
tree.forEach(item => pathToItem.set(item.path, item))
@ -69,9 +88,7 @@ function parse(treeData, metaData) {
...nodeTemplate,
...item,
name: item.path.replace(/^.*\//, ''),
url: item.url
? GitHubHelper.getUrlForRedirect(metaData, item.type, item.path)
: null,
url: item.url ? GitHubHelper.getUrlForRedirect(metaData, item.type, item.path) : null,
contents: item.type === 'tree' ? [] : null,
}
pathToNode.get(path).contents.push(node)
@ -81,7 +98,7 @@ function parse(treeData, metaData) {
})
setParentNode(root)
return {
gitModules: findGitModules(root),
root: sortFoldersToFront(root),

View file

@ -1,18 +1,18 @@
{
"files": ["src/content.jsx"],
"compilerOptions": {
"target": "es2015",
"target": "es2016",
"outDir": "dist",
"jsx": "react",
"allowJs": true,
"module": "commonjs",
"noImplicitAny": true,
"resolveJsonModule": true,
"lib": ["dom", "es2015"],
"lib": ["dom", "es2017.object", "es2016"],
"baseUrl": ".",
"experimentalDecorators": true,
"paths": {
"*": ["src/*"]
"*": ["src/*", "packages/*"]
}
},
"include": ["src"]

View file

@ -87,11 +87,35 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
"@types/chrome@^0.0.77":
version "0.0.77"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.77.tgz#afab7a45da9ebf67d5d50c0ee0a347582128d47e"
integrity sha512-VPjm9KeAbwNM0gY8wFGCqO45N382xxiUhgGaiqQPai/NmLNOHBl6w6m71seq566Na0v/+skcWozzyalmX/FMIg==
dependencies:
"@types/filesystem" "*"
"@types/filesystem@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.29.tgz#ee3748eb5be140dcf980c3bd35f11aec5f7a3748"
integrity sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==
dependencies:
"@types/filewriter" "*"
"@types/filewriter@*":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3"
integrity sha1-wFTor02d11205jq8dviFFocU1LM=
"@types/node@*":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5"
integrity sha512-wNBfvNjzsJl4tswIZKXCFQY0lss9nKUyJnG6T94X/eqjRgI2jHZ4evdjhQYBSan/vGtF6XVXPApOmNH2rf0KKw==
"@types/nprogress@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.0.29.tgz#060bd510022a005f1840234030d3132fb9195471"
integrity sha1-BgvVEAIqAF8YQCNAMNMTL7kZVHE=
"@types/prop-types@*":
version "15.5.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce"