diff --git a/examples/color-picker/color-picker.md b/examples/color-picker/color-picker.md index 075fe2f069..22aa0a8728 100644 --- a/examples/color-picker/color-picker.md +++ b/examples/color-picker/color-picker.md @@ -5,20 +5,28 @@ 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- +closeBtn | String / Boolean / Slot / Function | true | 关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 或 `undefined` 则不显示关闭按钮;值类型为函数,则表示自定义关闭按钮。TS 类型:`string | boolean | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +colorModes | Array | ['monochrome', 'linear-gradient'] | 颜色模式选择。同时支持单色和渐变两种模式,可仅使用单色或者渐变其中一种模式,也可以同时使用。`monochrome` 表示单色,`linear-gradient` 表示渐变色。TS 类型:`Array<'monochrome' | 'linear-gradient'>` | N disabled | Boolean | false | 是否禁用组件 | N enableAlpha | Boolean | false | 是否开启透明通道 | N -format | String | RGB | 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA/HEX8` 等值有效。可选项:RGB/RGBA/HSL/HSLA/HSB/HSV/HSVA/HEX/HEX8/CMYK/CSS | N -multiple | Boolean | false | 是否允许选中多个颜色 | N -popupProps | Object | - | 透传 Popup 组件全部属性,如 `placement` `overlayStyle` `overlayClassName` 等。TS 类型:`PopupProps`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts) | N -swatchColors | Array | - | 颜色样例。TS 类型:`Array` | N +format | String | RGB | 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA` 等值有效。可选项:RGB/RGBA/HSL/HSLA/HSB/HSV/HSVA/HEX/CMYK/CSS | N +inputProps | Object | - | 透传 Input 输入框组件全部属性。TS 类型:`InputProps`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts) | N +multiple | Boolean | false | 【开发中】是否允许选中多个颜色 | N +popupProps | Object | - | 透传 Popup 组件全部属性,如 `placement` `overlayStyle` `overlayClassName` `trigger`等。TS 类型:`PopupProps`,[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts) | N +recentColors | Array | [] | 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 null 则完全不显示“最近使用颜色”。支持语法糖 `v-model:recentColors`。TS 类型:`boolean | Array` | N +defaultRecentColors | Array | [] | 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 null 则完全不显示“最近使用颜色”。非受控属性。TS 类型:`boolean | Array` | N +selectInputProps | Object | - | 透传 SelectInputProps 筛选器输入框组件全部属性。TS 类型:`SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts) | N +swatchColors | Array | - | 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色。TS 类型:`Array` | N value | String | - | 色值。支持语法糖 `v-model` 或 `v-model:value` | N defaultValue | String | - | 色值。非受控属性 | N -onChange | Function | | TS 类型:`(value: string, context: { colors: string[]; trigger: ColorPickerChangeTrigger }) => void`
选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.colors` 表示当前调色板的所有色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette' | 'input'`
| N -onPaletteChange | Function | | TS 类型:`(context: { colors: string[] }) => void`
调色板变化时触发,当前色板的色值 | N +onChange | Function | | TS 类型:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' | 'palette-saturation' | 'palette-brightness' | 'palette-hue-bar' | 'palette-alpha-bar' | 'input'`
| N +onPaletteBarChange | Function | | TS 类型:`(context: { color: ColorObject }) => void`
调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
| N +onRecentColorsChange | Function | | TS 类型:`(value: Array, context: { trigger: RecentColorsChangeTrigger }) => void`
最近使用颜色发生变化时触发。第一个参数 `value` 表示变化后的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`type RecentColorsChangeTrigger = 'delete' | 'clear' | 'input' | 'palette-saturation-brightness' | 'palette-saturation' | 'palette-brightness' | 'palette-hue-bar' | 'palette-alpha-bar'`
| N ### ColorPicker Events 名称 | 参数 | 描述 -- | -- | -- -change | `(value: string, context: { colors: string[]; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.colors` 表示当前调色板的所有色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette' | 'input'`
-palette-change | `(context: { colors: string[] })` | 调色板变化时触发,当前色板的色值 +change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' | 'palette-saturation' | 'palette-brightness' | 'palette-hue-bar' | 'palette-alpha-bar' | 'input'`
+palette-bar-change | `(context: { color: ColorObject })` | 调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
+recent-colors-change | `(value: Array, context: { trigger: RecentColorsChangeTrigger })` | 最近使用颜色发生变化时触发。第一个参数 `value` 表示变化后的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/color-picker/type.ts)。
`type RecentColorsChangeTrigger = 'delete' | 'clear' | 'input' | 'palette-saturation-brightness' | 'palette-saturation' | 'palette-brightness' | 'palette-hue-bar' | 'palette-alpha-bar'`
diff --git a/examples/color-picker/demos/color-mode.vue b/examples/color-picker/demos/color-mode.vue new file mode 100644 index 0000000000..f5512c1f14 --- /dev/null +++ b/examples/color-picker/demos/color-mode.vue @@ -0,0 +1,35 @@ + + + diff --git a/examples/color-picker/demos/enable-alpha.vue b/examples/color-picker/demos/enable-alpha.vue new file mode 100644 index 0000000000..3a0ad8283b --- /dev/null +++ b/examples/color-picker/demos/enable-alpha.vue @@ -0,0 +1,13 @@ + + diff --git a/examples/color-picker/demos/panel.vue b/examples/color-picker/demos/panel.vue new file mode 100644 index 0000000000..3b33cc1395 --- /dev/null +++ b/examples/color-picker/demos/panel.vue @@ -0,0 +1,26 @@ + + diff --git a/examples/color-picker/demos/recent-color.vue b/examples/color-picker/demos/recent-color.vue new file mode 100644 index 0000000000..2e647e0a29 --- /dev/null +++ b/examples/color-picker/demos/recent-color.vue @@ -0,0 +1,37 @@ + + + diff --git a/examples/color-picker/demos/status-disabled.vue b/examples/color-picker/demos/status-disabled.vue new file mode 100644 index 0000000000..c6f2886940 --- /dev/null +++ b/examples/color-picker/demos/status-disabled.vue @@ -0,0 +1,10 @@ + + diff --git a/examples/color-picker/demos/status-readonly.vue b/examples/color-picker/demos/status-readonly.vue new file mode 100644 index 0000000000..17e0560b54 --- /dev/null +++ b/examples/color-picker/demos/status-readonly.vue @@ -0,0 +1,10 @@ + + diff --git a/examples/color-picker/demos/swatch-color.vue b/examples/color-picker/demos/swatch-color.vue new file mode 100644 index 0000000000..2241a300f0 --- /dev/null +++ b/examples/color-picker/demos/swatch-color.vue @@ -0,0 +1,26 @@ + + + diff --git a/examples/color-picker/demos/trigger.vue b/examples/color-picker/demos/trigger.vue new file mode 100644 index 0000000000..a251eae97b --- /dev/null +++ b/examples/color-picker/demos/trigger.vue @@ -0,0 +1,17 @@ + + diff --git a/examples/config-provider/config-provider.md b/examples/config-provider/config-provider.md index 29dc11d13c..0a5e16180c 100644 --- a/examples/config-provider/config-provider.md +++ b/examples/config-provider/config-provider.md @@ -11,6 +11,7 @@ animation | Object | `{ include: ['ripple','expand','fade'], exclude: [] }` | calendar | Object | - | 日历组件全局配置。TS 类型:`CalendarConfig` | N cascader | Object | - | 级联选择器全局配置。TS 类型:`CascaderConfig` | N classPrefix | String | t | CSS 类名前缀 | N +colorPicker | Object | - | 颜色选择器全局配置。TS 类型:`ColorPickerConfig` | N datePicker | Object | - | 日期选择器全局配置。TS 类型:`DatePickerConfig` | N dialog | Object | - | 对话框全局配置。TS 类型:`DialogConfig` | N drawer | Object | - | 抽屉全局配置。TS 类型:`DrawerConfig` | N @@ -70,6 +71,14 @@ empty | String | '暂无数据' | 空数据文本,示例:'empty data' | N loadingText | String | '加载中' | “加载中”描述文本 | N placeholder | String | '请选择' | 选择器占位文本,示例:'select time' | N +### ColorPickerConfig + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +clearConfirmText | String | '确定清空最近使用的颜色吗?' | 清空颜色确认文案,示例:'确定清空最近使用的颜色吗?' | N +recentColorTitle | String | '最近使用颜色' | 最近使用颜色区域标题文本,示例:'最近使用颜色' | N +swatchColorTitle | String | '系统预设颜色' | 系统预设颜色区域标题文本,示例:'系统预设颜色' | N + ### TransferConfig 名称 | 类型 | 默认值 | 说明 | 必传 @@ -192,7 +201,7 @@ dragger | Object | - | 语言配置,拖拽相关。示例:{ dragDropText: ' file | Object | - | 语言配置,文件信息相关。示例:{ fileNameText: '文件名', fileSizeText: '文件尺寸', fileStatusText: '状态', fileOperationText: '操作', fileOperationDateText: '上传日期' }。TS 类型:`UploadConfigFileList` | N progress | Object | - | 语言配置,上传进度相关。示例:{ uploadText: '上传中', waitingText: '待上传', 'failText': '上传失败', successText: '上传成功' }。TS 类型:`UploadConfigProgress` | N sizeLimitMessage | String | '文件大小不能超过 {sizeLimit}' | 语言配置,文件大小超出限制时提醒文本 | N -triggerUploadText | Object | - | 语言配置,上传功能触发文案。示例:{ image: '点击上传图片', normal: '点击上传', fileInput: '选择文件',reupload: '重新上传',fileInput: '删除' }。TS 类型:`UploadTriggerUploadText` `interface UploadTriggerUploadText { image?: string, normal?: string, fileInput?: string, reupload?: string, delete?: string }`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/config-provider/type.ts) | N +triggerUploadText | Object | - | 语言配置,上传功能触发文案。示例:{ image: '点击上传图片', normal: '点击上传', fileInput: '选择文件',reupload: '重新上传',fileInput: '删除' }。TS 类型:`UploadTriggerUploadText` `interface UploadTriggerUploadText { image?: string, normal?: string, fileInput?: string, reupload?: string, continueUpload: string, delete?: string }`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/config-provider/type.ts) | N ### UploadConfigProgress diff --git a/package.json b/package.json index d4bd22cada..484adb74e1 100644 --- a/package.json +++ b/package.json @@ -74,10 +74,12 @@ "@babel/runtime": "^7.14.8", "@popperjs/core": "^2.9.3", "@types/lodash": "^4.14.175", + "@types/tinycolor2": "^1.4.3", "@types/validator": "^13.6.3", "dayjs": "^1.9.7", "lodash": "^4.17.15", "tdesign-icons-vue-next": "~0.0.7", + "tinycolor2": "^1.4.2", "validator": "^13.5.1" }, "peerDependencies": { diff --git a/site/site.config.mjs b/site/site.config.mjs index db0e535993..051b9f8932 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -191,6 +191,13 @@ export default { path: '/vue-next/components/checkbox', component: () => import('@/examples/checkbox/checkbox.md'), }, + { + title: 'ColorPicker 颜色选择器', + name: 'color-picker', + docType: 'form', + path: '/vue-next/components/color-picker', + component: () => import('@/examples/color-picker/color-picker.md'), + }, { title: 'DatePicker 日期选择器', name: 'date-picker', diff --git a/src/_common b/src/_common index 97e6317813..5d6a68a1d7 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 97e63178134dc75cb360b9e10c0a3a6822e83b0e +Subproject commit 5d6a68a1d7c727089f466f422bef1fa5b1fe26a1 diff --git a/src/color-picker/color-picker-panel.tsx b/src/color-picker/color-picker-panel.tsx new file mode 100644 index 0000000000..c53bf7f237 --- /dev/null +++ b/src/color-picker/color-picker-panel.tsx @@ -0,0 +1,41 @@ +import { defineComponent, toRefs } from 'vue'; +import useVModel from '../hooks/useVModel'; +import props from './props'; +import ColorPanel from './panel'; +import { TdColorContext } from './interfaces'; +import { usePrefixClass } from '../index'; + +export default defineComponent({ + name: 'TColorPickerPanel', + components: { + ColorPanel, + }, + inheritAttrs: false, + props: { + ...props, + }, + setup(props) { + const prefix = usePrefixClass(); + const { value, modelValue } = toRefs(props); + const [innerValue, setInnerValue] = useVModel(value, modelValue, props.defaultValue, props.onChange); + + const handleChange = (value: string, context: TdColorContext) => { + setInnerValue(value, context); + }; + + const handlePaletteChange = (context: TdColorContext) => { + props.onPaletteBarChange(context); + }; + + return { + innerValue, + prefix, + handleChange, + handlePaletteChange, + }; + }, + render() { + const { prefix } = this; + return ; + }, +}); diff --git a/src/color-picker/color-picker.tsx b/src/color-picker/color-picker.tsx new file mode 100644 index 0000000000..13beb30dd2 --- /dev/null +++ b/src/color-picker/color-picker.tsx @@ -0,0 +1,97 @@ +import { ComponentPublicInstance, defineComponent, onBeforeUnmount, onMounted, ref, toRefs } from 'vue'; +import useVModel from '../hooks/useVModel'; +import { renderTNodeJSXDefault } from '../utils/render-tnode'; +import props from './props'; +import { Popup as TPopup } from '../popup'; +import { useClickOutsider } from './utils/click-outsider'; +import ColorPanel from './panel'; +import DefaultTrigger from './trigger'; +import { TdColorContext } from './interfaces'; +import { useBaseClassName } from './hooks'; + +export default defineComponent({ + name: 'TColorPicker', + components: { + TPopup, + ColorPanel, + DefaultTrigger, + }, + inheritAttrs: false, + props: { + ...props, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const visible = ref(false); + const setVisible = (value: boolean) => (visible.value = value); + + const { value: inputValue, modelValue } = toRefs(props); + const [innerValue, setInnerValue] = useVModel(inputValue, modelValue, props.defaultValue, props.onChange); + + const refTrigger = ref(); + const refColorPanel = ref(); + + const { addClickOutsider, removeClickOutsider } = useClickOutsider(); + onMounted(() => addClickOutsider([refTrigger.value, refColorPanel.value], () => setVisible(false))); + onBeforeUnmount(() => { + removeClickOutsider(); + }); + + const renderPopupContent = () => { + if (props.disabled) { + return null; + } + return ( + setInnerValue(value, context)} + ref="refColorPanel" + /> + ); + }; + + return { + baseClassName, + innerValue, + visible, + refTrigger, + refColorPanel, + renderPopupContent, + setVisible, + setInnerValue, + }; + }, + render() { + const { popupProps, disabled, baseClassName } = this; + const popProps = { + placement: 'bottom-left', + ...((popupProps as any) || {}), + trigger: 'click', + attach: 'body', + overlayClassName: [baseClassName], + visible: this.visible, + overlayStyle: { + padding: 0, + }, + }; + return ( + +
this.setVisible(!this.visible)} ref="refTrigger"> + {renderTNodeJSXDefault( + this, + 'default', + , + )} +
+
+ ); + }, +}); diff --git a/src/color-picker/const.ts b/src/color-picker/const.ts new file mode 100644 index 0000000000..5b542e9389 --- /dev/null +++ b/src/color-picker/const.ts @@ -0,0 +1,71 @@ +import { TdColorPickerProps } from '.'; + +/** 常量 */ + +// 最近使用颜色最大个数 +export const TD_COLOR_USED_COLORS_MAX_SIZE = 100; // 每行10个 + +// 颜色模式options配置 +export const COLOR_MODES = { + monochrome: '单色', + 'linear-gradient': '渐变', +}; + +// 非透明色格式化类型 +export const FORMATS: TdColorPickerProps['format'][] = ['HEX', 'RGB', 'HSL', 'HSV', 'CMYK', 'CSS']; + +// 默认颜色 +export const DEFAULT_COLOR = '#001F97'; + +// 默认渐变色 +export const DEFAULT_LINEAR_GRADIENT = 'linear-gradient(90deg, rgba(241,29,0,1) 0%, rgba(73,106,220,1) 100%);'; + +// 默认系统色彩 +export const DEFAULT_SYSTEM_SWATCH_COLORS = [ + '#ECF2FE', + '#D4E3FC', + '#BBD3FB', + '#96BBF8', + '#699EF5', + '#4787F0', + '#266FE8', + '#0052D9', + '#0034B5', + '#001F97', + '#FDECEE', + '#F9D7D9', + '#F8B9BE', + '#F78D94', + '#F36D78', + '#E34D59', + '#C9353F', + '#B11F26', + '#951114', + '#680506', + '#FEF3E6', + '#F9E0C7', + '#F7C797', + '#F2995F', + '#ED7B2F', + '#D35A21', + '#BA431B', + '#9E3610', + '#842B0B', + '#5A1907', + '#E8F8F2', + '#BCEBDC', + '#85DBBE', + '#48C79C', + '#00A870', + '#078D5C', + '#067945', + '#056334', + '#044F2A', + '#033017', +]; + +// saturation-panel default rect +export const SATURATION_PANEL_DEFAULT_WIDTH = 248; +export const SATURATION_PANEL_DEFAULT_HEIGHT = 140; +export const SLIDER_DEFAULT_WIDTH = 248; +export const GRADIENT_SLIDER_DEFAULT_WIDTH = 190; diff --git a/src/color-picker/hooks.ts b/src/color-picker/hooks.ts new file mode 100644 index 0000000000..3277d3e44e --- /dev/null +++ b/src/color-picker/hooks.ts @@ -0,0 +1,14 @@ +import { computed } from 'vue'; +import { usePrefixClass } from '../index'; + +const BASE_COMPONENT_NAME = 'color-picker'; + +/** + * 基础样式 + * @param className + * @returns + */ +export const useBaseClassName = (className?: string) => { + const baseClassName = usePrefixClass(BASE_COMPONENT_NAME); + return computed(() => (className ? `${baseClassName.value}-${className}` : baseClassName.value)); +}; diff --git a/src/color-picker/index.ts b/src/color-picker/index.ts new file mode 100644 index 0000000000..ce8136b04d --- /dev/null +++ b/src/color-picker/index.ts @@ -0,0 +1,12 @@ +import _ColorPickerPanel from './color-picker-panel'; +import _ColorPicker from './color-picker'; +import { withInstall, WithInstallType } from '../utils/withInstall'; +import { TdColorPickerProps } from './type'; + +import './style'; + +export * from './type'; +export type ColorPickerProps = TdColorPickerProps; + +export const ColorPickerPanel: WithInstallType = withInstall(_ColorPickerPanel); +export const ColorPicker: WithInstallType = withInstall(_ColorPicker); diff --git a/src/color-picker/interfaces.ts b/src/color-picker/interfaces.ts new file mode 100644 index 0000000000..40aab07247 --- /dev/null +++ b/src/color-picker/interfaces.ts @@ -0,0 +1,10 @@ +import { ColorObject, ColorPickerChangeTrigger } from '.'; + +// color modes +export type TdColorModes = 'monochrome' | 'linear-gradient'; + +// color context +export interface TdColorContext { + color: ColorObject; + trigger: ColorPickerChangeTrigger; +} diff --git a/src/color-picker/panel/alpha.tsx b/src/color-picker/panel/alpha.tsx new file mode 100644 index 0000000000..be025fd69d --- /dev/null +++ b/src/color-picker/panel/alpha.tsx @@ -0,0 +1,57 @@ +import { computed, defineComponent, PropType } from 'vue'; +import ColorSlider from './slider'; +import Color from '../utils/color'; +import { useBaseClassName } from '../hooks'; + +export default defineComponent({ + name: 'AlphaSlider', + components: { + ColorSlider, + }, + inheritAttrs: false, + props: { + color: { + type: Object as PropType, + }, + disabled: { + type: Boolean, + default: false, + }, + onChange: { + type: Function, + default: () => { + return () => {}; + }, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const handleChange = (v: number, isDragEnd?: boolean) => { + props.onChange(v / 100, isDragEnd); + }; + const railStyle = computed(() => { + return { + background: `linear-gradient(to right, rgba(0, 0, 0, 0), ${props.color.rgb})`, + }; + }); + return { + baseClassName, + railStyle, + handleChange, + }; + }, + render() { + const { baseClassName } = this; + return ( + + ); + }, +}); diff --git a/src/color-picker/panel/base-props.ts b/src/color-picker/panel/base-props.ts new file mode 100644 index 0000000000..da9ae7b1e4 --- /dev/null +++ b/src/color-picker/panel/base-props.ts @@ -0,0 +1,15 @@ +import { PropType } from 'vue'; +import Color from '../utils/color'; + +export default { + /** 是否禁用组件 */ + disabled: Boolean, + /** Color Instance */ + color: { + type: Object as PropType, + }, + onChange: { + type: Function, + default: () => () => {}, + }, +}; diff --git a/src/color-picker/panel/format/config.ts b/src/color-picker/panel/format/config.ts new file mode 100644 index 0000000000..63c31e9cbf --- /dev/null +++ b/src/color-picker/panel/format/config.ts @@ -0,0 +1,125 @@ +import { TdColorPickerProps } from '../../type'; + +export interface FormatInput { + key: string; + type: 'input'; + flex?: number; +} + +export interface FormatInputNumber { + key: string; + min: number; + max: number; + type: 'inputNumber'; + flex?: number; + format?: Function; +} + +export type FormatInputsConfig = { + [propName in TdColorPickerProps['format']]?: Array; +}; + +export const FORMAT_INPUT_CONFIG: FormatInputsConfig = { + RGB: [ + { + key: 'r', + min: 0, + max: 255, + type: 'inputNumber', + }, + { + key: 'g', + min: 0, + max: 255, + type: 'inputNumber', + }, + { + key: 'b', + min: 0, + max: 255, + type: 'inputNumber', + }, + ], + HSV: [ + { + key: 'h', + min: 0, + max: 360, + type: 'inputNumber', + }, + { + key: 's', + min: 0, + max: 100, + type: 'inputNumber', + }, + { + key: 'v', + min: 0, + max: 100, + type: 'inputNumber', + }, + ], + HSL: [ + { + key: 'h', + min: 0, + max: 360, + type: 'inputNumber', + }, + { + key: 's', + min: 0, + max: 100, + type: 'inputNumber', + }, + { + key: 'l', + min: 0, + max: 100, + type: 'inputNumber', + }, + ], + CMYK: [ + { + key: 'c', + min: 0, + max: 255, + type: 'inputNumber', + }, + { + key: 'm', + min: 0, + max: 255, + type: 'inputNumber', + }, + { + key: 'y', + min: 0, + max: 255, + type: 'inputNumber', + }, + { + key: 'k', + min: 0, + max: 255, + type: 'inputNumber', + }, + ], + CSS: [ + { + key: 'css', + type: 'input', + flex: 3, + }, + ], + HEX: [ + { + key: 'hex', + type: 'input', + flex: 3, + }, + ], +}; + +export default FORMAT_INPUT_CONFIG; diff --git a/src/color-picker/panel/format/index.tsx b/src/color-picker/panel/format/index.tsx new file mode 100644 index 0000000000..7b1da15107 --- /dev/null +++ b/src/color-picker/panel/format/index.tsx @@ -0,0 +1,94 @@ +import { defineComponent, PropType, ref, watch } from 'vue'; +import { upperCase } from 'lodash'; +import { TdColorPickerProps } from '../../type'; +import props from '../../props'; +import { FORMATS } from '../../const'; +import Color from '../../utils/color'; +import { Select as TSelect, Option as TOption } from '../../../select'; +import { Input as TInput } from '../../../input'; +import FormatInputs from './inputs'; +import { useBaseClassName } from '../../hooks'; + +export default defineComponent({ + name: 'FormatPanel', + components: { + TSelect, + TInput, + TOption, + FormatInputs, + }, + inheritAttrs: false, + props: { + ...props, + color: { + type: Object as PropType, + }, + onModeChange: { + type: Function, + default: () => { + return () => {}; + }, + }, + onInputChange: { + type: Function, + default: () => { + return () => {}; + }, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const formatModel = ref(props.format); + watch( + () => [props.format], + () => (formatModel.value = props.format), + ); + + /** + * 格式化类型改变触发 + * @param v + */ + const handleModeChange = (v: TdColorPickerProps['format']) => { + formatModel.value = v; + props.onModeChange(v); + }; + + return { + formatModel, + baseClassName, + handleModeChange, + }; + }, + render() { + const formats: TdColorPickerProps['format'][] = [...FORMATS]; + const { baseClassName, handleModeChange } = this; + const newProps = { + ...this.$props, + format: this.formatModel, + }; + const selectInputProps = { + ...((this.selectInputProps as Object) || {}), + }; + return ( +
+
+ + {formats.map((item) => ( + + ))} + +
+
+ +
+
+ ); + }, +}); diff --git a/src/color-picker/panel/format/inputs.tsx b/src/color-picker/panel/format/inputs.tsx new file mode 100644 index 0000000000..84c6f2f09e --- /dev/null +++ b/src/color-picker/panel/format/inputs.tsx @@ -0,0 +1,164 @@ +import { computed, defineComponent, PropType, reactive, watch } from 'vue'; +import { throttle } from 'lodash'; +import props from '../../props'; +import Color from '../../utils/color'; +import { Select as TSelect, Option as TOption } from '../../../select'; +import TInput from '../../../input'; +import TInputNumber from '../../../input-number'; +import { FORMAT_INPUT_CONFIG } from './config'; + +export default defineComponent({ + name: 'FormatInputs', + components: { + TSelect, + TOption, + TInput, + TInputNumber, + }, + inheritAttrs: false, + props: { + ...props, + color: { + type: Object as PropType, + }, + onInputChange: { + type: Function, + default: () => { + return () => {}; + }, + }, + }, + setup(props) { + const inputConfigs = computed(() => { + const configs = [...FORMAT_INPUT_CONFIG[props.format]]; + if (props.enableAlpha) { + configs.push({ + type: 'inputNumber', + key: 'a', + min: 0, + max: 100, + format: (value: number) => `${value}%`, + flex: 1.15, + }); + } + return configs; + }); + + const modelValue = reactive({}); + const lastModelValue = reactive({}); + + /** + * 获取不同格式的输入输出值 + * @param type 'encode' | 'decode' + * @returns + */ + const getFormatColorMap = (type: 'encode' | 'decode') => { + const { color } = props; + if (type === 'encode') { + return { + HSV: color.getHsva(), + HSL: color.getHsla(), + RGB: color.getRgba(), + CMYK: color.getCmyk(), + CSS: { + css: color.css, + }, + HEX: { + hex: color.hex, + }, + }; + } + // decode + return { + HSV: Color.object2color(modelValue, 'HSV'), + HSL: Color.object2color(modelValue, 'HSL'), + RGB: Color.object2color(modelValue, 'RGB'), + CMYK: Color.object2color(modelValue, 'CMYK'), + CSS: modelValue.css, + HEX: modelValue.hex, + }; + }; + + // 更新modelValue + const updateModelValue = () => { + const { format, color } = props; + const values: any = getFormatColorMap('encode')[format]; + values.a = Math.round(color.alpha * 100); + Object.keys(values).forEach((key) => { + modelValue[key] = values[key]; + lastModelValue[key] = values[key]; + }); + }; + + updateModelValue(); + + const throttleUpdate = throttle(updateModelValue, 100); + + watch(() => { + const { saturation, hue, value, alpha } = props.color; + return [saturation, hue, value, alpha, props.format]; + }, throttleUpdate); + + const handleChange = (key: string, v: number | string) => { + if (v === lastModelValue[key]) { + return; + } + const value = getFormatColorMap('decode')[props.format]; + props.onInputChange(value, modelValue.a / 100, key, v); + }; + + return { + modelValue, + inputConfigs, + handleChange, + }; + }, + render() { + const inputProps = { + ...((this.inputProps as any) || {}), + }; + return ( +
+ {this.inputConfigs.map((config) => { + return ( +
+ {config.type === 'input' ? ( + this.handleChange(config.key, v)} + onEnter={(v: string) => this.handleChange(config.key, v)} + /> + ) : ( + this.handleChange(config.key, v)} + onEnter={(v: number) => this.handleChange(config.key, v)} + /> + )} +
+ ); + })} +
+ ); + }, +}); diff --git a/src/color-picker/panel/header.tsx b/src/color-picker/panel/header.tsx new file mode 100644 index 0000000000..f21c809cce --- /dev/null +++ b/src/color-picker/panel/header.tsx @@ -0,0 +1,84 @@ +import { defineComponent, PropType, ref, watch } from 'vue'; +import { CloseIcon } from 'tdesign-icons-vue-next'; +import props from '../props'; +import { COLOR_MODES } from '../const'; +import { RadioGroup as TRadioGroup, RadioButton as TRadioButton } from '../../radio'; +import { TdColorModes } from '../interfaces'; +import { useBaseClassName } from '../hooks'; + +export default defineComponent({ + name: 'PanelHeader', + components: { + CloseIcon, + TRadioGroup, + TRadioButton, + }, + props: { + ...props, + mode: { + type: String as PropType, + default: 'color', + }, + togglePopup: { + type: Function, + }, + onModeChange: { + type: Function, + default: () => { + return () => {}; + }, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const modeValue = ref(props.mode); + const handleClosePopup = () => { + props.togglePopup?.(false); + }; + const handleModeChange = (v: string) => props.onModeChange(v); + watch( + () => props.mode, + (v) => (modeValue.value = v), + ); + return { + baseClassName, + modeValue, + handleModeChange, + handleClosePopup, + }; + }, + render() { + const { baseClassName } = this; + return ( +
+
+ {this.colorModes?.length === 1 ? ( + COLOR_MODES[this.colorModes[0]] + ) : ( + + {Object.keys(COLOR_MODES).map((key) => ( + + {COLOR_MODES[key]} + + ))} + + )} +
+ {this.closeBtn ? ( + + + + ) : null} +
+ ); + }, +}); diff --git a/src/color-picker/panel/hue.tsx b/src/color-picker/panel/hue.tsx new file mode 100644 index 0000000000..369609dd16 --- /dev/null +++ b/src/color-picker/panel/hue.tsx @@ -0,0 +1,32 @@ +import { defineComponent } from 'vue'; +import ColorSlider from './slider'; +import { useBaseClassName } from '../hooks'; +import baseProps from './base-props'; + +export default defineComponent({ + name: 'HueSlider', + components: { + ColorSlider, + }, + inheritAttrs: false, + props: { + ...baseProps, + }, + setup() { + const baseClassName = useBaseClassName(); + return { + baseClassName, + }; + }, + render() { + return ( + + ); + }, +}); diff --git a/src/color-picker/panel/index.tsx b/src/color-picker/panel/index.tsx new file mode 100644 index 0000000000..352421a209 --- /dev/null +++ b/src/color-picker/panel/index.tsx @@ -0,0 +1,378 @@ +import { defineComponent, ref, toRefs, watch } from 'vue'; +import { useCommonClassName, useConfig } from '../../config-provider'; +import props from '../props'; +import { + DEFAULT_COLOR, + DEFAULT_LINEAR_GRADIENT, + TD_COLOR_USED_COLORS_MAX_SIZE, + DEFAULT_SYSTEM_SWATCH_COLORS, +} from '../const'; +import PanelHeader from './header'; +import LinearGradient from './linear-gradient'; +import SaturationPanel from './saturation'; +import HueSlider from './hue'; +import AlphaSlider from './alpha'; +import FormatPanel from './format'; +import SwatchesPanel from './swatches'; +import Color, { getColorObject } from '../utils/color'; +import { GradientColorPoint } from '../utils/gradient'; +import { TdColorPickerProps, ColorPickerChangeTrigger, RecentColorsChangeTrigger } from '../index'; +import { TdColorModes } from '../interfaces'; +import { useBaseClassName } from '../hooks'; +import useVModel from '../../hooks/useVModel'; +import useDefaultValue from '../../hooks/useDefaultValue'; + +export default defineComponent({ + name: 'ColorPanel', + components: { + PanelHeader, + LinearGradient, + SaturationPanel, + HueSlider, + AlphaSlider, + FormatPanel, + SwatchesPanel, + }, + props: { + ...props, + togglePopup: { + type: Function, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const { STATUS } = useCommonClassName(); + const { t, global } = useConfig('colorPicker'); + const statusClassNames = STATUS.value; + const { value: inputValue, modelValue, recentColors } = toRefs(props); + const [innerValue, setInnerValue] = useVModel(inputValue, modelValue, props.defaultValue, props.onChange); + const color = ref(new Color(innerValue.value || DEFAULT_COLOR)); + const updateColor = () => color.value.update(innerValue.value || DEFAULT_COLOR); + const mode = ref(color.value.isGradient ? 'linear-gradient' : 'monochrome'); + const formatModel = ref(color.value.isGradient ? 'CSS' : 'RGB'); + + const [recentlyUsedColors, setRecentlyUsedColors] = useDefaultValue( + recentColors, + props.defaultRecentColors, + props.onRecentColorsChange, + 'recentColors', + ); + + if (props.colorModes.length === 1) { + // eslint-disable-next-line vue/no-setup-props-destructure + const m = props.colorModes[0]; + mode.value = m; + } + + const formatValue = () => { + // 渐变模式下直接输出css样式 + if (mode.value === 'linear-gradient') { + return color.value.linearGradient; + } + return color.value.getFormatsColorMap()[props.format] || color.value.css; + }; + + /** + * 添加最近使用颜色 + * @param value + */ + const addRecentlyUsedColor = (value: string, trigger?: RecentColorsChangeTrigger) => { + if (recentlyUsedColors.value === null || recentlyUsedColors.value === false) { + return; + } + const colors = (recentlyUsedColors.value as string[]) || []; + const currentColor = value || color.value.rgba; + const index = colors.indexOf(currentColor); + if (index > -1) { + colors.splice(index, 1); + } + colors.unshift(currentColor); + if (colors.length > TD_COLOR_USED_COLORS_MAX_SIZE) { + colors.length = TD_COLOR_USED_COLORS_MAX_SIZE; + } + handleRecentlyUsedColorsChange(colors, trigger || 'palette-saturation-brightness'); + }; + + /** + * 最近使用颜色变更时触发 + * @param colors + * @param trigger + */ + const handleRecentlyUsedColorsChange = (colors: string[], trigger?: RecentColorsChangeTrigger) => { + recentlyUsedColors.value = colors; + setRecentlyUsedColors(colors, { + trigger, + }); + }; + + /** + * onChange + * @param trigger + * @param addUsedColor + */ + const emitColorChange = (trigger?: ColorPickerChangeTrigger, addUsedColor?: boolean) => { + setInnerValue(formatValue(), { + color: getColorObject(color.value), + trigger: trigger || 'palette-saturation-brightness', + }); + if (addUsedColor) { + if (color.value.isGradient) { + addRecentlyUsedColor(color.value.linearGradient, trigger); + } else { + addRecentlyUsedColor(props.enableAlpha ? color.value.rgba : color.value.rgb, trigger); + } + } + }; + + watch(() => [props.defaultValue, props.enableAlpha], updateColor); + + watch( + () => innerValue.value, + (newColor) => { + if (newColor !== formatValue()) { + updateColor(); + mode.value = color.value.isGradient ? 'linear-gradient' : 'monochrome'; + } + }, + ); + + /** + * mode change + * @param value + * @returns + */ + const handleModeChange = (value: TdColorModes) => { + mode.value = value; + if (value === 'linear-gradient') { + color.value.update( + color.value.gradientColors.length > 0 ? color.value.linearGradient : DEFAULT_LINEAR_GRADIENT, + ); + return; + } + color.value.update(color.value.rgba); + }; + + /** + * 格式变化 + * @param format + * @returns + */ + const handleFormatModeChange = (format: TdColorPickerProps['format']) => (formatModel.value = format); + + /** + * 饱和度亮度变化 + * @param param0 + */ + const handleSatAndValueChange = ({ + saturation, + value, + addUsedColor, + }: { + saturation: number; + value: number; + addUsedColor?: boolean; + }) => { + const { saturation: sat, value: val } = color.value; + let changeTrigger: ColorPickerChangeTrigger = 'palette-saturation-brightness'; + if (value !== val && saturation !== sat) { + color.value.saturation = saturation; + color.value.value = value; + changeTrigger = 'palette-saturation-brightness'; + } else if (saturation !== sat) { + color.value.saturation = saturation; + changeTrigger = 'palette-saturation'; + } else if (value !== val) { + color.value.value = value; + changeTrigger = 'palette-brightness'; + } else { + return; + } + emitColorChange(changeTrigger, addUsedColor); + }; + + /** + * 色相变化 + * @param hue + */ + const handleHueChange = (hue: number, addUsedColor?: boolean) => { + color.value.hue = hue; + emitColorChange('palette-hue-bar', addUsedColor); + props.onPaletteBarChange?.({ + color: getColorObject(color.value), + }); + }; + + /** + * 透明度变化 + * @param alpha + */ + const handleAlphaChange = (alpha: number, addUsedColor?: boolean) => { + color.value.alpha = alpha; + emitColorChange('palette-alpha-bar', addUsedColor); + }; + + /** + * 输入框触发改变 + * @param input + * @param alpha + */ + const handleInputChange = (input: string, alpha?: number) => { + color.value.update(input); + color.value.alpha = alpha; + emitColorChange('input', true); + }; + + /** + * 渐变改变 + * @param param0 + */ + const handleGradientChange = ({ + key, + payload, + addUsedColor, + }: { + key: 'degree' | 'selectedId' | 'colors'; + payload: number | string | GradientColorPoint[]; + addUsedColor?: boolean; + }) => { + let trigger: ColorPickerChangeTrigger = 'palette-saturation-brightness'; + switch (key) { + case 'degree': + color.value.gradientDegree = payload as number; + trigger = 'input'; + break; + case 'selectedId': + color.value.gradientSelectedId = payload as string; + break; + case 'colors': + color.value.gradientColors = payload as GradientColorPoint[]; + break; + } + emitColorChange(trigger, addUsedColor); + }; + + /** + * 色块点击 + * @param type + * @param value + */ + const handleSetColor = (type: 'system' | 'used', value: string) => { + const isGradientValue = Color.isGradientColor(value); + if (type === 'system') { + if ( + (isGradientValue && mode.value === 'linear-gradient') || + (!isGradientValue && mode.value === 'monochrome') + ) { + // 每种模式下只能添加与模式匹配的颜色到最近使用色 + addRecentlyUsedColor(value); + } + } + if (isGradientValue) { + if (props.colorModes.includes('linear-gradient')) { + mode.value = 'linear-gradient'; + color.value.update(value); + color.value.updateCurrentGradientColor(); + } else { + console.warn('该模式不支持渐变色'); + } + } else if (mode.value === 'linear-gradient') { + color.value.updateStates(value); + color.value.updateCurrentGradientColor(); + addRecentlyUsedColor(color.value.linearGradient); + } else { + color.value.update(value); + } + emitColorChange(); + }; + + return { + baseClassName, + statusClassNames, + t, + global, + color, + mode, + formatModel, + recentlyUsedColors, + handleModeChange, + handleSatAndValueChange, + handleHueChange, + handleAlphaChange, + handleGradientChange, + handleSetColor, + handleFormatModeChange, + handleInputChange, + handleRecentlyUsedColorsChange, + }; + }, + render() { + const { baseClassName, statusClassNames, t, global, recentColors, recentlyUsedColors, swatchColors } = this; + const baseProps = { + color: this.color, + disabled: this.disabled, + }; + const showUsedColors = + recentColors !== null && recentColors !== false && ((recentlyUsedColors as string[]) || [])?.length > 0; + + let systemColors = swatchColors; + if (systemColors === undefined) { + systemColors = [...DEFAULT_SYSTEM_SWATCH_COLORS]; + } + const showSystemColors = systemColors?.length > 0; + + const renderSwatches = () => { + if (!showSystemColors && !showUsedColors) { + return null; + } + return ( + <> +
+ {showUsedColors ? ( + this.handleSetColor('used', color)} + onChange={this.handleRecentlyUsedColorsChange} + /> + ) : null} + {showSystemColors ? ( + this.handleSetColor('system', color)} + /> + ) : null} +
+ + ); + }; + + return ( +
e.stopPropagation()} + > + +
+ {this.mode === 'linear-gradient' ? ( + + ) : null} + + + {this.enableAlpha ? : null} + + {renderSwatches()} +
+
+ ); + }, +}); diff --git a/src/color-picker/panel/linear-gradient.tsx b/src/color-picker/panel/linear-gradient.tsx new file mode 100644 index 0000000000..02f06dfacb --- /dev/null +++ b/src/color-picker/panel/linear-gradient.tsx @@ -0,0 +1,278 @@ +import { defineComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; +import { cloneDeep } from 'lodash'; +import { GRADIENT_SLIDER_DEFAULT_WIDTH } from '../const'; +import { genGradientPoint } from '../utils/color'; +import { GradientColorPoint } from '../utils/gradient'; +import { InputNumber as TInputNumber } from '../../input-number'; +import { useBaseClassName } from '../hooks'; +import { useCommonClassName } from '../../config-provider'; +import baseProps from './base-props'; + +const DELETE_KEYS: string[] = ['delete', 'backspace']; + +export default defineComponent({ + name: 'LinearGradient', + components: { + TInputNumber, + }, + inheritAttrs: false, + props: { + ...baseProps, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const { STATUS } = useCommonClassName(); + const statusClassNames = STATUS.value; + const refSlider = ref(null); + const sliderRect = reactive({ + left: 0, + width: GRADIENT_SLIDER_DEFAULT_WIDTH, + }); + const isDragging = ref(false); + const isMoved = ref(false); + const degree = ref(props.color.gradientDegree); + const selectedId = ref(props.color.gradientSelectedId); + const colors = ref(cloneDeep(props.color.gradientColors)); + + watch( + () => props.color.gradientDegree, + (value) => (degree.value = value), + ); + watch( + () => props.color.gradientSelectedId, + (value) => (selectedId.value = value), + ); + watch( + () => props.color.gradientColors, + (value) => { + colors.value = cloneDeep(value); + }, + { + deep: true, + }, + ); + + const handleChange = (key: 'degree' | 'selectedId' | 'colors', payload: any, addUsedColor?: boolean) => { + if (props.disabled) { + return; + } + props.onChange({ + key, + payload, + addUsedColor, + }); + }; + + const handleDegreeChange = (value: number) => { + if (props.disabled || value === props.color.gradientDegree) { + return; + } + degree.value = value; + handleChange('degree', value, true); + }; + + const handleSelectedIdChange = (value: string) => { + if (props.disabled) { + return; + } + selectedId.value = value; + handleChange('selectedId', value); + }; + + const handleColorsChange = (value: GradientColorPoint[], isEnded?: boolean) => { + if (props.disabled) { + return; + } + colors.value = value; + handleChange('colors', value, isEnded); + }; + + /** + * 设置bar的位置 + * @param left + * @returns + */ + const updateActiveThumbLeft = (left: number) => { + const index = colors.value.findIndex((c) => c.id === selectedId.value); + if (index === -1) { + return; + } + const point = colors.value[index]; + left = Math.max(0, Math.min(sliderRect.width, left)); + const percentLeft = (left / sliderRect.width) * 100; + colors.value.splice(index, 1, { + color: point.color, + left: percentLeft, + id: point.id, + }); + handleColorsChange(colors.value); + }; + + // 移动开始 + const handleStart = (id: string, e: MouseEvent) => { + const rect = refSlider.value.getBoundingClientRect(); + sliderRect.left = rect.left; + sliderRect.width = rect.width || GRADIENT_SLIDER_DEFAULT_WIDTH; + if (isDragging.value || props.disabled) { + return; + } + isMoved.value = false; + isDragging.value = true; + e.preventDefault(); + e.stopPropagation(); + handleSelectedIdChange(id); + // 让slider获取焦点,以便键盘事件生效。 + refSlider.value.focus(); + window.addEventListener('mousemove', handleMove, false); + window.addEventListener('mouseup', handleEnd, false); + window.addEventListener('contextmenu', handleEnd, false); + }; + + // 移动中 + const handleMove = (e: MouseEvent) => { + if (!isDragging.value || props.disabled) { + return; + } + const left = e.clientX - sliderRect.left; + isMoved.value = true; + updateActiveThumbLeft(left); + }; + + // 移动结束 + const handleEnd = (e: MouseEvent) => { + if (!isDragging.value) { + return; + } + setTimeout(() => { + isDragging.value = false; + }, 0); + if (isMoved.value) { + handleColorsChange(colors.value, true); + isMoved.value = false; + } + window.removeEventListener('mousemove', handleMove, false); + window.removeEventListener('mouseup', handleEnd, false); + window.removeEventListener('contextmenu', handleEnd, false); + }; + + const handleKeyup = (e: KeyboardEvent) => { + if (props.disabled) { + return; + } + const points = colors.value; + let pos = points.findIndex((c) => c.id === selectedId.value); + const { length } = points; + // 必须保证有两个点 + if (DELETE_KEYS.includes(e.key.toLocaleLowerCase()) && length > 2 && pos >= 0 && pos <= length - 1) { + points.splice(pos, 1); + if (!points[pos]) { + // eslint-disable-next-line no-nested-ternary + pos = points[pos + 1] ? pos + 1 : points[pos - 1] ? pos - 1 : 0; + } + const current = points[pos]; + handleColorsChange(points, true); + handleSelectedIdChange(current?.id); + } + }; + + const handleThumbBarClick = (e: MouseEvent) => { + if (props.disabled) { + return; + } + let left = e.clientX - sliderRect.left; + left = Math.max(0, Math.min(sliderRect.width, left)); + const percentLeft = (left / sliderRect.width) * 100; + const newPoint = genGradientPoint(percentLeft, props.color.rgba); + colors.value.push(newPoint); + handleColorsChange(colors.value, true); + handleSelectedIdChange(newPoint.id); + }; + + onMounted(() => { + const rect = refSlider.value.getBoundingClientRect(); + sliderRect.left = rect.left; + sliderRect.width = rect.width || GRADIENT_SLIDER_DEFAULT_WIDTH; + }); + + onBeforeUnmount(() => { + window.removeEventListener('mousemove', handleMove, false); + window.removeEventListener('mouseup', handleEnd, false); + window.removeEventListener('contextmenu', handleEnd, false); + }); + + return { + baseClassName, + statusClassNames, + refSlider, + degree, + selectedId, + colors, + handleDegreeChange, + handleStart, + handleMove, + handleEnd, + handleKeyup, + handleThumbBarClick, + }; + }, + render() { + const { linearGradient } = this.color; + const { colors, selectedId, degree, disabled, baseClassName, statusClassNames } = this; + return ( +
+
+
+
    + {colors.map((t) => { + const left = `${Math.round(t.left * 100) / 100}%`; + return ( +
  • e.stopPropagation()} + onMousedown={(e: MouseEvent) => this.handleStart(t.id, e)} + > + +
  • + ); + })} +
+
+
+
+ `${value}°`} + v-model={this.degree} + onBlur={this.handleDegreeChange} + onEnter={this.handleDegreeChange} + disabled={disabled} + /> +
+
+ ); + }, +}); diff --git a/src/color-picker/panel/saturation.tsx b/src/color-picker/panel/saturation.tsx new file mode 100644 index 0000000000..19aca65561 --- /dev/null +++ b/src/color-picker/panel/saturation.tsx @@ -0,0 +1,120 @@ +import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'; +import { SATURATION_PANEL_DEFAULT_HEIGHT, SATURATION_PANEL_DEFAULT_WIDTH } from '../const'; +import { Select as TSelect, Option as TOption } from '../../select'; +import Draggable, { Coordinate } from '../utils/draggable'; +import { useBaseClassName } from '../hooks'; +import baseProps from './base-props'; + +export default defineComponent({ + name: 'SaturationPanel', + components: { + TSelect, + TOption, + }, + props: { + ...baseProps, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const refPanel = ref(null); + const refThumb = ref(null); + const dragInstance = ref(null); + const isMoved = ref(false); + const panelRect = reactive({ + width: SATURATION_PANEL_DEFAULT_WIDTH, + height: SATURATION_PANEL_DEFAULT_HEIGHT, + }); + + const styles = computed(() => { + const { saturation, value } = props.color; + const { width, height } = panelRect; + const top = Math.round((1 - value) * height); + const left = Math.round(saturation * width); + return { + color: props.color.rgb, + left: `${left}px`, + top: `${top}px`, + }; + }); + + const getSaturationAndValueByCoordinate = (coordinate: Coordinate) => { + const { width, height } = panelRect; + const { x, y } = coordinate; + const saturation = Math.round((x / width) * 100); + const value = Math.round((1 - y / height) * 100); + return { + saturation, + value, + }; + }; + + const handleDrag = (coordinate: Coordinate, isEnded?: boolean) => { + if (props.disabled) { + return; + } + const { saturation, value } = getSaturationAndValueByCoordinate(coordinate); + props.onChange({ + saturation: saturation / 100, + value: value / 100, + addUsedColor: isEnded, + }); + isMoved.value = true; + }; + + const handleDragEnd = (coordinate: Coordinate) => { + if (props.disabled || !isMoved.value) { + return; + } + isMoved.value = false; + nextTick(() => { + handleDrag(coordinate, true); + }); + }; + + const panelBackground = computed(() => { + return `hsl(${props.color.hue}, 100%, 50%)`; + }); + + onMounted(() => { + panelRect.width = refPanel.value.offsetWidth || SATURATION_PANEL_DEFAULT_WIDTH; + panelRect.height = refPanel.value.offsetHeight || SATURATION_PANEL_DEFAULT_HEIGHT; + dragInstance.value = new Draggable(refPanel.value, { + start() { + panelRect.width = refPanel.value.offsetWidth; + panelRect.height = refPanel.value.offsetHeight; + isMoved.value = false; + }, + drag: (coordinate: Coordinate) => { + handleDrag(coordinate); + }, + end: handleDragEnd, + }); + }); + + onBeforeUnmount(() => { + dragInstance.value.destroy(); + }); + + return { + baseClassName, + refThumb, + refPanel, + styles, + panelBackground, + }; + }, + render() { + const { baseClassName, styles, panelBackground } = this; + return ( +
+ +
+ ); + }, +}); diff --git a/src/color-picker/panel/slider.tsx b/src/color-picker/panel/slider.tsx new file mode 100644 index 0000000000..9f9ef8430b --- /dev/null +++ b/src/color-picker/panel/slider.tsx @@ -0,0 +1,107 @@ +import { computed, defineComponent, onBeforeUnmount, onMounted, PropType, reactive, ref } from 'vue'; +import { SLIDER_DEFAULT_WIDTH } from '../const'; +import { Select as TSelect, Option as TOption } from '../../select'; +import Draggable, { Coordinate } from '../utils/draggable'; +import { useBaseClassName } from '../hooks'; +import baseProps from './base-props'; + +export default defineComponent({ + name: 'ColorSlider', + components: { + TSelect, + TOption, + }, + props: { + ...baseProps, + className: { + type: String, + default: '', + }, + value: { + type: Number, + default: 0, + }, + maxValue: { + type: Number, + default: 360, + }, + railStyle: { + type: Object as PropType, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const refPanel = ref(null); + const refThumb = ref(null); + const dragInstance = ref(null); + const isMoved = ref(false); + const panelRect = reactive({ + width: SLIDER_DEFAULT_WIDTH, + }); + const styles = computed(() => { + const { width } = panelRect; + if (!width) { + return; + } + const left = Math.round((props.value / props.maxValue) * width); + return { + left: `${left}px`, + color: props.color.rgb, + }; + }); + + const handleDrag = (coordinate: Coordinate, isEnded?: boolean) => { + if (props.disabled) { + return; + } + const { width } = panelRect; + const { x } = coordinate; + const value = Math.round((x / width) * props.maxValue * 100) / 100; + isMoved.value = true; + props.onChange(value, isEnded); + }; + + const handleDragEnd = (coordinate: Coordinate) => { + if (props.disabled || !isMoved.value) { + return; + } + handleDrag(coordinate, true); + isMoved.value = false; + }; + + onMounted(() => { + panelRect.width = refPanel.value.offsetWidth || SLIDER_DEFAULT_WIDTH; + dragInstance.value = new Draggable(refPanel.value, { + start: () => { + // pop模式下由于是隐藏显示,这个宽度让其每次点击的时候重新计算 + panelRect.width = refPanel.value.offsetWidth; + isMoved.value = false; + }, + drag: (coordinate: Coordinate) => { + handleDrag(coordinate); + }, + end: handleDragEnd, + }); + }); + + onBeforeUnmount(() => { + dragInstance.value.destroy(); + }); + + return { + baseClassName, + refThumb, + refPanel, + styles, + }; + }, + render() { + const { baseClassName, className, railStyle, styles } = this; + return ( +
+
+ +
+ ); + }, +}); diff --git a/src/color-picker/panel/swatches.tsx b/src/color-picker/panel/swatches.tsx new file mode 100644 index 0000000000..dd60d8ba58 --- /dev/null +++ b/src/color-picker/panel/swatches.tsx @@ -0,0 +1,215 @@ +import { computed, defineComponent, PropType, ref } from 'vue'; +import { DeleteIcon, ErrorCircleFilledIcon } from 'tdesign-icons-vue-next'; +import { Select as TSelect, Option as TOption } from '../../select'; +import Color from '../utils/color'; +import { useBaseClassName } from '../hooks'; +import { useCommonClassName, useConfig, usePrefixClass } from '../../config-provider'; +import baseProps from './base-props'; +import { RecentColorsChangeTrigger } from '../type'; +import { Button as TButton, TdButtonProps } from '../../button'; + +export default defineComponent({ + name: 'SwatchesPanel', + components: { + TSelect, + TOption, + TButton, + }, + props: { + ...baseProps, + colors: { + type: Array as PropType, + default: () => [] as PropType, + }, + title: { + type: String, + default: '系统色彩', + }, + removable: { + type: Boolean, + default: false, + }, + onSetColor: { + type: Function, + default: () => { + return () => {}; + }, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const { t, global } = useConfig('colorPicker'); + const { global: confirmGlobal } = useConfig('popconfirm'); + const classPrefix = usePrefixClass(); + const { STATUS } = useCommonClassName(); + const statusClassNames = STATUS.value; + const visiblePopConfirm = ref(false); + const setVisiblePopConfirm = (visible: boolean) => { + visiblePopConfirm.value = visible; + }; + + const handleClick = (color: string) => props.onSetColor(color); + + const isEqualCurrentColor = (color: string) => { + return Color.compare(color, props.color.css); + }; + + const selectedColorIndex = computed(() => { + return props.colors.findIndex((color) => isEqualCurrentColor(color)); + }); + + /** + * 移除颜色 + */ + const handleRemoveColor = () => { + const colors = [...props.colors]; + const selectedIndex = selectedColorIndex.value; + let trigger: RecentColorsChangeTrigger = 'delete'; + if (selectedIndex > -1) { + colors.splice(selectedIndex, 1); + } else { + colors.length = 0; + trigger = 'clear'; + } + props.onChange(colors, trigger); + setVisiblePopConfirm(false); + }; + + return { + t, + global, + confirmGlobal, + classPrefix, + baseClassName, + statusClassNames, + selectedColorIndex, + visiblePopConfirm, + setVisiblePopConfirm, + handleClick, + isEqualCurrentColor, + handleRemoveColor, + }; + }, + render() { + const { + baseClassName, + statusClassNames, + classPrefix, + visiblePopConfirm, + t, + global, + confirmGlobal, + title, + removable, + } = this; + const swatchesClass = `${baseClassName}__swatches`; + const popupBaseClassName = `${classPrefix}-popup`; + const popConfirmBaseClassName = `${classPrefix}-popconfirm`; + + // 该方法暂时不用,后面交互讨论后再定 + const renderConfirm = () => { + return ( + + ); + }; + + const renderRemoveBtn = () => { + if (!removable) { + return null; + } + // if (this.selectedColorIndex === -1) { + // return ( + //
{ + // this.setVisiblePopConfirm(!visiblePopConfirm); + // }} + // > + // + // {renderConfirm()} + //
+ // ); + // } + return ( + this.handleRemoveColor()}> + + + ); + }; + + return ( +
+

+ {title} + {renderRemoveBtn()} +

+
    + {this.colors.map((color) => { + return ( +
  • { + if (this.disabled) { + return; + } + this.handleClick(color); + }} + > +
    + +
    +
  • + ); + })} +
+
+ ); + }, +}); diff --git a/src/color-picker/props.ts b/src/color-picker/props.ts new file mode 100644 index 0000000000..b45e48c6a6 --- /dev/null +++ b/src/color-picker/props.ts @@ -0,0 +1,82 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdColorPickerProps } from './type'; +import { PropType } from 'vue'; + +export default { + /** 关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 或 `undefined` 则不显示关闭按钮;值类型为函数,则表示自定义关闭按钮 */ + closeBtn: { + type: [String, Boolean, Function] as PropType, + default: true, + }, + /** 颜色模式选择。同时支持单色和渐变两种模式,可仅使用单色或者渐变其中一种模式,也可以同时使用。`monochrome` 表示单色,`linear-gradient` 表示渐变色 */ + colorModes: { + type: Array as PropType, + default: ['monochrome', 'linear-gradient'], + }, + /** 是否禁用组件 */ + disabled: Boolean, + /** 是否开启透明通道 */ + enableAlpha: Boolean, + /** 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA` 等值有效 */ + format: { + type: String as PropType, + default: 'RGB' as TdColorPickerProps['format'], + validator(val: TdColorPickerProps['format']): boolean { + if (!val) return true; + return ['RGB', 'RGBA', 'HSL', 'HSLA', 'HSB', 'HSV', 'HSVA', 'HEX', 'CMYK', 'CSS'].includes(val); + }, + }, + /** 透传 Input 输入框组件全部属性 */ + inputProps: { + type: Object as PropType, + }, + /** 【开发中】是否允许选中多个颜色 */ + multiple: Boolean, + /** 透传 Popup 组件全部属性,如 `placement` `overlayStyle` `overlayClassName` `trigger`等 */ + popupProps: { + type: Object as PropType, + }, + /** 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 null 则完全不显示“最近使用颜色” */ + recentColors: { + type: Array as PropType, + default: undefined, + }, + /** 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 null 则完全不显示“最近使用颜色”,非受控属性 */ + defaultRecentColors: { + type: Array as PropType, + default: (): TdColorPickerProps['defaultRecentColors'] => [], + }, + /** 透传 SelectInputProps 筛选器输入框组件全部属性 */ + selectInputProps: { + type: Object as PropType, + }, + /** 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色 */ + swatchColors: { + type: Array as PropType, + }, + /** 色值 */ + value: { + type: String, + default: undefined, + }, + modelValue: { + type: String, + default: undefined, + }, + /** 色值,非受控属性 */ + defaultValue: { + type: String, + default: '', + }, + /** 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源 */ + onChange: Function as PropType, + /** 调色板控制器的值变化时触发,`context.color` 指调色板控制器的值 */ + onPaletteBarChange: Function as PropType, + /** 最近使用颜色发生变化时触发。第一个参数 `value` 表示变化后的色值,`context.trigger` 表示触发颜色变化的来源 */ + onRecentColorsChange: Function as PropType, +}; diff --git a/src/color-picker/style/css.js b/src/color-picker/style/css.js new file mode 100644 index 0000000000..6a9a4b1328 --- /dev/null +++ b/src/color-picker/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/color-picker/style/index.js b/src/color-picker/style/index.js new file mode 100644 index 0000000000..180d455f6b --- /dev/null +++ b/src/color-picker/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/color-picker/_index.less'; diff --git a/src/color-picker/trigger.tsx b/src/color-picker/trigger.tsx new file mode 100644 index 0000000000..65eaa8c21d --- /dev/null +++ b/src/color-picker/trigger.tsx @@ -0,0 +1,95 @@ +import { defineComponent, PropType, ref, watch } from 'vue'; +import { Input as TInput } from '../input'; +import { InputNumber as TInputNumber } from '../input-number'; +import Color from './utils/color'; +import { TdColorPickerProps } from '.'; +import { useBaseClassName } from './hooks'; + +export default defineComponent({ + name: 'DefaultTrigger', + components: { + TInput, + TInputNumber, + }, + inheritAttrs: false, + props: { + color: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + inputProps: { + type: Object as PropType, + default: () => { + return { + autoWidth: true, + }; + }, + }, + onTriggerChange: { + type: Function, + default: () => { + return () => {}; + }, + }, + }, + setup(props) { + const baseClassName = useBaseClassName(); + const value = ref(props.color); + + watch( + () => [props.color], + () => (value.value = props.color), + ); + + const handleChange = (input: string) => { + if (input === props.color) { + return; + } + if (!Color.isValid(input)) { + value.value = props.color; + } else { + value.value = input; + } + props.onTriggerChange(value.value); + }; + + return { + baseClassName, + value, + handleChange, + }; + }, + + render() { + const { baseClassName } = this; + const inputSlots = { + label: () => { + return ( +
+ +
+ ); + }, + }; + return ( +
+ +
+ ); + }, +}); diff --git a/src/color-picker/type.ts b/src/color-picker/type.ts new file mode 100644 index 0000000000..47f436eb7f --- /dev/null +++ b/src/color-picker/type.ts @@ -0,0 +1,131 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { InputProps } from '../input'; +import { PopupProps } from '../popup'; +import { SelectInputProps } from '../select-input'; +import { TNode } from '../common'; + +export interface TdColorPickerProps { + /** + * 关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 或 `undefined` 则不显示关闭按钮;值类型为函数,则表示自定义关闭按钮 + * @default true + */ + closeBtn?: string | boolean | TNode; + /** + * 颜色模式选择。同时支持单色和渐变两种模式,可仅使用单色或者渐变其中一种模式,也可以同时使用。`monochrome` 表示单色,`linear-gradient` 表示渐变色 + * @default ['monochrome', 'linear-gradient'] + */ + colorModes?: Array<'monochrome' | 'linear-gradient'>; + /** + * 是否禁用组件 + * @default false + */ + disabled?: boolean; + /** + * 是否开启透明通道 + * @default false + */ + enableAlpha?: boolean; + /** + * 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA` 等值有效 + * @default RGB + */ + format?: 'RGB' | 'RGBA' | 'HSL' | 'HSLA' | 'HSB' | 'HSV' | 'HSVA' | 'HEX' | 'CMYK' | 'CSS'; + /** + * 透传 Input 输入框组件全部属性 + */ + inputProps?: InputProps; + /** + * 【开发中】是否允许选中多个颜色 + * @default false + */ + multiple?: boolean; + /** + * 透传 Popup 组件全部属性,如 `placement` `overlayStyle` `overlayClassName` `trigger`等 + */ + popupProps?: PopupProps; + /** + * 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 null 则完全不显示“最近使用颜色” + * @default [] + */ + recentColors?: boolean | Array; + /** + * 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 null 则完全不显示“最近使用颜色”,非受控属性 + * @default [] + */ + defaultRecentColors?: boolean | Array; + /** + * 透传 SelectInputProps 筛选器输入框组件全部属性 + */ + selectInputProps?: SelectInputProps; + /** + * 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色 + */ + swatchColors?: Array; + /** + * 色值 + * @default '' + */ + value?: string; + /** + * 色值,非受控属性 + * @default '' + */ + defaultValue?: string; + /** + * 色值 + * @default '' + */ + modelValue?: string; + /** + * 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源 + */ + onChange?: (value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void; + /** + * 调色板控制器的值变化时触发,`context.color` 指调色板控制器的值 + */ + onPaletteBarChange?: (context: { color: ColorObject }) => void; + /** + * 最近使用颜色发生变化时触发。第一个参数 `value` 表示变化后的色值,`context.trigger` 表示触发颜色变化的来源 + */ + onRecentColorsChange?: (value: Array, context: { trigger: RecentColorsChangeTrigger }) => void; +} + +export type ColorPickerChangeTrigger = + | 'palette-saturation-brightness' + | 'palette-saturation' + | 'palette-brightness' + | 'palette-hue-bar' + | 'palette-alpha-bar' + | 'input'; + +export interface ColorObject { + alpha: number; + css: string; + hex: string; + hex8: string; + hsl: string; + hsla: string; + hsv: string; + hsva: string; + rgb: string; + rgba: string; + saturation: number; + value: number; + isGradient: boolean; + linearGradient?: string; +} + +export type RecentColorsChangeTrigger = + | 'delete' + | 'clear' + | 'input' + | 'palette-saturation-brightness' + | 'palette-saturation' + | 'palette-brightness' + | 'palette-hue-bar' + | 'palette-alpha-bar'; diff --git a/src/color-picker/utils/click-outsider.ts b/src/color-picker/utils/click-outsider.ts new file mode 100644 index 0000000000..281bcf9fd0 --- /dev/null +++ b/src/color-picker/utils/click-outsider.ts @@ -0,0 +1,94 @@ +import { ComponentPublicInstance, VNode } from 'vue'; +import { prefix } from '../../config'; +import { on } from '../../utils/dom'; + +type Handler = (...args: unknown[]) => unknown; + +interface ElementHandler { + elements: HTMLElement[]; + handler: Handler; +} + +type FlushList = Map; + +const POPUP_SELECTOR = `.${prefix}-popup`; +const nodeList: FlushList = new Map(); + +let startClick: MouseEvent; + +let uid = 0; + +if (window && window.document) { + on(document, 'mousedown', (e: MouseEvent) => (startClick = e)); + on(document, 'mouseup', (e: MouseEvent) => { + for (const { handler } of nodeList.values()) { + handler(e); + } + }); +} + +type NodeElement = HTMLElement | VNode | ComponentPublicInstance; + +const createDocumentHandler = (elements: HTMLElement[], handler: Handler, includePopup = true) => { + return (e: MouseEvent) => { + if (includePopup) { + document.querySelectorAll(POPUP_SELECTOR).forEach((ele: Element) => { + elements.push(ele as HTMLElement); + }); + } + elements = Array.from(new Set(elements)); + const mouseUpTarget = e.target as Node; + const mouseDownTarget = startClick?.target as Node; + const isTargetUnExists = !mouseUpTarget || !mouseDownTarget; + if (isTargetUnExists) { + return; + } + const isContained = elements.some((el) => { + const isSelf = el === mouseUpTarget; + const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget); + return isSelf || isContainedByEl; + }); + if (isContained) { + return; + } + handler(); + }; +}; + +/** + * 元素外面点击 + * + * @example + * onMounted(() => { + * // 确保元素已挂载 + * addClickOutsider(refs.value, () => { + * visible.value = false + * }); + * }); + * + * onBeforeUnmount(() => removeClickOutsider); + */ +export const useClickOutsider = () => { + uid++; + const clickOutsiderId = uid; + const addClickOutsider = (els: NodeElement[], handler: Handler) => { + const elements = Array.from(new Set(els.filter((el) => el))).map((el: any) => { + const node = (el.el || el.$el || el) as HTMLElement; + return node; + }); + const documentHandler = createDocumentHandler(elements, handler, true); + nodeList.set(clickOutsiderId, { + elements, + handler: documentHandler, + }); + }; + + const removeClickOutsider = () => { + nodeList.has(clickOutsiderId) && nodeList.delete(clickOutsiderId); + }; + return { + clickOutsiderId, + addClickOutsider, + removeClickOutsider, + }; +}; diff --git a/src/color-picker/utils/cmyk.ts b/src/color-picker/utils/cmyk.ts new file mode 100644 index 0000000000..acbb52bb43 --- /dev/null +++ b/src/color-picker/utils/cmyk.ts @@ -0,0 +1,68 @@ +export const rgb2cmyk = (red: number, green: number, blue: number) => { + let computedC = 0; + let computedM = 0; + let computedY = 0; + let computedK = 0; + + const r = parseInt(`${red}`.replace(/\s/g, ''), 10); + const g = parseInt(`${green}`.replace(/\s/g, ''), 10); + const b = parseInt(`${blue}`.replace(/\s/g, ''), 10); + + if (r === 0 && g === 0 && b === 0) { + computedK = 1; + return [0, 0, 0, 1]; + } + + computedC = 1 - r / 255; + computedM = 1 - g / 255; + computedY = 1 - b / 255; + + const minCMY = Math.min(computedC, Math.min(computedM, computedY)); + computedC = (computedC - minCMY) / (1 - minCMY); + computedM = (computedM - minCMY) / (1 - minCMY); + computedY = (computedY - minCMY) / (1 - minCMY); + computedK = minCMY; + + return [computedC, computedM, computedY, computedK]; +}; + +export const cmyk2rgb = (c: number, m: number, y: number, k: number) => { + c /= 100; + m /= 100; + y /= 100; + k /= 100; + + c = c * (1 - k) + k; + m = m * (1 - k) + k; + y = y * (1 - k) + k; + + let r = 1 - c; + let g = 1 - m; + let b = 1 - y; + + r = Math.round(255 * r); + g = Math.round(255 * g); + b = Math.round(255 * b); + return { + r, + g, + b, + }; +}; + +const REG_CMYK_STRING = /cmyk\((\d+%?),(\d+%?),(\d+%?),(\d+%?)\)/; + +const toNumber = (str: string) => Math.max(0, Math.min(255, parseInt(str, 10))); +export const cmykInputToColor = (input: string) => { + if (/cmyk/i.test(input)) { + const str = input.replace(/\s/g, ''); + const match = str.match(REG_CMYK_STRING); + const c = toNumber(match[1]); + const m = toNumber(match[2]); + const y = toNumber(match[3]); + const k = toNumber(match[4]); + const { r, g, b } = cmyk2rgb(c, m, y, k); + return `rgb(${r}, ${g}, ${b})`; + } + return input; +}; diff --git a/src/color-picker/utils/color.ts b/src/color-picker/utils/color.ts new file mode 100644 index 0000000000..45d9cadf3a --- /dev/null +++ b/src/color-picker/utils/color.ts @@ -0,0 +1,441 @@ +import tinyColor from 'tinycolor2'; +import { TdColorPickerProps } from '../index'; +import { ColorObject } from '../type'; +import { cmykInputToColor, rgb2cmyk } from './cmyk'; +import { parseGradientString, GradientColors, GradientColorPoint, isGradientColor } from './gradient'; + +interface ColorStates { + s: number; + v: number; + h: number; + a: number; +} + +interface GradientStates { + colors: GradientColorPoint[]; + degree: number; + selectedId: string; + css?: string; +} + +const mathRound = Math.round; +const hsv2rgba = (states: ColorStates): tinyColor.ColorFormats.RGBA => tinyColor(states).toRgb(); +const hsv2hsva = (states: ColorStates): tinyColor.ColorFormats.HSVA => tinyColor(states).toHsv(); +const hsv2hsla = (states: ColorStates): tinyColor.ColorFormats.HSLA => tinyColor(states).toHsl(); + +/** + * 将渐变对象转换成字符串 + * @param object + * @returns + */ +const gradientColors2string = (object: GradientColors): string => { + const { points, degree } = object; + const colorsStop = points + .sort((pA, pB) => { + return pA.left - pB.left; + }) + .map((p) => { + return `${p.color} ${Math.round(p.left * 100) / 100}%`; + }); + + return `linear-gradient(${degree}deg,${colorsStop.join(',')})`; +}; + +/** + * 去除颜色的透明度 + * @param color + * @returns + */ +export const getColorWithoutAlpha = (color: string) => tinyColor(color).setAlpha(1).toHexString(); + +// 生成一个随机ID +export const genId = () => (1 + Math.random() * 4294967295).toString(16); + +/** + * 生成一个渐变颜色 + * @param left + * @param color + * @returns + */ +export const genGradientPoint = (left: number, color: string): GradientColorPoint => { + return { + id: genId(), + left, + color, + }; +}; + +export class Color { + states: ColorStates = { + s: 100, + v: 100, + h: 100, + a: 1, + }; + + originColor: string; + + isGradient: boolean; + + gradientStates: GradientStates = { + colors: [], + degree: 0, + selectedId: null, + css: '', + }; + + constructor(input: string) { + this.update(input); + } + + update(input: string) { + if (input === this.originColor) { + return; + } + this.originColor = input; + this.isGradient = false; + const gradientColors = parseGradientString(input); + let colorInput = input; + if (gradientColors) { + this.isGradient = true; + const object = gradientColors as GradientColors; + const points = object.points.map((c) => genGradientPoint(c.left, c.color)); + this.gradientStates = { + colors: points, + degree: object.degree, + selectedId: points[0]?.id || null, + }; + this.gradientStates.css = this.linearGradient; + colorInput = this.gradientSelectedPoint?.color; + } + + this.updateStates(colorInput); + } + + get saturation() { + return this.states.s; + } + + set saturation(value) { + this.states.s = Math.max(0, Math.min(100, value)); + this.updateCurrentGradientColor(); + } + + get value() { + return this.states.v; + } + + set value(value) { + this.states.v = Math.max(0, Math.min(100, value)); + this.updateCurrentGradientColor(); + } + + get hue() { + return this.states.h; + } + + set hue(value) { + this.states.h = Math.max(0, Math.min(360, value)); + this.updateCurrentGradientColor(); + } + + get alpha() { + return this.states.a; + } + + set alpha(value) { + this.states.a = Math.max(0, Math.min(1, Math.round(value * 100) / 100)); + this.updateCurrentGradientColor(); + } + + get rgb() { + const { r, g, b } = hsv2rgba(this.states); + return `rgb(${mathRound(r)}, ${mathRound(g)}, ${mathRound(b)})`; + } + + get rgba() { + const { r, g, b, a } = hsv2rgba(this.states); + return `rgba(${mathRound(r)}, ${mathRound(g)}, ${mathRound(b)}, ${a})`; + } + + get hsv() { + const { h, s, v } = this.getHsva(); + return `hsv(${h}, ${s}%, ${v}%)`; + } + + get hsva() { + const { h, s, v, a } = this.getHsva(); + return `hsva(${h}, ${s}%, ${v}%, ${a})`; + } + + get hsl() { + const { h, s, l } = this.getHsla(); + return `hsl(${h}, ${s}%, ${l}%)`; + } + + get hsla() { + const { h, s, l, a } = this.getHsla(); + return `hsla(${h}, ${s}%, ${l}%, ${a})`; + } + + get hex() { + return tinyColor(this.states).toHexString(); + } + + get hex8() { + return tinyColor(this.states).toHex8String(); + } + + get cmyk() { + const { c, m, y, k } = this.getCmyk(); + return `cmyk(${c}, ${m}, ${y}, ${k})`; + } + + get css() { + if (this.isGradient) { + return this.linearGradient; + } + return this.rgba; + } + + get linearGradient() { + const { gradientColors, gradientDegree } = this; + return gradientColors2string({ + points: gradientColors, + degree: gradientDegree, + }); + } + + get gradientColors() { + return this.gradientStates.colors; + } + + set gradientColors(colors: GradientColorPoint[]) { + this.gradientStates.colors = colors; + this.gradientStates.css = this.linearGradient; + } + + get gradientSelectedId() { + return this.gradientStates.selectedId; + } + + set gradientSelectedId(id: string) { + if (id === this.gradientSelectedId) { + return; + } + this.gradientStates.selectedId = id; + this.updateStates(this.gradientSelectedPoint?.color); + } + + get gradientDegree() { + return this.gradientStates.degree; + } + + set gradientDegree(degree: number) { + this.gradientStates.degree = Math.max(0, Math.min(360, degree)); + this.gradientStates.css = this.linearGradient; + } + + get gradientSelectedPoint() { + const { gradientColors, gradientSelectedId } = this; + return gradientColors.find((color) => color.id === gradientSelectedId); + } + + getFormatsColorMap() { + return { + HEX: this.hex, + CMYK: this.cmyk, + RGB: this.rgb, + RGBA: this.rgba, + HSL: this.hsl, + HSLA: this.hsla, + HSV: this.hsv, + HSVA: this.hsva, + CSS: this.css, + }; + } + + updateCurrentGradientColor() { + const { isGradient, gradientColors, gradientSelectedId } = this; + const { length } = gradientColors; + const current = this.gradientSelectedPoint; + if (!isGradient || length === 0 || !current) { + return false; + } + const index = gradientColors.findIndex((color) => color.id === gradientSelectedId); + const newColor = { + ...current, + color: this.rgba, + }; + gradientColors.splice(index, 1, newColor); + this.gradientColors = gradientColors; + } + + updateStates(input: string) { + const color = tinyColor(cmykInputToColor(input)); + const hsva = color.toHsv(); + this.states = hsva; + } + + getRgba() { + const { r, g, b, a } = hsv2rgba(this.states); + return { + r: mathRound(r), + g: mathRound(g), + b: mathRound(b), + a, + }; + } + + getCmyk() { + const { r, g, b } = this.getRgba(); + const [c, m, y, k] = rgb2cmyk(r, g, b); + return { + c: mathRound(c * 100), + m: mathRound(m * 100), + y: mathRound(y * 100), + k: mathRound(k * 100), + }; + } + + getHsva(): tinyColor.ColorFormats.HSVA { + let { h, s, v, a } = hsv2hsva(this.states); + h = mathRound(h); + s = mathRound(s * 100); + v = mathRound(v * 100); + a *= 1; + return { + h, + s, + v, + a, + }; + } + + getHsla(): tinyColor.ColorFormats.HSLA { + let { h, s, l, a } = hsv2hsla(this.states); + h = mathRound(h); + s = mathRound(s * 100); + l = mathRound(l * 100); + a *= 1; + return { + h, + s, + l, + a, + }; + } + + /** + * 判断输入色是否与当前色相同 + * @param color + * @returns + */ + equals(color: string): boolean { + return tinyColor.equals(this.rgba, color); + } + + /** + * 校验输入色是否是一个有效颜色 + * @param color + * @returns + */ + static isValid(color: string): boolean { + if (parseGradientString(color)) { + return true; + } + return tinyColor(color).isValid(); + } + + static hsva2color(h: number, s: number, v: number, a: number) { + return tinyColor({ h, s, v, a }).toHsvString(); + } + + static hsla2color(h: number, s: number, l: number, a: number) { + return tinyColor({ h, s, l, a }).toHslString(); + } + + static rgba2color(r: number, g: number, b: number, a: number) { + return tinyColor({ r, g, b, a }).toHsvString(); + } + + static hex2color(hex: string, a: number) { + const color = tinyColor(hex); + color.setAlpha(a); + return color.toHexString(); + } + + /** + * 对象转颜色字符串 + * @param object + * @param format + * @returns + */ + static object2color(object: any, format: TdColorPickerProps['format']) { + if (format === 'CMYK') { + const { c, m, y, k } = object; + return `cmyk(${c}, ${m}, ${y}, ${k})`; + } + const color = tinyColor(object, { + format, + }); + return color.toRgbString(); + } + + static isGradientColor = (input: string) => !!isGradientColor(input); + + /** + * 比较两个颜色是否相同 + * @param color1 + * @param color2 + * @returns + */ + static compare = (color1: string, color2: string): boolean => { + const isGradientColor1 = Color.isGradientColor(color1); + const isGradientColor2 = Color.isGradientColor(color2); + if (isGradientColor1 && isGradientColor2) { + const gradientColor1 = gradientColors2string(parseGradientString(color1) as GradientColors); + const gradientColor2 = gradientColors2string(parseGradientString(color2) as GradientColors); + return gradientColor1 === gradientColor2; + } + if (!isGradientColor1 && !isGradientColor2) { + return tinyColor.equals(color1, color2); + } + return false; + }; +} + +const COLOR_OBJECT_OUTPUT_KEYS = [ + 'alpha', + 'css', + 'hex', + 'hex8', + 'hsl', + 'hsla', + 'hsv', + 'hsva', + 'rgb', + 'rgba', + 'saturation', + 'value', + 'isGradient', +]; + +/** + * 获取对外输出的color对象 + * @param color + * @returns + */ +export const getColorObject = (color: Color): ColorObject => { + if (!color) { + return null; + } + const colorObject = Object.create(null); + COLOR_OBJECT_OUTPUT_KEYS.forEach((key) => (colorObject[key] = color[key])); + if (color.isGradient) { + colorObject.linearGradient = color.linearGradient; + } + return colorObject; +}; + +export default Color; diff --git a/src/color-picker/utils/draggable.ts b/src/color-picker/utils/draggable.ts new file mode 100644 index 0000000000..92e24e2a9e --- /dev/null +++ b/src/color-picker/utils/draggable.ts @@ -0,0 +1,99 @@ +export interface Coordinate { + x: number; + y: number; +} + +export type DraggableEvent = MouseEvent; + +interface DraggableCallback { + (coordinate: Coordinate, event?: DraggableEvent): void; +} + +export interface DraggableProps { + start?: DraggableCallback; + drag?: DraggableCallback; + end?: DraggableCallback; +} + +interface DraggableHandles { + start: (this: Draggable, event: DraggableEvent) => {}; + drag: (this: Draggable, event: DraggableEvent) => {}; + end: (this: Draggable, event: DraggableEvent) => {}; +} + +// 配置项 +const defaultsOptions: DraggableProps = { + start: (coordinate: Coordinate, event: DraggableEvent) => {}, + drag: (coordinate: Coordinate, event: DraggableEvent) => {}, + end: (coordinate: Coordinate, event: DraggableEvent) => {}, +}; + +export class Draggable { + private dragging = false; + + private $el: HTMLElement; + + private props: DraggableProps; + + private handles: DraggableHandles; + + constructor(el: HTMLElement, options?: DraggableProps) { + this.$el = el; + this.props = { ...defaultsOptions, ...options }; + this.handles = { + start: this.#dragStart.bind(this), + drag: this.#drag.bind(this), + end: this.#dragEnd.bind(this), + }; + this.$el.addEventListener('mousedown', this.handles.start, false); + } + + #dragStart(event: DraggableEvent) { + if (this.dragging) { + return; + } + // event.preventDefault(); + window.addEventListener('mousemove', this.handles.drag, false); + window.addEventListener('mouseup', this.handles.end, false); + window.addEventListener('contextmenu', this.handles.end, false); + this.dragging = true; + this.props.start(this.#getCoordinate(event), event); + } + + #drag(event: DraggableEvent) { + if (!this.dragging) { + return; + } + this.props.drag(this.#getCoordinate(event), event); + } + + #dragEnd(event: DraggableEvent) { + setTimeout(() => { + this.dragging = false; + this.props.end(this.#getCoordinate(event), event); + }, 0); + window.removeEventListener('mousemove', this.handles.drag, false); + window.removeEventListener('mouseup', this.handles.end, false); + window.removeEventListener('contextmenu', this.handles.end, false); + } + + #getCoordinate(event: DraggableEvent) { + const rect = this.$el.getBoundingClientRect(); + const mouseEvent = event; + const left = mouseEvent.clientX - rect.left; + const top = mouseEvent.clientY - rect.top; + return { + y: Math.min(Math.max(0, top), rect.height), + x: Math.min(Math.max(0, left), rect.width), + }; + } + + destroy() { + this.$el.removeEventListener('mousedown', this.handles.start, false); + window.removeEventListener('mousemove', this.handles.drag, false); + window.removeEventListener('mouseup', this.handles.end, false); + window.removeEventListener('contextmenu', this.handles.end, false); + } +} + +export default Draggable; diff --git a/src/color-picker/utils/gradient.ts b/src/color-picker/utils/gradient.ts new file mode 100644 index 0000000000..5b450d7d05 --- /dev/null +++ b/src/color-picker/utils/gradient.ts @@ -0,0 +1,232 @@ +/** + * 用于反解析渐变字符串为对象 + * https://stackoverflow.com/questions/20215440/parse-css-gradient-rule-with-javascript-regex + */ +import tinyColor from 'tinycolor2'; + +/** + * Utility combine multiple regular expressions. + * + * @param {RegExp[]|string[]} regexpList List of regular expressions or strings. + * @param {string} flags Normal RegExp flags. + */ +const combineRegExp = function (regexpList: (string | RegExp)[], flags: string): RegExp { + let i; + let source = ''; + for (i = 0; i < regexpList.length; i++) { + if (typeof regexpList[i] === 'string') { + source += regexpList[i]; + } else { + source += (regexpList[i] as RegExp).source; + } + } + return new RegExp(source, flags); +}; + +interface regExpLib { + gradientSearch: RegExp; + colorStopSearch: RegExp; +} + +interface ColorStop { + color: string; + position?: string; +} + +interface ParseGradientResult { + original: string; + colorStopList?: ColorStop[]; + line?: string; + angle?: string; + sideCorner?: string; +} + +/** + * Generate the required regular expressions once. + * + * Regular Expressions are easier to manage this way and can be well described. + * + * @result {object} Object containing regular expressions. + */ +const generateRegExp = (): regExpLib => { + // Note any variables with "Capture" in name include capturing bracket set(s). + const searchFlags = 'gi'; // ignore case for angles, "rgb" etc + const rAngle = /(?:[+-]?\d*\.?\d+)(?:deg|grad|rad|turn)/; // Angle +ive, -ive and angle types + const rSideCornerCapture = /to\s+((?:(?:left|right|top|bottom)(?:\s+(?:top|bottom|left|right))?))/; // optional 2nd part + const rComma = /\s*,\s*/; // Allow space around comma. + const rColorHex = /#(?:[a-f0-9]{6}|[a-f0-9]{3})/; // 3 or 6 character form + const rDigits3 = /\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*\)/; + const // "(1, 2, 3)" + rDigits4 = /\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*,\s*\d*\.?\d+\)/; + const // "(1, 2, 3, 4)" + rValue = /(?:[+-]?\d*\.?\d+)(?:%|[a-z]+)?/; + const // ".9", "-5px", "100%". + rKeyword = /[_a-z-][_a-z0-9-]*/; + const // "red", "transparent". + rColor = combineRegExp( + ['(?:', rColorHex, '|', '(?:rgb|hsl)', rDigits3, '|', '(?:rgba|hsla)', rDigits4, '|', rKeyword, ')'], + '', + ); + const rColorStop = combineRegExp([rColor, '(?:\\s+', rValue, '(?:\\s+', rValue, ')?)?'], ''); + const // Single Color Stop, optional %, optional length. + rColorStopList = combineRegExp(['(?:', rColorStop, rComma, ')*', rColorStop], ''); + const // List of color stops min 1. + rLineCapture = combineRegExp(['(?:(', rAngle, ')|', rSideCornerCapture, ')'], ''); + const // Angle or SideCorner + rGradientSearch = combineRegExp(['(?:(', rLineCapture, ')', rComma, ')?(', rColorStopList, ')'], searchFlags); + const // Capture 1:"line", 2:"angle" (optional), 3:"side corner" (optional) and 4:"stop list". + rColorStopSearch = combineRegExp( + ['\\s*(', rColor, ')', '(?:\\s+', '(', rValue, '))?', '(?:', rComma, '\\s*)?'], + searchFlags, + ); // Capture 1:"color" and 2:"position" (optional). + + return { + gradientSearch: rGradientSearch, + colorStopSearch: rColorStopSearch, + }; +}; + +/** + * Actually parse the input gradient parameters string into an object for reusability. + * + * + * @note Really this only supports the standard syntax not historical versions, see MDN for details + * https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient + * + * @param regExpLib + * @param {string} input Input string in the form "to right bottom, #FF0 0%, red 20px, rgb(0, 0, 255) 100%" + * @returns {object|undefined} Object containing break down of input string including array of stop points. + */ +const parseGradient = function (regExpLib: regExpLib, input: string) { + let result: ParseGradientResult; + let matchColorStop; + let stopResult: ColorStop; + + // reset search position, because we reuse regex. + regExpLib.gradientSearch.lastIndex = 0; + + const matchGradient = regExpLib.gradientSearch.exec(input); + if (matchGradient !== null) { + result = { + original: matchGradient[0], + colorStopList: [], + }; + + // Line (Angle or Side-Corner). + if (matchGradient[1]) { + // eslint-disable-next-line prefer-destructuring + result.line = matchGradient[1]; + } + // Angle or undefined if side-corner. + if (matchGradient[2]) { + // eslint-disable-next-line prefer-destructuring + result.angle = matchGradient[2]; + } + // Side-corner or undefined if angle. + if (matchGradient[3]) { + // eslint-disable-next-line prefer-destructuring + result.sideCorner = matchGradient[3]; + } + + // reset search position, because we reuse regex. + regExpLib.colorStopSearch.lastIndex = 0; + + // Loop though all the color-stops. + matchColorStop = regExpLib.colorStopSearch.exec(matchGradient[4]); + while (matchColorStop !== null) { + stopResult = { + color: matchColorStop[1], + }; + + // Position (optional). + if (matchColorStop[2]) { + // eslint-disable-next-line prefer-destructuring + stopResult.position = matchColorStop[2]; + } + result.colorStopList.push(stopResult); + + // Continue searching from previous position. + matchColorStop = regExpLib.colorStopSearch.exec(matchGradient[4]); + } + } + + // Can be undefined if match not found. + return result; +}; + +export interface GradientColorPoint { + id?: string; + color?: string; + left?: number; +} + +export interface GradientColors { + points: GradientColorPoint[]; + degree: number; +} + +const REGEXP_LIB = generateRegExp(); +const REG_GRADIENT = /.*gradient\s*\(((?:\([^)]*\)|[^)(]*)*)\)/gim; + +/** + * 验证是否是渐变字符串 + * @param input + * @returns + */ +export const isGradientColor = (input: string): null | RegExpExecArray => { + REG_GRADIENT.lastIndex = 0; + return REG_GRADIENT.exec(input); +}; + +// 边界字符串和角度关系 +const sideCornerDegreeMap = { + top: 0, + right: 90, + bottom: 180, + left: 270, + 'top left': 225, + 'left top': 225, + 'top right': 135, + 'right top': 135, + 'bottom left': 315, + 'left bottom': 315, + 'bottom right': 45, + 'right bottom': 45, +}; + +/** + * 解析渐变字符串为 GradientColors 对象 + * @param input + * @returns + */ +export const parseGradientString = (input: string): GradientColors | boolean => { + const match = isGradientColor(input); + if (!match) { + return false; + } + const gradientColors: GradientColors = { + points: [], + degree: 0, + }; + + const result: ParseGradientResult = parseGradient(REGEXP_LIB, match[1]); + if (result.original.trim() !== match[1].trim()) { + return false; + } + const points: GradientColorPoint[] = result.colorStopList.map(({ color, position }) => { + const point = Object.create(null); + point.color = tinyColor(color).toRgbString(); + point.left = parseFloat(position); + return point; + }); + gradientColors.points = points; + let degree = parseInt(result.angle, 10); + if (Number.isNaN(degree)) { + degree = sideCornerDegreeMap[result.sideCorner] || 90; + } + gradientColors.degree = degree; + + return gradientColors; +}; + +export default parseGradientString; diff --git a/src/components.ts b/src/components.ts index 007adab6b1..b9f70af4c6 100644 --- a/src/components.ts +++ b/src/components.ts @@ -34,6 +34,7 @@ export * from './textarea'; export * from './transfer'; export * from './time-picker'; export * from './tree-select'; +export * from './color-picker'; // 数据展示 export * from './avatar'; diff --git a/src/config-provider/en_US_config.ts b/src/config-provider/en_US_config.ts index 57e2019bd6..f7b025d374 100644 --- a/src/config-provider/en_US_config.ts +++ b/src/config-provider/en_US_config.ts @@ -141,6 +141,11 @@ const GLOBAL_CONFIG_EN: GlobalConfigProvider = { input: { placeholder: '', }, + colorPicker: { + swatchColorTitle: 'System Default', + recentColorTitle: 'Recently Used', + clearConfirmText: 'Clear recently used colors?', + }, }; export default GLOBAL_CONFIG_EN; diff --git a/src/config-provider/type.ts b/src/config-provider/type.ts index f67d2c2a83..b716ab2621 100644 --- a/src/config-provider/type.ts +++ b/src/config-provider/type.ts @@ -36,6 +36,10 @@ export interface GlobalConfigProvider { * @default t */ classPrefix?: string; + /** + * 颜色选择器全局配置 + */ + colorPicker?: ColorPickerConfig; /** * 日期选择器全局配置 */ @@ -222,6 +226,24 @@ export interface CascaderConfig { placeholder?: string; } +export interface ColorPickerConfig { + /** + * 清空颜色确认文案,示例:'确定清空最近使用的颜色吗?' + * @default '确定清空最近使用的颜色吗?' + */ + clearConfirmText?: string; + /** + * 最近使用颜色区域标题文本,示例:'最近使用颜色' + * @default '最近使用颜色' + */ + recentColorTitle?: string; + /** + * 系统预设颜色区域标题文本,示例:'系统预设颜色' + * @default '系统预设颜色' + */ + swatchColorTitle?: string; +} + export interface TransferConfig { /** * 空数据描述文本 diff --git a/src/config-provider/zh_CN_config.ts b/src/config-provider/zh_CN_config.ts index 0c84be77e5..009a88547c 100644 --- a/src/config-provider/zh_CN_config.ts +++ b/src/config-provider/zh_CN_config.ts @@ -185,6 +185,11 @@ const GLOBAL_CONFIG_ZH: GlobalConfigProvider = { copySuccessText: '链接复制成功', copyText: '复制链接', }, + colorPicker: { + swatchColorTitle: '系统预设颜色', + recentColorTitle: '最近使用颜色', + clearConfirmText: '确定清空最近使用的颜色吗?', + }, }; export default GLOBAL_CONFIG_ZH; diff --git a/test/e2e/color-picker/color-picker.js b/test/e2e/color-picker/color-picker.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/ssr/__snapshots__/ssr.test.js.snap b/test/ssr/__snapshots__/ssr.test.js.snap index 38d25de2b4..0649b8c80f 100644 --- a/test/ssr/__snapshots__/ssr.test.js.snap +++ b/test/ssr/__snapshots__/ssr.test.js.snap @@ -6590,6 +6590,2818 @@ exports[`ssr snapshot test renders ./examples/checkbox/demos/link.vue correctly `; +exports[`ssr snapshot test renders ./examples/color-picker/demos/color-mode.vue correctly 1`] = ` +
+
+
默认(单色 + 线性渐变)
+
+ + +
+
+
+
+ +
+ +
+ +
#0052d9 + + +
+ +
+
+
+ +
+
+
+
仅单色模式
+
+ + +
+
+
+
+ +
+ +
+ +
#0052d9 + + +
+ +
+
+
+ +
+
+
+
+
仅线性渐变模式
+
+ + +
+
+
+
+ +
+ +
+ +
linear-gradient(45deg, #4facfe 0%, #00f2fe 100%) + + +
+ +
+
+
+ +
+
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/enable-alpha.vue correctly 1`] = ` +
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

系统预设颜色 + +

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+ +
+
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/panel.vue correctly 1`] = ` +
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

系统预设颜色 + +

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+ +
+
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/recent-color.vue correctly 1`] = ` +
+
+
预设最近使用色
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+
+

最近使用颜色

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+

系统预设颜色 + +

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+ +
+
+
+
+
完全不显示最近使用色
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

系统预设颜色 + +

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+ +
+
+
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/status-disabled.vue correctly 1`] = ` +
+
+ + +
+
+
+
+ +
+ +
+ +
#0052d9 + + +
+ +
+
+
+ +
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/status-readonly.vue correctly 1`] = ` +
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

系统预设颜色 + +

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+ +
+
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/swatch-color.vue correctly 1`] = ` +
+
+
自定义系统色
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

系统预设颜色 + +

+
    + +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • +
  • +
    +
  • + +
+
+
+ +
+
+
+
+
完全不显示系统色
+
+
+
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + RGB + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + +
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+`; + +exports[`ssr snapshot test renders ./examples/color-picker/demos/trigger.vue correctly 1`] = ` +
+
+ + +
+
+
+
+ +
+ +
+ +
#0052d9 + + +
+ +
+
+
+ +
+
+`; + exports[`ssr snapshot test renders ./examples/comment/demos/base.vue correctly 1`] = `
diff --git a/test/unit/color-picker/__snapshots__/demo.test.js.snap b/test/unit/color-picker/__snapshots__/demo.test.js.snap new file mode 100644 index 0000000000..a8e93aab53 --- /dev/null +++ b/test/unit/color-picker/__snapshots__/demo.test.js.snap @@ -0,0 +1,9281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorPicker ColorPicker colorModeVue demo works fine 1`] = ` +
+
+
+ 默认(单色 + 线性渐变) +
+
+ + +
+
+ 仅单色模式 +
+
+ + +
+
+
+
+ 仅线性渐变模式 +
+
+ + +
+
+`; + +exports[`ColorPicker ColorPicker enableAlphaVue demo works fine 1`] = ` +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

+ + 系统预设颜色 + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+ +
+
+
+`; + +exports[`ColorPicker ColorPicker panelVue demo works fine 1`] = ` +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

+ + 系统预设颜色 + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+ +
+
+
+`; + +exports[`ColorPicker ColorPicker recentColorVue demo works fine 1`] = ` +
+
+
+ 预设最近使用色 +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+
+

+ + 最近使用颜色 + + + + + + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+

+ + 系统预设颜色 + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+ +
+
+
+
+
+ 完全不显示最近使用色 +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

+ + 系统预设颜色 + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+ +
+
+
+
+`; + +exports[`ColorPicker ColorPicker statusDisabledVue demo works fine 1`] = ` +
+
+ + + + +
+
+
+
+ +
+ +
+ +
+ +
+ + + #0052d9 + + + +
+ +
+
+
+ +
+
+`; + +exports[`ColorPicker ColorPicker statusReadonlyVue demo works fine 1`] = ` +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

+ + 系统预设颜色 + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+ +
+
+
+`; + +exports[`ColorPicker ColorPicker swatchColorVue demo works fine 1`] = ` +
+
+
+ 自定义系统色 +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+ +
+

+ + 系统预设颜色 + + +

+
    + +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • + +
+
+
+ +
+
+
+
+
+ 完全不显示系统色 +
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+ + + + + + RGB + + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+`; + +exports[`ColorPicker ColorPicker triggerVue demo works fine 1`] = ` +
+
+ + +`; diff --git a/test/unit/color-picker/demo.test.js b/test/unit/color-picker/demo.test.js new file mode 100644 index 0000000000..aff655f9c6 --- /dev/null +++ b/test/unit/color-picker/demo.test.js @@ -0,0 +1,33 @@ +/** + * 该文件为由脚本 `npm run test:demo` 自动生成,如需修改,执行脚本命令即可。请勿手写直接修改,否则会被覆盖 + */ + +import { mount } from '@vue/test-utils'; +import colorModeVue from '@/examples/color-picker/demos/color-mode.vue'; +import enableAlphaVue from '@/examples/color-picker/demos/enable-alpha.vue'; +import panelVue from '@/examples/color-picker/demos/panel.vue'; +import recentColorVue from '@/examples/color-picker/demos/recent-color.vue'; +import statusDisabledVue from '@/examples/color-picker/demos/status-disabled.vue'; +import statusReadonlyVue from '@/examples/color-picker/demos/status-readonly.vue'; +import swatchColorVue from '@/examples/color-picker/demos/swatch-color.vue'; +import triggerVue from '@/examples/color-picker/demos/trigger.vue'; + +const mapper = { + colorModeVue, + enableAlphaVue, + panelVue, + recentColorVue, + statusDisabledVue, + statusReadonlyVue, + swatchColorVue, + triggerVue, +}; + +describe('ColorPicker', () => { + Object.keys(mapper).forEach((demoName) => { + it(`ColorPicker ${demoName} demo works fine`, () => { + const wrapper = mount(mapper[demoName]); + expect(wrapper.element).toMatchSnapshot(); + }); + }); +}); diff --git a/test/unit/color-picker/index.test.js b/test/unit/color-picker/index.test.js new file mode 100644 index 0000000000..41c350e20f --- /dev/null +++ b/test/unit/color-picker/index.test.js @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils'; +import { ColorPickerPanel, ColorPicker } from '@/src/color-picker/index.ts'; + +// every component needs four parts: props/events/slots/functions. +describe('ColorPickerPanel', () => { + // test props api + describe(':props', () => { + it('', () => { + const wrapper = mount({ + render() { + return ; + }, + }); + expect(wrapper.exists()).toBe(true); + }); + }); +}); + +describe('ColorPicker', () => { + // test props api + describe(':props', () => { + it('', () => { + const wrapper = mount({ + render() { + return ; + }, + }); + expect(wrapper.exists()).toBe(true); + }); + }); +}); diff --git a/test/unit/input-number/__snapshots__/demo.test.js.snap b/test/unit/input-number/__snapshots__/demo.test.js.snap index bd8cf341f8..f123eda820 100644 --- a/test/unit/input-number/__snapshots__/demo.test.js.snap +++ b/test/unit/input-number/__snapshots__/demo.test.js.snap @@ -1,5 +1,402 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`InputNumber InputNumber alignVue demo works fine 1`] = ` +
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+ +
+
+ + + + + + +
+ +
+ +
+
+
+`; + +exports[`InputNumber InputNumber autoWidthVue demo works fine 1`] = ` +
+
+ +
+
+ + + + + 3 + + + +
+ +
+ +
+
+`; + exports[`InputNumber InputNumber centerVue demo works fine 1`] = `
`; +exports[`InputNumber InputNumber statusVue demo works fine 1`] = ` +
+
+ +
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+
+ 这是普通文本提示 +
+
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+
+ 校验通过文本提示 +
+
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+
+ 校验不通过文本提示 +
+
+ +
+ + +
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + +
+
+ 校验存在严重问题文本提示 +
+
+ +
+ + +
+ +
+
+ +
+
+`; + exports[`InputNumber InputNumber stepVue demo works fine 1`] = `
`; +exports[`Upload Upload fileFlowListBatchUploadVue demo works fine 1`] = ` +
+
+ + + + + +
+
+ +
+ +
+ + + 批量合并上传,会通过一个接口上传所有的文件 + +
+ + + + + + + + + + + + +
+ 文件名 + + 文件尺寸 + + 状态 + + 操作 +
+
+ 点击上方“选择文件”或将文件拖到此区域 +
+
+ +
+ + +
+
+ + + +
+
+`; + exports[`Upload Upload fileFlowListVue demo works fine 1`] = `