diff --git a/packages/color-picker/package.json b/packages/color-picker/package.json index 44f87769..30b43f00 100644 --- a/packages/color-picker/package.json +++ b/packages/color-picker/package.json @@ -27,10 +27,12 @@ "classnames": "^2.3.1", "lodash": "^4.17.21", "reactcss": "^1.2.3", + "rxjs": "^7.5.5", "tinycolor2": "^1.4.2", "zustand": "^3.7.1" }, "devDependencies": { - "@types/lodash": "^4.14.181" + "@types/lodash": "^4.14.181", + "@types/reactcss": "^1.2.6" } } diff --git a/packages/color-picker/src/ColorPicker.md b/packages/color-picker/src/ColorPicker.md index b530c465..1b97255a 100644 --- a/packages/color-picker/src/ColorPicker.md +++ b/packages/color-picker/src/ColorPicker.md @@ -4,7 +4,7 @@ order: 2 group: path: / nav: - path: /components + path: /biz-components --- # ColorPicker 取色器 diff --git a/packages/color-picker/src/components/DraggableLabel/index.less b/packages/color-picker/src/components/DraggableLabel/index.less new file mode 100644 index 00000000..38edb77c --- /dev/null +++ b/packages/color-picker/src/components/DraggableLabel/index.less @@ -0,0 +1,12 @@ +.avx-color-fields-label { + width: 31px; + + line-height: 1; + text-align: center; + text-transform: capitalize; + cursor: ew-resize; + user-select: none; + &:active { + color: hsla(0, 0%, 0%, 0.65); + } +} diff --git a/packages/color-picker/src/components/DraggableLabel/index.tsx b/packages/color-picker/src/components/DraggableLabel/index.tsx new file mode 100644 index 00000000..c205eab0 --- /dev/null +++ b/packages/color-picker/src/components/DraggableLabel/index.tsx @@ -0,0 +1,61 @@ +import type { CSSProperties, FC } from 'react'; +import React, { memo, useEffect, useMemo } from 'react'; +import { fromEvent, Subject, BehaviorSubject } from 'rxjs'; +import { distinctUntilChanged, withLatestFrom, concatMap, takeUntil, map } from 'rxjs/operators'; +import cls from 'classnames'; + +import './index.less'; + +interface DraggableLabelProps { + text: string; + value?: number; + onChange?: (value: number) => void; + className?: string; + style?: CSSProperties; +} + +const DraggableLabel: FC = memo( + ({ text, onChange, value, className, style }) => { + const currentValue$ = useMemo(() => new BehaviorSubject(value), []); + const mouseDown$ = useMemo(() => new Subject(), []); + + useEffect(() => { + currentValue$.next(value); + }, [value]); + + useEffect(() => { + mouseDown$ + .pipe( + concatMap(() => + fromEvent(window, 'mousemove').pipe( + takeUntil(fromEvent(window, 'mouseup')), + ), + ), + map((e) => e.movementX), + withLatestFrom(currentValue$, (movementX, value) => Math.round(value + movementX)), + distinctUntilChanged(), + ) + .subscribe((value) => { + onChange?.(value); + }); + + return () => { + mouseDown$.unsubscribe(); + }; + }, []); + + return ( +
{ + mouseDown$.next(e); + }} + style={style} + > + {text} +
+ ); + }, +); + +export default DraggableLabel; diff --git a/packages/color-picker/src/components/EditableInput/index.less b/packages/color-picker/src/components/EditableInput/index.less new file mode 100644 index 00000000..c32cf79b --- /dev/null +++ b/packages/color-picker/src/components/EditableInput/index.less @@ -0,0 +1,8 @@ +.avx-color-fields-input { + width: 100%; + padding: 2px 3px; + font-size: 12px; + border: none; + border-radius: 4px; + box-shadow: inset 0 0 0 1px #ccc; +} diff --git a/packages/color-picker/src/components/EditableInput/index.tsx b/packages/color-picker/src/components/EditableInput/index.tsx new file mode 100644 index 00000000..83df17b1 --- /dev/null +++ b/packages/color-picker/src/components/EditableInput/index.tsx @@ -0,0 +1,30 @@ +import type { CSSProperties, FC } from 'react'; +import React from 'react'; +import cls from 'classnames'; + +import './index.less'; + +export interface EditableInputProps { + value: number | string; + + onChange: (value) => void; + className?: string; + style?: CSSProperties; +} + +const EditableInput: FC = ({ value, style, className, onChange }) => { + return ( + + ); +}; + +export default EditableInput; diff --git a/packages/color-picker/src/components/SketchFields.tsx b/packages/color-picker/src/components/SketchFields.tsx deleted file mode 100644 index d6cdc428..00000000 --- a/packages/color-picker/src/components/SketchFields.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* eslint-disable no-param-reassign */ - -import React from 'react'; -import reactCSS from 'reactcss'; -import * as color from '../helpers/color'; - -import { EditableInput } from './common'; -import { colorSelector, useStore } from '@arvinxu/color-picker/store'; -import isEqual from 'lodash/isEqual'; - -export const SketchFields: React.FC<{ - onChange?: (color: any, e: React.ChangeEvent) => void | undefined; - disableAlpha?: boolean; -}> = ({ onChange, disableAlpha }) => { - const { rgb, hsl, hex } = useStore(colorSelector, isEqual); - - const styles = reactCSS( - { - default: { - fields: { - display: 'flex', - paddingTop: '4px', - }, - single: { - flex: '1', - paddingLeft: '6px', - }, - alpha: { - flex: '1', - paddingLeft: '6px', - }, - double: { - flex: '2', - }, - input: { - width: '100%', - padding: '4px 10% 3px', - border: 'none', - borderRadius: '4px', - boxShadow: 'inset 0 0 0 1px #ccc', - fontSize: '12px', - }, - label: { - display: 'block', - textAlign: 'center', - fontSize: '10px', - color: '#222', - paddingTop: '3px', - paddingBottom: '4px', - textTransform: 'capitalize', - }, - }, - disableAlpha: { - alpha: { - display: 'none', - }, - } as any, - }, - { disableAlpha }, - ); - - const handleChange = (data: any, e: React.ChangeEvent) => { - if (data.hex) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - color.isValidHex(data.hex) && - onChange?.( - { - hex: data.hex, - source: 'hex', - }, - e, - ); - } else if (data.r || data.g || data.b) { - onChange?.( - { - r: data.r || rgb?.r, - g: data.g || rgb?.g, - b: data.b || rgb?.b, - a: rgb?.a, - source: 'rgb', - }, - e, - ); - } else if (data.a) { - if (data.a < 0) { - data.a = 0; - } else if (data.a > 100) { - data.a = 100; - } - - data.a /= 100; - onChange?.( - { - h: hsl?.h, - s: hsl?.s, - l: hsl?.l, - a: data.a, - source: 'rgb', - }, - e, - ); - } - }; - - return ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- ); -}; - -export default SketchFields; diff --git a/packages/color-picker/src/components/SketchFields/index.less b/packages/color-picker/src/components/SketchFields/index.less new file mode 100644 index 00000000..da715975 --- /dev/null +++ b/packages/color-picker/src/components/SketchFields/index.less @@ -0,0 +1,73 @@ +.avx-color-fields { + &-hex { + flex: 2; + width: 56px; + } + + &-input { + width: 31px; + } + + &-switcher { + position: relative; + padding: 2px 0; + border-radius: 4px; + + &:hover { + background: hsla(0, 0, 0, 0.04); + } + } + + &-switcher:hover &-select { + opacity: 1; + } + + &-select { + position: absolute; + top: 0; + left: 0px; + + display: flex; + align-items: center; + + width: 16px; + height: 100%; + padding: 0 2px; + + // + background: none; + + border: none; + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + cursor: pointer; + opacity: 0; + + &:hover { + background: hsla(0, 0, 0, 0.02); + } + + &:focus-visible { + outline-width: 0; + } + } + + &-label-ctn { + color: rgba(0, 0, 0, 0.45); + font-size: 12px; + } + + &-label { + &-hex { + width: 48px; + padding-left: 8px; + } + + &-alpha, + &-hex { + padding: 2px 0; + } + } +} diff --git a/packages/color-picker/src/components/SketchFields/index.tsx b/packages/color-picker/src/components/SketchFields/index.tsx new file mode 100644 index 00000000..18f0efb6 --- /dev/null +++ b/packages/color-picker/src/components/SketchFields/index.tsx @@ -0,0 +1,108 @@ +import type { FC } from 'react'; +import React, { memo } from 'react'; +import isEqual from 'lodash/isEqual'; +import { Flexbox } from '@arvinxu/layout-kit'; +import cls from 'classnames'; + +import type { ColorMode, ColorObj } from '../../store'; +import { colorSpaceSelector, useStore } from '../../store'; +import { isValidHex } from '../../helpers/color'; +import DraggableLabel from '../DraggableLabel'; +import EditableInput from '../EditableInput'; + +import './index.less'; + +export const SketchFields: FC = memo(() => { + const mode: ColorMode = useStore((s) => s.colorMode); + + const hex = useStore((s) => s.colorModel.hex('rgb')); + const alpha = useStore((s) => Math.round(s.colorModel.alpha() * 100)); + + const modeValue = useStore(colorSpaceSelector, isEqual) as ColorObj; + + const { updateAlpha, updateByHex, disableAlpha, updateColorMode, updateByColorSpace } = + useStore(); + + const prefixCls = 'avx-color-fields'; + + return ( + + +
+ { + if (isValidHex(str)) { + updateByHex(str); + } + }} + style={{ width: 56 }} + /> +
+ {mode.split('').map((dim) => { + return ( +
+ { + updateByColorSpace(dim, value); + }} + /> +
+ ); + })} + +
+ +
+
+ +
+ hex +
+ +
+ +
+ {mode.split('').map((dim, index) => ( + { + updateByColorSpace(dim, value); + }} + /> + ))} +
+ +
+
+ ); +}); + +export default SketchFields; diff --git a/packages/color-picker/src/components/common/EditableInput.js b/packages/color-picker/src/components/common/EditableInput.js index fc35abd5..b78b78fe 100644 --- a/packages/color-picker/src/components/common/EditableInput.js +++ b/packages/color-picker/src/components/common/EditableInput.js @@ -39,10 +39,6 @@ export class EditableInput extends (PureComponent || Component) { } } - componentWillUnmount() { - this.unbindEventListeners(); - } - getValueObjectWithLabel(value) { return { [this.props.label]: value, @@ -83,57 +79,22 @@ export class EditableInput extends (PureComponent || Component) { this.setState({ value }); } - handleDrag = (e) => { - if (this.props.dragLabel) { - const newValue = Math.round(this.props.value + e.movementX); - if (newValue >= 0 && newValue <= this.props.dragMax) { - this.props.onChange && this.props.onChange(this.getValueObjectWithLabel(newValue), e); - } - } - }; - - handleMouseDown = (e) => { - if (this.props.dragLabel) { - e.preventDefault(); - this.handleDrag(e); - window.addEventListener('mousemove', this.handleDrag); - window.addEventListener('mouseup', this.handleMouseUp); - } - }; - - handleMouseUp = () => { - this.unbindEventListeners(); - }; - - unbindEventListeners = () => { - window.removeEventListener('mousemove', this.handleDrag); - window.removeEventListener('mouseup', this.handleMouseUp); - }; - render() { - const styles = reactCSS( - { - default: { - wrap: { - position: 'relative', - }, - }, - 'user-override': { - wrap: this.props.style && this.props.style.wrap ? this.props.style.wrap : {}, - input: this.props.style && this.props.style.input ? this.props.style.input : {}, - label: this.props.style && this.props.style.label ? this.props.style.label : {}, + const styles = reactCSS({ + default: { + wrap: { + position: 'relative', }, - 'dragLabel-true': { - label: { - cursor: 'ew-resize', - }, + input: { + width: '100%', + padding: '2px 3px', + border: 'none', + borderRadius: '4px', + boxShadow: 'inset 0 0 0 1px #ccc', + fontSize: '12px', }, }, - { - 'user-override': true, - }, - this.props, - ); + }); return (
@@ -148,11 +109,6 @@ export class EditableInput extends (PureComponent || Component) { placeholder={this.props.placeholder} spellCheck="false" /> - {this.props.label && !this.props.hideLabel ? ( - - ) : null}
); } diff --git a/packages/color-picker/src/container/Sketch.tsx b/packages/color-picker/src/container/Sketch.tsx index ccb9fd05..26606a69 100644 --- a/packages/color-picker/src/container/Sketch.tsx +++ b/packages/color-picker/src/container/Sketch.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import React from 'react'; +import React, { memo } from 'react'; import reactCSS from 'reactcss'; import merge from 'lodash/merge'; import isEqual from 'lodash/isEqual'; @@ -12,10 +12,9 @@ import SketchPresetColors from '../components/SketchPresetColors'; import { colorSelector, useStore } from '../store'; import type { ColorPickerProps } from '../types'; -export const Sketch: FC = ({ onSwatchHover, className }) => { +export const Sketch: FC = memo(({ onSwatchHover, className }) => { const { presetColors, - onChange, styles: passedStyles, width, disableAlpha, @@ -25,7 +24,7 @@ export const Sketch: FC = ({ onSwatchHover, className }) => { updateByHsv, } = useStore(); - const { rgb, hsl, hsv, hex } = useStore(colorSelector, isEqual); + const { rgb, hsl, hsv } = useStore(colorSelector, isEqual); const styles: any = reactCSS( //@ts-ignore @@ -122,7 +121,7 @@ export const Sketch: FC = ({ onSwatchHover, className }) => { radius={6} /> -
+
= ({ onSwatchHover, className }) => { rgb={rgb} hsl={hsl} onChange={({ a }) => { - updateAlpha(a); + updateAlpha(a * 100); }} radius={4} /> @@ -152,7 +151,7 @@ export const Sketch: FC = ({ onSwatchHover, className }) => {
- + { @@ -162,6 +161,6 @@ export const Sketch: FC = ({ onSwatchHover, className }) => { />
); -}; +}); export default Sketch; diff --git a/packages/color-picker/src/spec.js b/packages/color-picker/src/spec.js index c6f110c8..fd9c8ffb 100644 --- a/packages/color-picker/src/spec.js +++ b/packages/color-picker/src/spec.js @@ -7,7 +7,7 @@ import * as color from './helpers/color'; // import canvas from 'canvas' import Sketch from './container/Sketch'; -import SketchFields from './components/SketchFields'; +import SketchFields from './components/SketchFields/SketchFields'; import SketchPresetColors from './components/SketchPresetColors'; import { Swatch } from './components/common'; diff --git a/packages/color-picker/src/store.ts b/packages/color-picker/src/store.ts index bb2bee9a..0f03c516 100644 --- a/packages/color-picker/src/store.ts +++ b/packages/color-picker/src/store.ts @@ -5,7 +5,8 @@ import chroma from 'chroma-js'; import type { HSLColor, HSVColor, RGBColor } from './types'; -type ColorMode = 'rgb' | 'hsl'; +export type ColorMode = 'rgb' | 'hsl' | 'hsv'; + interface ColorPickerState { disableAlpha: boolean; width: number; @@ -19,12 +20,26 @@ interface ColorPickerAction { internalUpdateColor: (color: Color) => void; updateAlpha: (a: number) => void; updateHue: (hue: number) => void; + updateColorMode: (mode: ColorMode) => void; + updateByColorSpace: (key: string, value: number) => void; updateByHex: (hex: string) => void; updateByHsv: (hsv: HSVColor) => void; + updateByRgb: (rgb: RGBColor) => void; } export type ColorPickerStore = ColorPickerState & ColorPickerAction; +const maxValueMap = { + h: 359, + s: 100, + v: 100, + l: 100, + a: 100, + r: 255, + g: 255, + b: 255, +}; + const initialState: ColorPickerState = { disableAlpha: false, width: 200, @@ -53,6 +68,11 @@ const initialState: ColorPickerState = { const createStore = () => create((set, get) => ({ ...initialState, + + updateColorMode: (mode) => { + set({ colorMode: mode }); + }, + internalUpdateColor: (color) => { set({ colorModel: color }); @@ -63,6 +83,13 @@ const createStore = () => updateAlpha: (a) => { const { colorModel, internalUpdateColor } = get(); + if (a < 0) { + a = 0; + } else if (a > 100) { + a = 100; + } + + a /= 100; internalUpdateColor(colorModel.alpha(a)); }, @@ -71,6 +98,24 @@ const createStore = () => internalUpdateColor(colorModel.set('hsl.h', h)); }, + updateByColorSpace: (key, value) => { + const { colorModel, internalUpdateColor, colorMode } = get(); + let v; + + // 将值限定在相应的最大区间内 + v = value > maxValueMap[key] ? maxValueMap[key] : value; + // 且确保 v 最小值为 0 + if (value <= 0) v = 1; + + // 如果是 s v l 三个维度值,将其转换为百分比 + + if (['s', 'v', 'l'].includes(key)) { + v /= 100; + } + + internalUpdateColor(colorModel.set(`${colorMode}.${key}`, v)); + }, + updateByHex: (hex) => { const { internalUpdateColor } = get(); @@ -83,20 +128,27 @@ const createStore = () => internalUpdateColor(chroma([h, s, v, a], 'hsv')); }, + + updateByRgb: (rgb) => { + const { r, g, b, a } = rgb; + const { internalUpdateColor } = get(); + + internalUpdateColor(chroma([r, g, b, a], 'rgb')); + }, })); const { Provider, useStore } = createContext(); export { Provider, useStore, createStore }; -export const colorSelector = ( - s: ColorPickerState, -): { +export interface ColorObj { rgb: RGBColor; hsl: HSLColor; hsv: HSVColor; hex: string; -} => { +} + +export const colorSelector = (s: ColorPickerState): ColorObj => { const [r, g, b, a] = s.colorModel.rgba(); const hsv = s.colorModel.hsv(); const hsl = s.colorModel.hsl(); @@ -108,3 +160,23 @@ export const colorSelector = ( hex: s.colorModel.hex(), }; }; + +export type SpaceColor = RGBColor | HSLColor | HSVColor; + +export const colorSpaceSelector = (s: ColorPickerState): SpaceColor => { + const [r, g, b, a] = s.colorModel.rgba(); + const hsv = s.colorModel.hsv(); + const hsl = s.colorModel.hsl(); + + switch (s.colorMode) { + case 'rgb': { + return { r, g, b, a }; + } + case 'hsv': { + return { h: hsv[0], s: hsv[1] * 100, v: hsv[2] * 100, a }; + } + case 'hsl': { + return { h: hsl[0], s: hsl[1] * 100, l: hsl[2] * 100, a }; + } + } +};