forked from ddrilling/asb_cloud_front
* Добавлен метод копирования текста в буфер
* Добавлен компонент выбора цвета
This commit is contained in:
parent
64246bd2fd
commit
354d6945d7
129
src/components/ColorPicker.tsx
Normal file
129
src/components/ColorPicker.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { Input, Popover, Slider } from 'antd'
|
||||||
|
import { CopyOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { copyToClipboard } from './factory'
|
||||||
|
|
||||||
|
import '@styles/components/color_picker.less'
|
||||||
|
|
||||||
|
export class Color {
|
||||||
|
public r: number
|
||||||
|
public g: number
|
||||||
|
public b: number
|
||||||
|
public a: number = 1
|
||||||
|
|
||||||
|
public constructor(color: Color | string)
|
||||||
|
public constructor(r: number, g: number, b: number, a?: number)
|
||||||
|
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
let out
|
||||||
|
if (args[0] instanceof Color) {
|
||||||
|
out = args[0]
|
||||||
|
} else if (typeof args[0] === 'string') {
|
||||||
|
out = Color.parseToObject(args[0])
|
||||||
|
} else if (typeof args[0] === 'number') {
|
||||||
|
out = { r: args[0], g: args[1], b: args[2], a: args[3] ?? 1 }
|
||||||
|
} else throw new Error('Некорректные аргументы')
|
||||||
|
this.r = out.r
|
||||||
|
this.g = out.g
|
||||||
|
this.b = out.b
|
||||||
|
this.a = out.a
|
||||||
|
}
|
||||||
|
|
||||||
|
public static parse(str: string): Color {
|
||||||
|
const out = Color.parseToObject(str)
|
||||||
|
return new Color(out.r, out.g, out.b, out.a)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseToObject(str: string) {
|
||||||
|
let rgb: number[] = []
|
||||||
|
let a: number = 1
|
||||||
|
if (str.startsWith('rgb')) {
|
||||||
|
const parts = str.replaceAll(/\s/g, '').match(/rgba?\((\d+),(\d+),(\d)+(?:,([\d.]+))?\)/)
|
||||||
|
if (parts) {
|
||||||
|
rgb = parts.slice(1, 4).map((v) => Math.min(0, Math.max(parseInt(v), 255)))
|
||||||
|
if (parts[4]) a = parseFloat(`0${parts[4]}`)
|
||||||
|
}
|
||||||
|
} else if (str.startsWith('#')) {
|
||||||
|
const parts = str.slice(1)
|
||||||
|
let rgba: string[] | null = parts.length > 5 ? parts.match(/.{1,2}/g) : [...parts]
|
||||||
|
if (rgba) {
|
||||||
|
rgb = rgba.slice(0, 3).map((v) => parseInt(v, 16))
|
||||||
|
if (rgba[3]) a = parseInt(rgba[3], 16) / 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rgb.length < 3)
|
||||||
|
throw new Error('Некорректная строка')
|
||||||
|
return { r: rgb[0], g: rgb[1], b: rgb[2], a }
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString = () => this.toHexString()
|
||||||
|
public toCssString = () => `rgba(${this.r},${this.g},${this.b},${this.a})`
|
||||||
|
public toHexString() {
|
||||||
|
const a = Math.floor(this.a * 255)
|
||||||
|
let out = '#' + [this.r, this.g, this.b].map((v) => v.toString(16).padStart(2, '0')).join('')
|
||||||
|
if (a < 255) out += a.toString(16).padStart(2, '0')
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorPickerProps = {
|
||||||
|
value?: string | Color
|
||||||
|
onChange?: (value: Color) => void
|
||||||
|
size?: number | string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeChangeColor = (set: React.Dispatch<React.SetStateAction<Color>>, accessor: 'r' | 'g' | 'b' | 'a') => (value: number) => set((prev: Color) => {
|
||||||
|
const out = new Color(prev)
|
||||||
|
out[accessor] = value
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ColorPicker = memo<ColorPickerProps>(({ value = '#AA33BB', onChange, size, ...other }) => {
|
||||||
|
const [color, setColor] = useState<Color>(new Color(255, 255, 255))
|
||||||
|
|
||||||
|
useEffect(() => setColor(new Color(value)), [value])
|
||||||
|
|
||||||
|
const divStyle = useMemo(() => ({
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
backgroundColor: color.toCssString(),
|
||||||
|
}), [size, color])
|
||||||
|
|
||||||
|
const changeR = useMemo(() => makeChangeColor(setColor, 'r'), [])
|
||||||
|
const changeG = useMemo(() => makeChangeColor(setColor, 'g'), [])
|
||||||
|
const changeB = useMemo(() => makeChangeColor(setColor, 'b'), [])
|
||||||
|
const changeA = useMemo(() => makeChangeColor(setColor, 'a'), [])
|
||||||
|
|
||||||
|
const onClose = useCallback((visible: boolean) => {
|
||||||
|
if (!visible)
|
||||||
|
onChange?.(color)
|
||||||
|
}, [color, onChange])
|
||||||
|
|
||||||
|
const onCopyClick = useCallback(() => copyToClipboard(color.toHexString()), [color])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
trigger={'click'}
|
||||||
|
onVisibleChange={onClose}
|
||||||
|
content={(
|
||||||
|
<div className={'asb-color-picker-content'}>
|
||||||
|
<div className={'asb-color-picker-sliders'}>
|
||||||
|
<Slider vertical min={0} max={255} defaultValue={color.r} onChange={changeR} />
|
||||||
|
<Slider vertical min={0} max={255} defaultValue={color.g} onChange={changeG} />
|
||||||
|
<Slider vertical min={0} max={255} defaultValue={color.b} onChange={changeB} />
|
||||||
|
<Slider vertical min={0} max={1} step={0.01} defaultValue={color.a} onChange={changeA} />
|
||||||
|
</div>
|
||||||
|
<Input {...other} value={color.toHexString()} addonBefore={(
|
||||||
|
<CopyOutlined onClick={onCopyClick} />
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={'asb-color-picker-preview'} style={divStyle}/>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ColorPicker
|
@ -35,6 +35,15 @@ export const notify = (body?: ReactNode, notifyType: NotifyType = 'info', other?
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const copyToClipboard = (value: string, successText?: string, errorText?: string) => {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(value)
|
||||||
|
notify(successText ?? 'Текст успешно скопирован в буфер обмена', 'info')
|
||||||
|
} catch (ex) {
|
||||||
|
notify(errorText ?? 'Не удалось скопировать текст в буфер обмена', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type asyncFunction = (...args: any) => Promise<any|void>
|
type asyncFunction = (...args: any) => Promise<any|void>
|
||||||
|
|
||||||
const parseApiEror = (err: unknown, actionName?: string) => {
|
const parseApiEror = (err: unknown, actionName?: string) => {
|
||||||
|
21
src/styles/components/color_picker.less
Normal file
21
src/styles/components/color_picker.less
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.asb-color-picker-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
|
||||||
|
& > .asb-color-picker-sliders {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asb-color-picker-preview {
|
||||||
|
border: 1px solid black;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user