Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize what is going to be copied / Copy everything together #193

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions packages/leva/src/components/Leva/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useMemo, useState, useEffect, useRef } from 'react'
import { useDrag } from 'react-use-gesture'
import { debounce } from '../../utils'
import { debounce, LevaErrors, warn } from '../../utils'
import { FolderTitleProps } from '../Folder'
import { Chevron } from '../UI'
import { StyledFilterInput, StyledTitleWithFilter, TitleContainer, Icon, FilterWrapper } from './StyledFilter'
import { useStoreContext } from '../../context'
import { DataInput } from '../../types'

type FilterProps = { setFilter: (value: string) => void }

Expand Down Expand Up @@ -52,6 +54,8 @@ export type TitleWithFilterProps = FilterProps &
title: React.ReactNode
drag: boolean
filterEnabled: boolean
hideCopyButton: boolean
copy?: (values: unknown) => string
}

export function TitleWithFilter({
Expand All @@ -62,9 +66,12 @@ export function TitleWithFilter({
title,
drag,
filterEnabled,
hideCopyButton,
copy,
}: TitleWithFilterProps) {
const [filterShown, setShowFilter] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
const store = useStoreContext()

useEffect(() => {
if (filterShown) inputRef.current?.focus()
Expand All @@ -83,13 +90,35 @@ export function TitleWithFilter({
return () => window.removeEventListener('keydown', handleShortcut)
}, [])

const [copied, setCopied] = useState(false)
const handleCopyClick = async () => {
const data: any = store.getData()
italodeandra marked this conversation as resolved.
Show resolved Hide resolved
try {
for (let key in data) {
if (data.hasOwnProperty(key)) {
const keyData = data[key] as DataInput
if (!keyData.disabled) {
data[key] = keyData.value
} else {
delete data[key]
}
}
}
await navigator.clipboard.writeText(copy ? copy(data) : JSON.stringify(data))
setCopied(true)
} catch {
warn(LevaErrors.CLIPBOARD_ERROR, data)
}
}

return (
<>
<StyledTitleWithFilter mode={drag ? 'drag' : undefined}>
<StyledTitleWithFilter mode={drag ? 'drag' : undefined} onPointerLeave={() => setCopied(false)}>
<Icon active={!toggled} onClick={() => toggle()}>
<Chevron toggled={toggled} width={12} height={8} />
</Icon>
<TitleContainer {...(drag ? bind() : {})} drag={drag} filterEnabled={filterEnabled}>
{!hideCopyButton && <div style={{ width: 40 }} />}
{title === undefined && drag ? (
<svg width="20" height="10" viewBox="0 0 28 14" xmlns="http://www.w3.org/2000/svg">
<circle cx="2" cy="2" r="2" />
Expand All @@ -103,6 +132,25 @@ export function TitleWithFilter({
title
)}
</TitleContainer>
{!hideCopyButton && (
<Icon onClick={!copied ? handleCopyClick : undefined} title={`Click to copy all values`}>
{!copied ? (
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
<path
fillRule="evenodd"
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
)}
</Icon>
)}
{filterEnabled && (
<Icon active={filterShown} onClick={() => setShowFilter((f) => !f)}>
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 20 20">
Expand Down
57 changes: 32 additions & 25 deletions packages/leva/src/components/Leva/LevaRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export type LevaRootProps = {
* If true, the copy button will be hidden
*/
hideCopyButton?: boolean
/**
* Change what will be copied when clicking the global copy button
*/
copy?: (values: unknown) => string
Comment on lines 65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it might sense to group those options similar to the titleBar options?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of that, but hideCopyButton works for both Input and Title Bar copy button.
But perhaps I could move copy into titleBar? What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on this? I was planning on finishing the changes for this PR today.

}


Expand Down Expand Up @@ -97,6 +101,7 @@ const LevaCore = React.memo(
filter: true
},
hideCopyButton = false,
copy,
toggled,
setToggle,
}: LevaCoreProps) => {
Expand All @@ -115,31 +120,33 @@ const LevaCore = React.memo(

return (
<PanelSettingsContext.Provider value={{ hideCopyButton }}>
<StyledRoot
ref={rootRef}
className={rootClass}
fill={fill}
flat={flat}
oneLineLabels={oneLineLabels}
hideTitleBar={!titleBar}
style={{ display: shouldShow ? 'block' : 'none' }}>
{titleBar && (
<TitleWithFilter
onDrag={set}
setFilter={setFilter}
toggle={() => setToggle((t) => !t)}
toggled={toggled}
title={title}
drag={drag}
filterEnabled={filterEnabled}
/>
)}
{shouldShow && (
<StoreContext.Provider value={store}>
<TreeWrapper isRoot fill={fill} flat={flat} tree={tree} toggled={toggled} />
</StoreContext.Provider>
)}
</StyledRoot>
<StoreContext.Provider value={store}>
<StyledRoot
ref={rootRef}
className={rootClass}
fill={fill}
flat={flat}
oneLineLabels={oneLineLabels}
hideTitleBar={!titleBar}
style={{ display: shouldShow ? 'block' : 'none' }}>
{titleBar && (
<TitleWithFilter
onDrag={set}
setFilter={setFilter}
toggle={() => setToggle((t) => !t)}
toggled={toggled}
title={title}
drag={drag}
filterEnabled={filterEnabled}
hideCopyButton={hideCopyButton}
copy={copy}
/>
)}
{shouldShow && (
<TreeWrapper isRoot fill={fill} flat={flat} tree={tree} toggled={toggled} />
)}
</StyledRoot>
</StoreContext.Provider>
</PanelSettingsContext.Provider>
)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/leva/src/components/UI/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function RawLabel(props: LabelProps) {
}

export function Label({ align, ...props }: LabelProps) {
const { value, label, key } = useInputContext()
const { value, label, key, copy = (key, value) => JSON.stringify({ [key]: value ?? '' }) } = useInputContext()
const { hideCopyButton } = usePanelSettingsContext()

const copyEnabled = !hideCopyButton && key !== undefined
Expand All @@ -54,7 +54,7 @@ export function Label({ align, ...props }: LabelProps) {

const handleClick = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify({ [key]: value ?? '' }))
await navigator.clipboard.writeText(copy(key, value))
setCopied(true)
} catch {
warn(LevaErrors.CLIPBOARD_ERROR, { [key]: value })
Expand Down
2 changes: 2 additions & 0 deletions packages/leva/src/types/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ type GenericSchemaItemOptions = {
render?: RenderFn
label?: string | JSX.Element
hint?: string
copy?: (key: string, value: unknown) => string
}

export type InputOptions = GenericSchemaItemOptions & {
Expand Down Expand Up @@ -263,6 +264,7 @@ export type InputContextProps = {
id: string
label: string | JSX.Element
hint?: string
copy: (key: string, value: unknown) => string
path: string
key: string
optional: boolean
Expand Down
4 changes: 2 additions & 2 deletions packages/leva/src/utils/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export function parseOptions(_input: any, key: string, mergedOptions = {}, custo
}

// parse generic options from input object
const { render, label, optional, disabled, hint, onChange, ...inputWithType } = _input
const commonOptions = { render, key, label: label ?? key, hint, ...mergedOptions }
const { render, label, optional, disabled, hint, copy, onChange, ...inputWithType } = _input
const commonOptions = { render, key, label: label ?? key, hint, copy, ...mergedOptions }

let { type, ...input } = inputWithType
type = customType ?? type
Expand Down
38 changes: 34 additions & 4 deletions packages/leva/stories/input-options.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Meta } from '@storybook/react'
import Reset from './components/decorator-reset'
import { Half2Icon, OpacityIcon, DimensionsIcon } from '@radix-ui/react-icons'

import { folder, useControls } from '../src'
import { folder, Leva, useControls } from '../src'

export default {
title: 'Misc/Input options',
Expand Down Expand Up @@ -76,14 +76,14 @@ export const Optional = () => {

function A() {
const renderRef = React.useRef(0)
const divRef = React.useRef(null)
const divRef = React.useRef<HTMLDivElement>(null)
renderRef.current++
const data = useControls({
color: {
value: '#f00',
onChange: (v) => {
divRef.current.style.color = v
divRef.current.innerText = `Transient color is ${v}`
divRef.current!.style.color = v
divRef.current!.innerText = `Transient color is ${v}`
},
},
})
Expand Down Expand Up @@ -118,3 +118,33 @@ export const OnChange = () => {
</>
)
}

export const CustomCopy = () => {
const { id, label } = useControls({
id: {
value: 'button-id',
copy(key, value): string {
return `<button ${key}="${value}">${label}</button>`
}
},
label: {
value: 'Leva is awesome',
copy(key, value) {
return `<button>{${value}}</button>`
}
},
})

const handleLevaCopy = (values: any) => {
return `<button
id="${values.id}"
>${values.label}</button>`
}

return (
<div>
<Leva copy={handleLevaCopy} />
<button id={id}>{label}</button>
</div>
)
}