spaCy/website/src/components/quickstart.js
2020-08-19 00:28:37 +02:00

299 lines
14 KiB
JavaScript

import React, { Fragment, useState, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { window, document } from 'browser-monads'
import Section from './section'
import Icon from './icon'
import { H2 } from './typography'
import { copyToClipboard } from './copy'
import classes from '../styles/quickstart.module.sass'
function getNewChecked(optionId, checkedForId, multiple) {
if (!multiple) return [optionId]
if (checkedForId.includes(optionId)) return checkedForId.filter(opt => opt !== optionId)
return [...checkedForId, optionId]
}
const Quickstart = ({
data = [],
title,
description,
copy = true,
download,
rawContent = null,
id = 'quickstart',
setters = {},
hidePrompts,
small,
codeLang,
children,
}) => {
const contentRef = useRef()
const copyAreaRef = useRef()
const isClient = typeof window !== 'undefined'
const supportsCopy = isClient && document.queryCommandSupported('copy')
const showCopy = supportsCopy && copy
const [styles, setStyles] = useState({})
const [checked, setChecked] = useState({})
const [initialized, setInitialized] = useState(false)
const [copySuccess, setCopySuccess] = useState(false)
const [otherState, setOtherState] = useState({})
const setOther = (id, value) => setOtherState({ ...otherState, [id]: value })
const getRawContent = ref => {
if (rawContent !== null) return rawContent
if (ref.current && ref.current.childNodes) {
// Select all currently visible nodes (spans and text nodes)
const result = [...ref.current.childNodes].filter(el => el.offsetParent !== null)
return result.map(el => el.textContent).join('\n')
}
return ''
}
const onClickCopy = () => {
copyAreaRef.current.value = getRawContent(contentRef)
copyToClipboard(copyAreaRef, setCopySuccess)
}
const getCss = (id, checkedOptions) => {
const checkedForId = checkedOptions[id] || []
const exclude = checkedForId
.map(value => `:not([data-quickstart-${id}="${value}"])`)
.join('')
return `[data-quickstart-results]>[data-quickstart-${id}]${exclude} {display: none}`
}
useEffect(() => {
window.dispatchEvent(new Event('resize')) // scroll position for progress
if (!initialized) {
const initialChecked = Object.assign(
{},
...data.map(({ id, options = [] }) => ({
[id]: options.filter(option => option.checked).map(({ id }) => id),
}))
)
const initialStyles = Object.assign(
{},
...data.map(({ id }) => ({ [id]: getCss(id, initialChecked) }))
)
setChecked(initialChecked)
setStyles(initialStyles)
setInitialized(true)
}
}, [data, initialized])
return !data.length ? null : (
<Section id={id}>
<div className={classNames(classes.root, { [classes.hidePrompts]: !!hidePrompts })}>
{title && (
<H2 className={classes.title} name={id}>
<a href={`#${id}`}>{title}</a>
</H2>
)}
{description && <p className={classes.description}>{description}</p>}
{data.map(
({
id,
title,
options = [],
dropdown = [],
defaultValue,
multiple,
other,
help,
}) => {
// Optional function that's called with the value
const setterFunc = setters[id] || (() => {})
return (
<div key={id} data-quickstart-group={id} className={classes.group}>
<style data-quickstart-style={id} scoped>
{styles[id] ||
`[data-quickstart-results]>[data-quickstart-${id}] { display: none }`}
</style>
<div className={classes.legend}>
{title}
{help && (
<span data-tooltip={help} className={classes.help}>
{' '}
<Icon name="help" width={16} />
</span>
)}
</div>
<div className={classes.fields}>
{!!dropdown.length && (
<select
defaultValue={defaultValue}
className={classes.select}
onChange={({ target }) => {
const value = target.value
if (value != other) {
setterFunc(value)
setOther(id, false)
} else {
setterFunc('')
setOther(id, true)
}
}}
>
{dropdown.map(({ id, title }) => (
<option key={id} value={id}>
{title}
</option>
))}
{other && <option value={other}>{other}</option>}
</select>
)}
{other && otherState[id] && (
<input
type="text"
className={classes.textInput}
placeholder="Type here..."
onChange={({ target }) => setterFunc(target.value)}
/>
)}
{options.map(option => {
const optionType = multiple ? 'checkbox' : 'radio'
const checkedForId = checked[id] || []
return (
<Fragment key={option.id}>
<input
onChange={() => {
const newChecked = {
...checked,
[id]: getNewChecked(
option.id,
checkedForId,
multiple
),
}
setChecked(newChecked)
setStyles({
...styles,
[id]: getCss(id, newChecked),
})
setterFunc(newChecked[id])
}}
type={optionType}
className={classNames(
classes.input,
classes[optionType]
)}
name={id}
id={`quickstart-${option.id}`}
value={option.id}
checked={checkedForId.includes(option.id)}
/>
<label
className={classes.label}
htmlFor={`quickstart-${option.id}`}
>
{option.title}
{option.meta && (
<span className={classes.meta}>
{option.meta}
</span>
)}
{option.help && (
<span
data-tooltip={option.help}
className={classes.help}
>
{' '}
<Icon name="help" width={16} />
</span>
)}
</label>
</Fragment>
)
})}
</div>
</div>
)
}
)}
<pre className={classes.code}>
<code
className={classNames(classes.results, {
[classes.small]: !!small,
[`language-${codeLang}`]: !!codeLang,
})}
data-quickstart-results=""
ref={contentRef}
>
{children}
</code>
<menu className={classes.menu}>
{showCopy && (
<button
title="Copy to clipboard"
onClick={onClickCopy}
className={classes.iconButton}
>
<Icon width={18} name={copySuccess ? 'accept' : 'clipboard'} />
</button>
)}
{download && (
<a
href={`data:application/octet-stream,${getRawContent(contentRef)}`}
title="Download file"
download={download}
className={classes.iconButton}
>
<Icon width={18} name="download" />
</a>
)}
</menu>
</pre>
{showCopy && <textarea ref={copyAreaRef} className={classes.copyArea} rows={1} />}
</div>
</Section>
)
}
Quickstart.propTypes = {
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
data: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
multiple: PropTypes.bool,
options: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
checked: PropTypes.bool,
help: PropTypes.string,
})
),
help: PropTypes.string,
})
),
}
const QS = ({ children, prompt = 'bash', divider = false, comment = false, ...props }) => {
const qsClassNames = classNames({
[classes.prompt]: !!prompt && !divider,
[classes.bash]: prompt === 'bash' && !divider,
[classes.python]: prompt === 'python' && !divider,
[classes.divider]: !!divider,
[classes.comment]: !!comment,
})
const attrs = Object.assign(
{},
...Object.keys(props).map(key => ({
[`data-quickstart-${key}`]: props[key],
}))
)
return (
<span className={qsClassNames} {...attrs}>
{children}
</span>
)
}
export { Quickstart, QS }