Skip to content

Commit 62ce89b

Browse files
authored
Add color picker to custom component library (#219)
1 parent 452f3fd commit 62ce89b

10 files changed

Lines changed: 215 additions & 2 deletions

File tree

custom-component-library/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ These are the ones currently available:
3131
- [x] Markdown
3232
- [x] "Enhanced" link
3333
- [x] Image
34-
- [ ] Colour picker (to-do)
34+
- [x] Colour picker
3535

3636
## Development
3737

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* A Colour picker Custom Component
3+
*
4+
* Can handle named colours, Hex, RGB and HSL formats, with an optional alpha
5+
* channel
6+
*/
7+
8+
import React, { lazy, Suspense, useRef } from 'react'
9+
import { HsvColor } from 'react-colorful'
10+
import { colord, extend, getFormat, HsvaColor } from 'colord'
11+
import namesPlugin from 'colord/plugins/names'
12+
import { useDebouncedCallback } from 'use-debounce'
13+
import { StringEdit, toPathString, type CustomNodeProps } from '@json-edit-react'
14+
import { Loading } from '../_common/Loading'
15+
16+
extend([namesPlugin])
17+
18+
const HsvaColorPicker = lazy(() =>
19+
import('react-colorful').then((m) => ({ default: m.HsvaColorPicker }))
20+
)
21+
22+
const HsvColorPicker = lazy(() =>
23+
import('react-colorful').then((m) => ({ default: m.HsvColorPicker }))
24+
)
25+
26+
export interface ColorPickerProps {
27+
loadingText?: string
28+
swatchStyles?: React.CSSProperties
29+
invalidColorError?: string
30+
/**
31+
* If true, the color picker will include an alpha channel slider
32+
* @default false
33+
*/
34+
alpha?: boolean
35+
/**
36+
* If `keepAsColor` is true, if the text input is not a valid color, the
37+
* component will display an error on submission and not update the value
38+
* @default true
39+
*/
40+
keepAsColor?: boolean
41+
}
42+
43+
export const ColorPickerComponent: React.FC<CustomNodeProps<ColorPickerProps>> = ({
44+
isEditing,
45+
setIsEditing,
46+
handleKeyPress,
47+
value,
48+
setValue,
49+
getStyles,
50+
nodeData,
51+
originalNode,
52+
customNodeProps = {},
53+
onError,
54+
handleEdit,
55+
...props
56+
}) => {
57+
const {
58+
loadingText,
59+
swatchStyles,
60+
alpha = false,
61+
keepAsColor = true,
62+
invalidColorError = 'Invalid Color',
63+
} = customNodeProps
64+
65+
const text = value as string
66+
67+
// The current internal state of the color picker
68+
const [hsvValue, setHsvValue] = React.useState(colord(text).toHsv())
69+
70+
const lastValidColor = useRef(text)
71+
72+
// Debounced setValue to avoid excessive updates while dragging the picker
73+
const debouncedSetValue = useDebouncedCallback((value: string) => {
74+
setValue(value)
75+
}, 150)
76+
77+
const stringStyle = getStyles('string', nodeData)
78+
79+
const PickerComponent = alpha ? HsvaColorPicker : HsvColorPicker
80+
81+
if (typeof text !== 'string') return null
82+
83+
return (
84+
<Suspense
85+
fallback={
86+
<div style={stringStyle}>
87+
<Loading text={loadingText} />
88+
</div>
89+
}
90+
>
91+
{isEditing ? (
92+
<div style={{ position: 'relative' }}>
93+
<StringEdit
94+
styles={getStyles('input', nodeData)}
95+
pathString={toPathString(nodeData.path)}
96+
{...props}
97+
value={text}
98+
setValue={
99+
((value: string) => {
100+
// Only update the color picker display if the input text is a
101+
// valid color
102+
const parsed = colord(value)
103+
if (parsed.isValid()) {
104+
setHsvValue(parsed.toHsv())
105+
}
106+
setValue(value)
107+
}) as React.Dispatch<React.SetStateAction<string>>
108+
}
109+
handleEdit={() => {
110+
if (keepAsColor && !colord(text).isValid()) {
111+
handleEdit(lastValidColor.current)
112+
onError({ code: 'UPDATE_ERROR', message: invalidColorError }, text)
113+
return
114+
}
115+
lastValidColor.current = text
116+
setHsvValue(colord(text).toHsv())
117+
handleEdit(text)
118+
}}
119+
/>
120+
<PickerComponent
121+
style={{
122+
position: 'absolute',
123+
bottom: '1.5em',
124+
zIndex: 1000,
125+
boxShadow: 'rgba(0, 0, 0, 0.35) 0px 5px 15px',
126+
borderRadius: '8px',
127+
}}
128+
color={hsvValue}
129+
onChange={(newColor) => {
130+
setHsvValue(colord(newColor).toHsv())
131+
debouncedSetValue(getFormattedColorText(newColor, text))
132+
}}
133+
onKeyDown={handleKeyPress}
134+
/>
135+
</div>
136+
) : (
137+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.2em' }}>
138+
{originalNode}
139+
{/* Display a color swatch */}
140+
<div
141+
style={{
142+
width: '1em',
143+
height: '1em',
144+
backgroundColor: colord(text).toHex(),
145+
borderRadius: '0.15em',
146+
...swatchStyles,
147+
}}
148+
onDoubleClick={() => setIsEditing(true)}
149+
/>
150+
</div>
151+
)}
152+
</Suspense>
153+
)
154+
}
155+
156+
// Set the text value based on the selected color, keeping the
157+
// format the same as the current text input, where possible
158+
const getFormattedColorText = (color: HsvColor | HsvaColor, currentText: string): string => {
159+
let newValue = ''
160+
switch (getFormat(currentText)) {
161+
case 'rgb':
162+
newValue = colord(color).toRgbString()
163+
break
164+
case 'hsl':
165+
newValue = colord(color).toHslString()
166+
break
167+
default:
168+
newValue = colord(color).toHex()
169+
break
170+
}
171+
return newValue
172+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type CustomNodeDefinition } from '@json-edit-react'
2+
import { ColorPickerComponent, ColorPickerProps } from './component'
3+
import { colord } from 'colord'
4+
5+
export const ColorPickerNodeDefinition: CustomNodeDefinition<ColorPickerProps> = {
6+
condition: ({ value }) => typeof value === 'string' && colord(value).isValid(),
7+
element: ColorPickerComponent,
8+
name: 'Color Picker',
9+
// customNodeProps: {},
10+
showOnView: true,
11+
showOnEdit: true,
12+
showInTypesSelector: true,
13+
defaultValue: '#ff69B4', // Hot Pink!
14+
passOriginalNode: true,
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './definition'

custom-component-library/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './Symbol'
99
export * from './BigInt'
1010
export * from './Markdown'
1111
export * from './Image'
12+
export * from './ColorPicker'

custom-component-library/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
"preview": "vite preview"
1313
},
1414
"dependencies": {
15+
"colord": "^2.9.3",
1516
"json-edit-react": "1.28.2",
1617
"react": "^19.0.0",
18+
"react-colorful": "^5.6.1",
1719
"react-datepicker": "^7.5.0",
1820
"react-dom": "^19.0.0",
19-
"react-markdown": "^10.1.0"
21+
"react-markdown": "^10.1.0",
22+
"use-debounce": "^10.0.5"
2023
},
2124
"devDependencies": {
2225
"@eslint/js": "^9.28.0",

custom-component-library/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
MarkdownNodeDefinition,
1515
EnhancedLinkCustomNodeDefinition,
1616
ImageNodeDefinition,
17+
ColorPickerNodeDefinition,
1718
} from '../components'
1819
import { testData } from './data'
1920
import { JsonEditor } from '@json-edit-react'
@@ -57,6 +58,7 @@ function App() {
5758
customNodeDefinitions={[
5859
{ ...ImageNodeDefinition, customNodeProps: { imageStyles: { maxWidth, maxHeight } } },
5960
LinkCustomNodeDefinition,
61+
ColorPickerNodeDefinition,
6062
{
6163
...(STORE_DATE_AS_DATE_OBJECT ? DateObjectDefinition : DatePickerDefinition),
6264
customNodeProps: { showTime },

custom-component-library/src/data.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const testData = {
2020
- NaN
2121
- Symbol
2222
- Image
23+
- ColorPicker
2324
2425
Click [here](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md) for more info
2526
`,
@@ -58,4 +59,5 @@ export const testData = {
5859
maxHeight: 100,
5960
},
6061
},
62+
'Color Picker': '#ff69B4', // Hot Pink
6163
}

custom-component-library/yarn.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,11 @@ color-name@~1.1.4:
961961
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
962962
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
963963

964+
colord@^2.9.3:
965+
version "2.9.3"
966+
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
967+
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
968+
964969
comma-separated-tokens@^2.0.0:
965970
version "2.0.3"
966971
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
@@ -1957,6 +1962,11 @@ queue-microtask@^1.2.2:
19571962
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
19581963
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
19591964

1965+
react-colorful@^5.6.1:
1966+
version "5.6.1"
1967+
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b"
1968+
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
1969+
19601970
react-datepicker@^7.5.0:
19611971
version "7.6.0"
19621972
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-7.6.0.tgz#6171988c6a9dbd4f49a45a06e3510035884b6b74"
@@ -2260,6 +2270,11 @@ uri-js@^4.2.2:
22602270
dependencies:
22612271
punycode "^2.1.0"
22622272

2273+
use-debounce@^10.0.5:
2274+
version "10.0.5"
2275+
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.5.tgz#dfad6efbab981a9cff6c7cd8678509d1bc21e34f"
2276+
integrity sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==
2277+
22632278
vfile-message@^4.0.0:
22642279
version "4.0.2"
22652280
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"

demo/src/demoData/dataDefinitions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
MarkdownNodeDefinition,
1414
EnhancedLinkCustomNodeDefinition,
1515
ImageNodeDefinition,
16+
ColorPickerNodeDefinition,
1617
} from '../../../custom-component-library/components'
1718
import { testData } from '../../../custom-component-library/src/data'
1819
import {
@@ -848,6 +849,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
848849
NanDefinition,
849850
SymbolDefinition,
850851
BigIntDefinition,
852+
ColorPickerNodeDefinition,
851853
{
852854
...MarkdownNodeDefinition,
853855
condition: ({ key }) => key === 'Markdown',

0 commit comments

Comments
 (0)