mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
/**
|
|
* this helper helps manipulating DOM
|
|
*/
|
|
|
|
import { $ } from './$'
|
|
|
|
export const rootElementID = 'gitako-root'
|
|
export const gitakoDescriptionTarget = document.documentElement
|
|
|
|
// Some custom attributes added to GitHub html would be removed by GitHub when some events happen
|
|
function attachStickyAttribute(
|
|
target: Node,
|
|
shouldAttach: (mutation: MutationRecord) => boolean,
|
|
attach: (mutation: MutationRecord) => void,
|
|
mutationOptions?: MutationObserverInit,
|
|
) {
|
|
const observer = new MutationObserver(mutations => {
|
|
for (const mutation of mutations) if (shouldAttach(mutation)) attach(mutation)
|
|
})
|
|
|
|
observer.observe(target, {
|
|
attributeOldValue: true,
|
|
attributes: true,
|
|
...mutationOptions,
|
|
})
|
|
|
|
return () => observer.disconnect()
|
|
}
|
|
|
|
export const attachStickyDataAttribute = (
|
|
target: HTMLElement,
|
|
attributeName: string,
|
|
attach: (mutation: MutationRecord) => void,
|
|
) =>
|
|
attachStickyAttribute(target, () => !target.getAttribute(attributeName), attach, {
|
|
attributeFilter: [attributeName],
|
|
})
|
|
|
|
export const attachStickyStyle = (
|
|
target: HTMLElement,
|
|
styleName: string,
|
|
attach: (mutation: MutationRecord) => void,
|
|
) =>
|
|
attachStickyAttribute(
|
|
target,
|
|
() => !target.style.getPropertyValue(styleName), // `''` if not exist
|
|
attach,
|
|
{ attributeFilter: ['style'] },
|
|
)
|
|
|
|
/**
|
|
* when gitako is ready, attach attribute to activate CSS selectors
|
|
* e.g. make page's header narrower on pin sidebar
|
|
*/
|
|
const readyDataAttributeName = 'data-gitako-ready'
|
|
export const attachStickyGitakoReadyState = () =>
|
|
attachStickyDataAttribute(gitakoDescriptionTarget, readyDataAttributeName, ({ oldValue }) =>
|
|
markGitakoReadyState(oldValue === 'true'),
|
|
)
|
|
export function markGitakoReadyState(ready: boolean) {
|
|
return gitakoDescriptionTarget.setAttribute(readyDataAttributeName, `${ready}`)
|
|
}
|
|
|
|
/**
|
|
* if should show gitako, then move body right to make space for showing gitako
|
|
* otherwise, hide the space
|
|
*/
|
|
const spacingAttributeName = 'data-with-gitako-spacing'
|
|
export const attachStickyBodyIndent = () =>
|
|
attachStickyDataAttribute(gitakoDescriptionTarget, spacingAttributeName, ({ oldValue }) =>
|
|
setBodyIndent(oldValue === 'true'),
|
|
)
|
|
export function setBodyIndent(shouldShowGitako: boolean) {
|
|
gitakoDescriptionTarget.setAttribute(spacingAttributeName, `${shouldShowGitako}`)
|
|
}
|
|
|
|
/**
|
|
* DOM Structure after calling the `insert*MountPoint` functions
|
|
*
|
|
* <html>
|
|
* <body>
|
|
* </body>
|
|
* <div id={rootElementID}>
|
|
* <div id={sidebarMountPointID}>
|
|
* </div>
|
|
* <div id={logoMountPointID}>
|
|
* </div>
|
|
* </div>
|
|
* </html>
|
|
*/
|
|
|
|
const mountPointContainer = document.documentElement
|
|
export function insertMountPoint() {
|
|
return $(formatID(rootElementID), undefined, () => {
|
|
const element = document.createElement('div')
|
|
element.setAttribute('id', rootElementID)
|
|
mountPointContainer.appendChild(element)
|
|
return element
|
|
})
|
|
}
|
|
|
|
export function insertSideBarMountPoint() {
|
|
const sidebarMountPointID = 'gitako-sidebar-mount-point'
|
|
return $(formatID(sidebarMountPointID), undefined, () => {
|
|
const sideBarElement = document.createElement('div')
|
|
sideBarElement.setAttribute('id', sidebarMountPointID)
|
|
insertMountPoint().appendChild(sideBarElement)
|
|
return sideBarElement
|
|
})
|
|
}
|
|
|
|
export function insertLogoMountPoint() {
|
|
const logoMountPointID = 'gitako-logo-mount-point'
|
|
return $(formatID(logoMountPointID), undefined, () => {
|
|
const logoMountElement = document.createElement('div')
|
|
logoMountElement.setAttribute('id', logoMountPointID)
|
|
insertMountPoint().appendChild(logoMountElement)
|
|
return logoMountElement
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export function scrollToRepoContent() {
|
|
const repositoryContentSelector = '.repository-content'
|
|
// do NOT use behavior: smooth here as it will scroll horizontally
|
|
$(repositoryContentSelector, repositoryContentElement =>
|
|
repositoryContentElement.scrollIntoView(),
|
|
)
|
|
}
|
|
|
|
/**
|
|
* copy content of a DOM element to clipboard
|
|
*/
|
|
export function copyElementContent(element: Element, trimLeadingSpace?: boolean): boolean {
|
|
window.getSelection()?.removeAllRanges()
|
|
|
|
const range = document.createRange()
|
|
if (trimLeadingSpace) {
|
|
// Leading spaces can be produced by embedded DOM structures
|
|
let realWrapper: Element | null = element
|
|
while (realWrapper?.childElementCount === 1) realWrapper = realWrapper?.firstElementChild
|
|
if (realWrapper?.childElementCount && realWrapper.childElementCount > 1) {
|
|
const first = realWrapper.firstElementChild
|
|
const last = realWrapper.lastElementChild
|
|
if (first && last) {
|
|
range.selectNode(first)
|
|
range.setEndAfter(last)
|
|
}
|
|
}
|
|
} else {
|
|
range.selectNode(element)
|
|
}
|
|
|
|
window.getSelection()?.addRange(range)
|
|
const isCopySuccessful = document.execCommand('copy')
|
|
window.getSelection()?.removeAllRanges()
|
|
return isCopySuccessful
|
|
}
|
|
|
|
export function findNodeElement(node: TreeNode, rootElement: HTMLElement): HTMLElement | null {
|
|
const nodeElement = rootElement.querySelector(`a[href="${node.url}"]`)
|
|
if (nodeElement instanceof HTMLElement) return nodeElement
|
|
return null
|
|
}
|
|
|
|
export function setCSSVariable(name: string, value: string | undefined, element: HTMLElement) {
|
|
if (value === undefined) element.style.removeProperty(name)
|
|
else element.style.setProperty(name, value)
|
|
}
|
|
|
|
const gitakoWidthVariable = '--gitako-width'
|
|
export const attachStickyGitakoWidthCSSVariable = (getLatestSize: () => number) =>
|
|
attachStickyStyle(gitakoDescriptionTarget, gitakoWidthVariable, () => {
|
|
setGitakoWidthCSSVariable(getLatestSize())
|
|
})
|
|
export const setGitakoWidthCSSVariable = (size: number) => {
|
|
setCSSVariable(gitakoWidthVariable, `${size}px`, gitakoDescriptionTarget)
|
|
}
|
|
|
|
export function formatID(id: string) {
|
|
return `#${id}`
|
|
}
|
|
|
|
export function formatClass(className: string) {
|
|
return `.${className}`
|
|
}
|
|
|
|
export function parseIntFromElement(e: HTMLElement): number {
|
|
return parseInt((e.innerText || '').replace(/[^0-9]/g, ''))
|
|
}
|
|
|
|
export function cancelEvent(e: Event | React.BaseSyntheticEvent): void {
|
|
e.stopPropagation()
|
|
e.preventDefault()
|
|
}
|
|
|
|
export function onEnterKeyDown<E extends HTMLElement>(
|
|
e: React.KeyboardEvent<E>,
|
|
callback: (e: React.KeyboardEvent<E>) => void,
|
|
) {
|
|
if (e.key === 'Enter') callback(e)
|
|
}
|