mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: make clippy button react
This commit is contained in:
parent
943a2ccc4b
commit
7bee13c6c3
4 changed files with 85 additions and 86 deletions
33
src/components/Clippy.tsx
Normal file
33
src/components/Clippy.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from 'react'
|
||||
import { cx } from 'utils/cx'
|
||||
import { copyElementContent } from 'utils/DOMHelper'
|
||||
|
||||
type Props = {
|
||||
codeSnippetElement: Element
|
||||
}
|
||||
|
||||
export function Clippy({ codeSnippetElement }: Props) {
|
||||
const [status, setStatus] = React.useState<'normal' | 'success' | 'fail'>('normal')
|
||||
React.useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
setStatus('normal')
|
||||
}, 1000)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [status])
|
||||
|
||||
const onClippyClick = React.useCallback(function onClippyClick() {
|
||||
if (copyElementContent(codeSnippetElement)) {
|
||||
setStatus('success')
|
||||
} else {
|
||||
setStatus('fail')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="clippy-wrapper">
|
||||
<button className="clippy" onClick={onClippyClick}>
|
||||
<i className={cx('icon', status)} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -59,14 +59,10 @@
|
|||
background-image: url('~@primer/octicons/build/svg/clippy.svg?inline');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
&.success {
|
||||
.icon {
|
||||
&.success {
|
||||
background-image: url('~@primer/octicons/build/svg/check.svg?inline');
|
||||
}
|
||||
}
|
||||
&.fail {
|
||||
.icon {
|
||||
&.fail {
|
||||
background-image: url('~@primer/octicons/build/svg/x.svg?inline');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
* this helper helps manipulating DOM
|
||||
*/
|
||||
import { raiseError } from 'analytics'
|
||||
import { Clippy } from 'components/Clippy'
|
||||
import { CopyFileButton } from 'components/CopyFileButton'
|
||||
import * as NProgress from 'nprogress'
|
||||
import * as PJAX from 'pjax'
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { renderReact } from './general'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
|
|
@ -37,10 +39,10 @@ function $<EE extends Element, E extends (element: EE) => any, O extends () => a
|
|||
otherwise?: O,
|
||||
): E extends never
|
||||
? O extends never
|
||||
? (Element | null)
|
||||
? Element | null
|
||||
: ReturnType<O> | null
|
||||
: O extends never
|
||||
? (ReturnType<E> | null)
|
||||
? ReturnType<E> | null
|
||||
: ReturnType<O> | ReturnType<E> {
|
||||
const element = document.querySelector(selector)
|
||||
if (element) {
|
||||
|
|
@ -221,10 +223,8 @@ export function attachCopyFileBtn() {
|
|||
|
||||
/**
|
||||
* copy content of a DOM element to clipboard
|
||||
* @param {element} element
|
||||
* @returns {boolean} whether copy is successful
|
||||
*/
|
||||
export function copyElementContent(element: Element) {
|
||||
export function copyElementContent(element: Element): boolean {
|
||||
let selection = window.getSelection()
|
||||
if (selection) selection.removeAllRanges()
|
||||
const range = document.createRange()
|
||||
|
|
@ -237,54 +237,6 @@ export function copyElementContent(element: Element) {
|
|||
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: Element, type: 'success' | 'fail') {
|
||||
const tempIconClassName = type === 'success' ? 'success' : 'fail'
|
||||
clippy.classList.add(tempIconClassName)
|
||||
window.setTimeout(() => {
|
||||
clippy.classList.remove(tempIconClassName)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* <div class="clippy-wrapper">
|
||||
* <button class="clippy">
|
||||
* <i class="octicon octicon-clippy" />
|
||||
* </button>
|
||||
* </div>
|
||||
*/
|
||||
const clippyWrapper = document.createElement('div')
|
||||
clippyWrapper.classList.add('clippy-wrapper')
|
||||
const clippy = document.createElement('button')
|
||||
clippy.classList.add('clippy')
|
||||
const clippyIcon = document.createElement('i')
|
||||
clippyIcon.classList.add('icon')
|
||||
|
||||
clippyWrapper.appendChild(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 clippyWrapper
|
||||
}
|
||||
|
||||
const clippy = createClippy()
|
||||
|
||||
let currentCodeSnippetElement: Element
|
||||
export function attachCopySnippet() {
|
||||
const readmeSelector = '.repository-content div#readme'
|
||||
return $(readmeSelector, () => {
|
||||
|
|
@ -292,12 +244,15 @@ export function attachCopySnippet() {
|
|||
$(
|
||||
readmeArticleSelector,
|
||||
readmeElement =>
|
||||
readmeElement.addEventListener('mouseover', e => {
|
||||
readmeElement.addEventListener('mouseover', async 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
|
||||
if (
|
||||
target.previousSibling === null ||
|
||||
!(target.previousSibling instanceof Element) ||
|
||||
!target.previousSibling.classList.contains('clippy-wrapper')
|
||||
) {
|
||||
/**
|
||||
* <article>
|
||||
* <pre></pre> <!-- case A -->
|
||||
|
|
@ -306,7 +261,12 @@ export function attachCopySnippet() {
|
|||
* </div>
|
||||
* </article>
|
||||
*/
|
||||
if (target.parentNode) target.parentNode.insertBefore(clippy, target)
|
||||
if (target.parentNode) {
|
||||
const clippyElement = await renderReact(
|
||||
React.createElement(Clippy, { codeSnippetElement: target }),
|
||||
)
|
||||
target.parentNode.insertBefore(clippyElement, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { ReactElement } from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { TreeNode } from './VisibleNodesGenerator'
|
||||
|
||||
export function pick<T>(source: T, keys: string[]): Partial<T> {
|
||||
if (keys && typeof keys === 'object') {
|
||||
return (Array.isArray(keys) ? keys : Object.keys(keys)).reduce(
|
||||
(copy, key) => {
|
||||
if (key in source) {
|
||||
copy[key as keyof T] = source[key as keyof T]
|
||||
}
|
||||
return copy
|
||||
},
|
||||
{} as Partial<T>,
|
||||
)
|
||||
return (Array.isArray(keys) ? keys : Object.keys(keys)).reduce((copy, key) => {
|
||||
if (key in source) {
|
||||
copy[key as keyof T] = source[key as keyof T]
|
||||
}
|
||||
return copy
|
||||
}, {} as Partial<T>)
|
||||
}
|
||||
return {} as Partial<T>
|
||||
}
|
||||
|
|
@ -114,20 +113,22 @@ export function parseURLSearch(search: string = window.location.search) {
|
|||
}
|
||||
|
||||
export async function JSONRequest(url: string, data: any, extra: RequestInit = { method: 'post' }) {
|
||||
return (await fetch(url, {
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer',
|
||||
method: extra.method || 'post',
|
||||
body: JSON.stringify(data),
|
||||
...extra,
|
||||
})).json()
|
||||
return (
|
||||
await fetch(url, {
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer',
|
||||
method: extra.method || 'post',
|
||||
body: JSON.stringify(data),
|
||||
...extra,
|
||||
})
|
||||
).json()
|
||||
}
|
||||
|
||||
export function searchKeyToRegexps(searchKey: string) {
|
||||
|
|
@ -140,3 +141,12 @@ export function searchKeyToRegexps(searchKey: string) {
|
|||
return [/$^/] // matching nothing if failed transforming regexp
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderReact(element: ReactElement) {
|
||||
return new Promise<Node>(resolve => {
|
||||
const mount = document.createElement('div')
|
||||
ReactDOM.render(element, mount, () => {
|
||||
resolve(mount.childNodes[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue