├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README-en.md
├── README.md
├── demo
├── env.d.ts
├── index.html
├── src
│ ├── App.vue
│ └── main.ts
└── vite.config.js
├── docs
├── .vitepress
│ ├── config.mts
│ └── theme
│ │ ├── index.css
│ │ └── index.ts
├── index.md
├── start.md
├── use.md
└── zh
│ ├── index.md
│ ├── start.md
│ └── use.md
├── images
├── effect-dark.png
└── effect-light.png
├── jest.config.js
├── jest.setup.js
├── package.json
├── rollup.config.ts
├── src
├── ColorPicker.vue
├── add-color-item
│ ├── AddColorItem.vue
│ └── index.ts
├── color-item
│ ├── ColorItem.vue
│ └── index.ts
├── constant.ts
├── hooks
│ └── usePopper.ts
├── index.ts
├── picker
│ ├── Alpha.vue
│ ├── Colors.vue
│ ├── Hue.vue
│ ├── Picker.vue
│ ├── Saturation.vue
│ ├── index.ts
│ └── input-value
│ │ ├── FormatValue.vue
│ │ ├── InputValue.vue
│ │ └── index.ts
├── shims-vue.ts
└── utils.ts
├── test
├── add-color-item.test.ts
└── color-item.test.ts
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env"]
4 | ],
5 | "plugins": [
6 | "@babel/transform-typescript",
7 | "@babel/transform-runtime"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | *.spec.ts
4 | demo
5 | coverage
6 | images
7 | test
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true
5 | },
6 | extends: [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/standard'
10 | ],
11 | parser: 'vue-eslint-parser',
12 | parserOptions: {
13 | parser: '@typescript-eslint/parser',
14 | sourceType: 'module',
15 | ecmaFeatures: {
16 | jsx: true,
17 | tsx: true
18 | }
19 | },
20 | rules: {
21 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
23 | },
24 | overrides: [
25 | {
26 | files: [
27 | '**/__tests__/*.{j,t}s?(x)',
28 | '**/tests/unit/**/*.spec.{j,t}s?(x)'
29 | ],
30 | env: {
31 | jest: true
32 | }
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | lerna.json
5 | lerna-debug.log
6 | coverage
7 | cache
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | pnpm-debug.log*
17 |
18 | # Editor directories and files
19 | .idea
20 | .vscode
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 ayuan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
1 | ### [vue-pick-colors](https://github.com/qiuzongyuan/vue-pick-colors)
2 |
3 | > 🎉 A Color picker for Vue.js 3
4 |
5 | > [🇨🇳中文](https://github.com/qiuzongyuan/vue-pick-colors/blob/main/README.md)
6 |
7 | ### [Demo](https://qiuzongyuan.github.io/vue-pick-colors/use.html)
8 |
9 |
10 |

11 |

12 |
13 |
14 |
15 |
16 |
17 | ### Installation
18 |
19 | ```
20 | npm install vue-pick-colors
21 | ```
22 |
23 | or
24 |
25 | ```
26 | yarn add vue-pick-colors
27 | ```
28 |
29 |
30 |
31 | ### Usage
32 |
33 | ```
34 |
35 |
36 |
37 |
38 |
42 | ```
43 |
44 |
45 |
46 | ### API
47 |
48 | | Property | Description | Type | Default | version |
49 | | -------------------- | ------------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------ | ------- |
50 | | value(v-model) | Binding value, support hex、rgb、rgba、hsl、hsla、hsv、hsva | string | string[] | — | |
51 | | show-picker(v-model) | Control picker hide or show | boolean | — | 1.5.0 |
52 | | size | Color block size | number \| string | 20 | |
53 | | width | Color block width, if empty use size | number \| string | — | 1.5.0 |
54 | | height | Color block height, if empty use size | number \| string | — | 1.5.0 |
55 | | theme | Component theme | light | dark | light | |
56 | | colors | Predefined color options support hex、rgb、rgba、hsl、hsla、hsva、hsv | string [] | ['#ff4500','#ff8c00','#ffd700', '#90ee90','#00ced1','#1e90ff', '#c71585','#ff4500','#ff7800', '#00babd','#1f93ff','#fa64c3'] | |
57 | | format | Color format | hex | rgb | hsl \| hsv | hex | |
58 | | show-alpha | Whether to display the alpha slider | boolean | false | |
59 | | add-color | Support for adding colors | boolean | false | |
60 | | popup-container | Defines the container for the picker | string \| Vue.RendererElement\| boolean | 'body' | 1.5.0 |
61 | | z-index | The z-index of the picker | number | 1000 | 1.5.0 |
62 | | max | Maximum number of colors to add | number | 13 | |
63 | | format-options | Format options, when false, no options appear | (hex | rgb | hsl | hsv) [] \|false | false | 1.7.0 |
64 | | position | The position of the picker | absolute \|fixed | absolute | 1.7.0 |
65 | | placement | The placement of the picker | bottom \|top \|left \|right | bottom | 1.7.0 |
66 |
67 |
68 |
69 |
70 |
71 | ### Events
72 |
73 | | Events Name | Description | Arguments | version |
74 | | ------------ | ------------------ | ------------------------------------------------------------ | ------- |
75 | | change | color value change | function(value: string|string [],color: string,index: number) | |
76 | | formatChange | format change | function(format: string) | 1.7.0 |
77 | | close-picker | close picker | function(value: string|string []) | 1.5.0 |
78 | | overflow-max | color added to max | — | |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### [vue-pick-colors](https://github.com/qiuzongyuan/vue-pick-colors)
2 |
3 | > 🎉 vue3 颜色拾取器
4 |
5 | > [English](https://github.com/qiuzongyuan/vue-pick-colors/blob/main/README-en.md)
6 |
7 | ### [Demo](https://qiuzongyuan.github.io/vue-pick-colors/zh/use.html)
8 |
9 |
10 |

11 |

12 |
13 |
14 |
15 |
16 |
17 |
18 | ### 安装
19 |
20 | ```
21 | npm install vue-pick-colors
22 | ```
23 |
24 | 或者
25 |
26 | ```
27 | yarn add vue-pick-colors
28 | ```
29 |
30 |
31 |
32 | ### 使用
33 |
34 | ```
35 |
36 |
37 |
38 |
39 |
43 | ```
44 |
45 |
46 |
47 | ### API
48 |
49 | | 属性 | 说明 | 类型 | 默认值 | 版本 |
50 | | -------------------- | ------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------ | ----- |
51 | | value(v-model) | 值,
支持hex、rgb、rgba、hsl、hsla、hsv、hsva | string | string[] | — | |
52 | | show-picker(v-model) | 控制拾取器隐藏或显示 | boolean | — | 1.5.0 |
53 | | size | 颜色块大小 | number \| string | 20 | |
54 | | width | 色块宽度
如果为空使用 `size`属性 | number \| string | — | 1.5.0 |
55 | | height | 色块高度
如果为空使用 `size`属性 | number \| string | — | 1.5.0 |
56 | | theme | 主题 | light | dark | light | |
57 | | colors | 预留颜色组
支持hex、rgb、rgba、hsl、hsla、hsv、hsva | string [] | ['#ff4500','#ff8c00','#ffd700', '#90ee90','#00ced1','#1e90ff', '#c71585','#ff4500','#ff7800', '#00babd','#1f93ff','#fa64c3'] | |
58 | | format | 颜色值格式化 | hex | rgb | hsl | hsv | hex | |
59 | | show-alpha | 是否支持透明度选择 | boolean | false | |
60 | | add-color | 是否支持添加颜色 | boolean | false | |
61 | | popup-container | 定义拾取器的容器 | string \| Vue.RendererElement\| boolean | 'body' | 1.5.0 |
62 | | z-index | 拾取器的层级 | number | 1000 | 1.5.0 |
63 | | max | 添加颜色最大数 | number | 13 | |
64 | | format-options | 格式选项,当为false时,不出现选项 | (hex | rgb | hsl | hsv) [] \| false | false | 1.7.0 |
65 | | position | 定位方式 | absolute \| fixed | absolute | 1.7.0 |
66 | | placement | 弹出窗口的位置 | bottom \| top \| left \| right | bottom | 1.7.0 |
67 |
68 |
69 |
70 | ### 事件
71 |
72 | | 事件名 | 描述 | 参数 | 版本 |
73 | | ------------ | ------------------ | ------------------------------------------------------------ | ----- |
74 | | change | 颜色值变化 | function(value: string|string [],color: string,index: number) | |
75 | | formatChange | 格式变化 | function(format: string) | 1.7.0 |
76 | | close-picker | 关闭拾取器 | function(value: string|string []) | 1.5.0 |
77 | | overflow-max | 颜色添加达到最大值 | — | |
78 |
79 |
--------------------------------------------------------------------------------
/demo/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module '*.vue' {
3 | import { DefineComponent } from 'vue'
4 | const component: DefineComponent<{}, {}, any>
5 | export default component
6 | }
7 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 颜色拾取器
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
82 |
83 |
85 |
--------------------------------------------------------------------------------
/demo/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App)
5 | .mount('#app')
6 |
--------------------------------------------------------------------------------
/demo/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import eslintPlugin from 'vite-plugin-eslint'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | server: {
8 | port: 3000,
9 | host: true
10 | },
11 | plugins: [
12 | eslintPlugin({
13 | cache: false
14 | }),
15 | vue()
16 | ]
17 | })
18 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 |
3 | // https://vitepress.dev/reference/site-config
4 | export default defineConfig({
5 | title: "vue-pick-colors",
6 | description: "A Color picker for Vue.js 3",
7 | base: '/vue-pick-colors/',
8 | locales: {
9 | root: {
10 | label: 'English',
11 | lang: 'en',
12 | link: '/' ,
13 | themeConfig: {
14 | nav: [
15 | { text: 'Guide', link: '/start' },
16 | ],
17 | }
18 | },
19 | zh: {
20 | label: '中文',
21 | lang: 'zh',
22 | link: '/zh/',
23 | themeConfig: {
24 | docFooter: { prev: "上一页", next: "下一页" },
25 | nav: [
26 | { text: '指南', link: '/zh/start' },
27 | ],
28 | }
29 | }
30 | },
31 | themeConfig: {
32 | outlineTitle: " ",
33 |
34 | sidebar: {
35 | '/': [
36 | {
37 | text: 'Guide',
38 | items: [
39 | { text: 'Get Started', link: '/start' },
40 | { text: 'Usage', link: '/use' }
41 | ]
42 | }
43 | ],
44 | '/zh/': [
45 | {
46 | text: '指南',
47 | base: '/zh/',
48 | items: [
49 | { text: '开始', link: '/start' },
50 | { text: '快速上手', link: '/use' }
51 | ]
52 | }
53 | ]
54 | },
55 | socialLinks: [
56 | { icon: 'github', link: 'https://github.com/qiuzongyuan/vue-pick-colors' }
57 | ],
58 | }
59 | })
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.css:
--------------------------------------------------------------------------------
1 | .clip {
2 | font-size: 73px !important;
3 | }
4 |
5 | .primary-button {
6 | color: #fff;
7 | border-color: #1890ff;
8 | background: #1890ff;
9 | text-shadow: 0 -1px 0 rgba(0,0,0,.12);
10 | box-shadow: 0 2px #0000000b;
11 | padding: 3px 20px;
12 | border-radius: 5px;
13 | display: inline;
14 | font-weight: 500;
15 | margin:0 10px;
16 | font-size: 14px;
17 | }
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { AppContext } from 'vue'
2 | import DefaultTheme from 'vitepress/theme'
3 | import PickColors from 'vue-pick-colors'
4 | import './index.css'
5 | export default {
6 | extends: DefaultTheme,
7 | enhanceApp(ctx: AppContext) {
8 | // 注册全局组件
9 | ctx.app.component('PickColors', PickColors)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | hero:
5 | name: vue-pick-colors
6 | text: 🎉 A Color picker for Vue.js 3
7 | actions:
8 | - theme: brand
9 | text: Get Started
10 | link: /start
11 | - theme: alt
12 | text: View on GitHub
13 | link: https://github.com/qiuzongyuan/vue-pick-colors
14 | ---
--------------------------------------------------------------------------------
/docs/start.md:
--------------------------------------------------------------------------------
1 | # Get Started
2 |
3 | ## Installation
4 | ```
5 | # NPM
6 | npm install vue-pick-colors -S
7 |
8 | # Yarn
9 | yarn add vue-pick-colors
10 | ```
11 |
12 | ## Usage
13 |
14 | ```vue
15 |
16 |
17 |
18 |
19 |
23 | ```
--------------------------------------------------------------------------------
/docs/use.md:
--------------------------------------------------------------------------------
1 |
37 |
38 | # Usage
39 |
40 | ## Basic Usage
41 |
42 | `value` is set to `#ff4500`
43 |
44 | ```vue
45 |
46 |
47 |
48 |
49 |
53 | ```
54 |
55 |
56 | ## Alpha
57 |
58 | Use `show-alpha`
59 |
60 | ```vue
61 |
62 |
63 |
64 |
65 |
69 | ```
70 |
71 | ## Format
72 |
73 | `format` is set to `rgb`
74 |
75 | ```vue
76 |
77 |
78 |
79 |
80 |
85 | ```
86 |
87 | ## Format Options
88 |
89 | `format-options` is set to `['rgb', 'hex', 'hsl', 'hsv']`
90 |
91 | ```vue
92 |
93 |
94 |
95 |
96 |
102 | ```
103 |
104 | ## Size
105 |
106 | Use `size`
107 |
108 | If `width` or `height` is empty, use `size`
109 |
110 | `width` is set to `80`
111 |
112 | `height` is set to `80`
113 |
114 | ```vue
115 |
116 |
117 |
118 |
119 |
120 |
121 |
128 | ```
129 |
130 | ## Predefined Colors
131 |
132 | Use `colors`
133 |
134 | ```vue
135 |
136 |
137 |
138 |
139 |
156 | ```
157 |
158 | ## Theme
159 |
160 | Use `theme`
161 |
162 | ```vue
163 |
164 |
165 |
166 |
171 | ```
172 |
173 | ## Control Picker
174 |
175 |
176 | Use show-picker
177 |
178 |
179 | {{ showPicker ? 'close' : 'open' }}
180 |
181 |
182 |
183 | ```vue
184 |
185 |
186 |
187 |
188 |
193 |
207 | ```
208 |
209 |
210 | ## Add Color
211 |
212 | Use `add-color`
213 |
214 | ```vue
215 |
216 |
217 |
218 |
222 | ```
223 |
224 |
225 | ### API
226 | | Property | Description | Type | Default | version |
227 | | -------------------- | ------------------------------------------------------------ | ----------------------------- | ------------------------------------------------------------ | ------- |
228 | | value(v-model) | binding value, support hex、rgb、rgba、hsl、hsla、hsv、hsva | string | string[] | — | |
229 | | show-picker(v-model) | control picker hide or show | boolean | — | 1.5.0 |
230 | | size | color block size | number \| string | 20 | |
231 | | width | color block width, if empty use size | number \| string | — | 1.5.0 |
232 | | height | color block height, if empty use size | number \| string | — | 1.5.0 |
233 | | theme | component theme | light | dark | light | |
234 | | colors | predefined color options support hex、rgb、rgba、hsl、hsla、hsva、hsv | string [] | ['#ff4500','#ff8c00','#ffd700', '#90ee90','#00ced1','#1e90ff', '#c71585','#ff4500','#ff7800', '#00babd','#1f93ff','#fa64c3']
| |
235 | | format | color format | hex | rgb | hsl \| hsv | hex | |
236 | | show-alpha | whether to display the alpha slider | boolean | false | |
237 | | add-color | support for adding colors | boolean | false | |
238 | | popup-container | defines the container for the picker | string \| Vue.RendererElement | 'body' | 1.5.0 |
239 | |z-index | the z-index of the picker | number | 1000 | 1.5.0 |
240 | | max | maximum number of colors to add | number | 13 | |
241 | | format-options | Format options, when false, no options appear | (hex | rgb | hsl | hsv) [] \|false | ['rgb', 'hex', 'hsl', 'hsv'] | 1.7.0 |
242 | | position | The position of the picker | absolute \| fixed | absolute | 1.7.0 |
243 | | placement | The placement of the picker | bottom \| top \| left \| right | bottom | 1.7.0 |
244 |
245 | ### Events
246 |
247 | | Events Name | Description | Arguments | version |
248 | | ------------ | ------------------ | ------------------------------------------------------------ | ------- |
249 | | change | color value change | function(value: string|string [],color: string,index: number) | |
250 | | formatChange | format change | function(format: string) | 1.7.0 |
251 | | close-picker | close picker | function(value: string|string []) | 1.5.0 |
252 | | overflow-max
| color added to max | — | |
253 |
--------------------------------------------------------------------------------
/docs/zh/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | hero:
5 | name: vue-pick-colors
6 | text: 🎉 一款 Vue3.x 颜色拾取器
7 | actions:
8 | - theme: brand
9 | text: 开始
10 | link: /zh/start
11 | - theme: alt
12 | text: GitHub
13 | link: https://github.com/qiuzongyuan/vue-pick-colors
14 | ---
--------------------------------------------------------------------------------
/docs/zh/start.md:
--------------------------------------------------------------------------------
1 | # 开始
2 |
3 | ## 安装
4 | ```
5 | # 使用 NPM
6 | npm install vue-pick-colors -S
7 |
8 | # 使用 Yarn
9 | yarn add vue-pick-colors
10 | ```
11 |
12 | ## 使用
13 |
14 | ```vue
15 |
16 |
17 |
18 |
19 |
23 | ```
--------------------------------------------------------------------------------
/docs/zh/use.md:
--------------------------------------------------------------------------------
1 |
37 |
38 | # 快速上手
39 |
40 | ## 基本使用
41 |
42 | `value` 设置为 `#ff4500`
43 |
44 | ```vue
45 |
46 |
47 |
48 |
49 |
53 | ```
54 |
55 |
56 | ## 使用透明度
57 |
58 | 使用 `show-alpha`
59 |
60 | ```vue
61 |
62 |
63 |
64 |
65 |
69 | ```
70 |
71 | ## 设置格式化
72 |
73 | `format` 设置为 `rgb`
74 |
75 | ```vue
76 |
77 |
78 |
79 |
80 |
85 | ```
86 |
87 |
88 | ## 设置格式选项
89 |
90 | `format-options` 设置为 `['rgb', 'hex', 'hsl', 'hsv']`
91 |
92 | ```vue
93 |
94 |
95 |
96 |
97 |
103 | ```
104 |
105 | ## 设置尺寸
106 |
107 | 使用 `size`
108 |
109 | 如果 `width` 或者 `height` 为空,则使用 `size`
110 |
111 | `width` 设置 `80`
112 |
113 | `height` 设置 `80`
114 |
115 | ```vue
116 |
117 |
118 |
119 |
120 |
121 |
122 |
129 | ```
130 |
131 | ## 设置预定义颜色
132 |
133 | 使用 `colors`
134 |
135 | ```vue
136 |
137 |
138 |
139 |
140 |
157 | ```
158 |
159 | ## 使用主题
160 |
161 | 使用 `theme`
162 |
163 | ```vue
164 |
165 |
166 |
167 |
172 | ```
173 |
174 | ## 控制拾取器
175 |
176 |
177 | 使用 show-picker
178 |
179 | {{ showPicker ? '关闭' : '打开' }}
180 |
181 |
182 | ```vue
183 |
184 |
185 |
186 |
187 |
192 |
206 | ```
207 |
208 |
209 | ## 添加颜色
210 |
211 | 使用 `add-color`
212 |
213 | ```vue
214 |
215 |
216 |
217 |
221 | ```
222 |
223 | ## API
224 |
225 | | 属性 | 说明 | 类型 | | 版本 |
226 | | -------------------- | ------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------ | ----- |
227 | | value(v-model) | 值,
支持hex、rgb、rgba、hsl、hsla、hsv、hsva | string | string[] | — | |
228 | | show-picker(v-model) | 控制拾取器隐藏或显示 | boolean | — | 1.5.0 |
229 | | size | 颜色块大小 | number \| string | 20 | |
230 | | width | 色块宽度
如果为空使用 `size`属性 | number \| string | — | 1.5.0 |
231 | | height | 色块高度
如果为空使用 `size`属性 | number \| string | — | 1.5.0 |
232 | | theme | 主题 | light | dark | light | |
233 | | colors | 预留颜色组
支持hex、rgb、rgba、hsl、hsla、hsv、hsva | string [] | ['#ff4500','#ff8c00','#ffd700', '#90ee90','#00ced1','#1e90ff', '#c71585','#ff4500','#ff7800', '#00babd','#1f93ff','#fa64c3']
| |
234 | | format | 颜色值格式化 | hex | rgb | hsl | hsv | hex | |
235 | | show-alpha | 是否支持透明度选择 | boolean | false | |
236 | | add-color | 是否支持添加颜色 | boolean | false | |
237 | | popup-container | 定义拾取器的容器 | string \| Vue.RendererElement | 'body' | 1.5.0 |
238 | | z-index | 拾取器的层级 | number | 1000 | 1.5.0 |
239 | | max | 添加颜色最大数 | number | 13 | |
240 | | format-options | 格式选项,当为false时,不出现选项 | (hex | rgb | hsl | hsv) [] \| false | ['rgb', 'hex', 'hsl', 'hsv'] | 1.7.0 |
241 | | position | 定位方式 | absolute \| fixed | absolute | 1.7.0 |
242 | | placement | 弹出窗口的位置 | bottom \| top \| left \| right | bottom | 1.7.0 |
243 |
244 | ## 事件
245 |
246 | | 事件名 | 描述 | 参数 | 版本 |
247 | | ------------ | ------------------ | ------------------------------------------------------------ | ----- |
248 | | change | 颜色值变化 | function(value: string|string [],color: string,index: number) | |
249 | | formatChange | 格式变化 | function(format: string) | 1.7.0 |
250 | | close-picker | 关闭拾取器 | function(value: string|string []) | 1.5.0 |
251 | | overflow-max
| 颜色添加达到最大值 | — | |
252 |
253 |
--------------------------------------------------------------------------------
/images/effect-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiuzongyuan/vue-pick-colors/d59c4787649382b5f08040a657c41f4917fb511c/images/effect-dark.png
--------------------------------------------------------------------------------
/images/effect-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiuzongyuan/vue-pick-colors/d59c4787649382b5f08040a657c41f4917fb511c/images/effect-light.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | globals: {
3 | // work around: https://github.com/kulshekhar/ts-jest/issues/748#issuecomment-423528659
4 | 'ts-jest': {
5 | diagnostics: {
6 | ignoreCodes: [151001]
7 | }
8 | }
9 | },
10 | setupFiles: ['./jest.setup.js'],
11 | testPathIgnorePatterns: ['/node_modules/', 'dist', 'build'],
12 | modulePathIgnorePatterns: ['/node_modules/', 'dist', 'build'],
13 | testEnvironment: 'jsdom',
14 | transform: {
15 | // Doesn't support jsx/tsx since sucrase doesn't support Vue JSX
16 | '\\.(j|t)s$': '@sucrase/jest-plugin',
17 | '^.+\\.vue$': 'vue-jest'
18 | },
19 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
20 | // u can change this option to a more specific folder for test single component or util when dev
21 | // for example, ['/packages/input']
22 | roots: ['/test']
23 | }
24 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | const { config } = require('@vue/test-utils')
2 | const _ResizeObserver = require('resize-observer-polyfill')
3 |
4 | config.global.stubs = {}
5 |
6 | global.ResizeObserver = _ResizeObserver
7 | process.addListener('unhandledRejection', (err) => console.error(err))
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-pick-colors",
3 | "version": "1.8.0",
4 | "description": "A Color picker for Vue.js 3",
5 | "main": "dist/index.umd.js",
6 | "module": "dist/index.esm.js",
7 | "typings": "dist/index.d.ts",
8 | "types": "dist/index.d.ts",
9 | "files": [
10 | "dist"
11 | ],
12 | "publishConfig": {
13 | "access": "public"
14 | },
15 | "scripts": {
16 | "build": "npm run clean && npm run lint && rollup -c ./rollup.config.ts",
17 | "build:dev": "npm run clean && npm run lint && rollup -wc ./rollup.config.ts",
18 | "dev": "vite ./demo",
19 | "lint": "eslint ./src --ext .vue,.js,.ts,.jsx,.tsx",
20 | "lint:fix": "eslint --fix ./src --ext .vue,.js,.ts,.jsx,.tsx",
21 | "clean": "rimraf ./dist",
22 | "test": "jest",
23 | "prepublishOnly": "npm run build",
24 | "test:coverage": "jest --coverage",
25 | "docs": "vitepress dev docs",
26 | "docs:build": "vitepress build docs",
27 | "docs:preview": "vitepress preview docs"
28 | },
29 | "keywords": [
30 | "vue",
31 | "vue3",
32 | "vuejs",
33 | "color",
34 | "picker",
35 | "pick",
36 | "component",
37 | "color picker",
38 | "vue-pick-colors",
39 | "vue pick colors"
40 | ],
41 | "author": "ayuan",
42 | "license": "MIT",
43 | "repository": {
44 | "type": "git",
45 | "url": "git+https://github.com/qiuzongyuan/vue-pick-colors.git"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "^7.16.7",
49 | "@babel/plugin-transform-runtime": "^7.16.8",
50 | "@babel/plugin-transform-typescript": "^7.22.11",
51 | "@babel/preset-env": "^7.16.8",
52 | "@rollup/plugin-babel": "^5.3.0",
53 | "@rollup/plugin-commonjs": "^21.0.1",
54 | "@rollup/plugin-json": "^4.1.0",
55 | "@rollup/plugin-node-resolve": "^13.1.3",
56 | "@sucrase/jest-plugin": "^2.2.0",
57 | "@types/jest": "^26.0.23",
58 | "@typescript-eslint/parser": "^4.31.2",
59 | "@vitejs/plugin-vue": "^2.0.1",
60 | "@vue/compiler-sfc": "^3.2.26",
61 | "@vue/eslint-config-standard": "^6.1.0",
62 | "@vue/test-utils": "2.0.0-rc.17",
63 | "autoprefixer": "^10.4.2",
64 | "babel-jest": "^26.6.3",
65 | "eslint": "7.32.0",
66 | "eslint-plugin-import": "^2.25.4",
67 | "eslint-plugin-node": "^11.1.0",
68 | "eslint-plugin-promise": "^5.1.1",
69 | "eslint-plugin-vue": "^7.20.0",
70 | "jest": "^26.6.3",
71 | "less": "^4.1.2",
72 | "postcss": "^8.4.5",
73 | "resize-observer-polyfill": "^1.5.1",
74 | "rimraf": "^3.0.2",
75 | "rollup": "^2.63.0",
76 | "rollup-plugin-commonjs": "^10.1.0",
77 | "rollup-plugin-css-only": "^3.1.0",
78 | "rollup-plugin-dts": "5.3.1",
79 | "rollup-plugin-filesize": "^9.1.2",
80 | "rollup-plugin-postcss": "^4.0.2",
81 | "rollup-plugin-terser": "^7.0.2",
82 | "rollup-plugin-vue": "^6.0.0",
83 | "ts-jest": "^26.5.6",
84 | "typescript": "4.4.4",
85 | "vite": "^2.7.12",
86 | "vite-plugin-dts": "^3.5.2",
87 | "vite-plugin-eslint": "^1.3.0",
88 | "vitepress": "1.0.0-rc.5",
89 | "vue": "^3.2.26",
90 | "vue-jest": "^5.0.0-alpha.10"
91 | },
92 | "peerDependencies": {
93 | "@popperjs/core": "^2.11.2",
94 | "vue": "^3.2.26"
95 | },
96 | "dependencies": {
97 | "@popperjs/core": "^2.11.2"
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { nodeResolve } from '@rollup/plugin-node-resolve'
3 | import commonjs from '@rollup/plugin-commonjs'
4 | import babel from '@rollup/plugin-babel'
5 | import json from '@rollup/plugin-json'
6 | import dts from 'vite-plugin-dts'
7 | import vue from 'rollup-plugin-vue'
8 | import postcss from 'rollup-plugin-postcss'
9 | import cssnano from 'cssnano'
10 | import { terser } from 'rollup-plugin-terser'
11 | import filesize from 'rollup-plugin-filesize'
12 | const inputPath = resolve(__dirname, './src/index.ts')
13 | const outputPath = (t) => resolve(__dirname, `./dist/index.${t}.js`)
14 | const extensions = [
15 | '.js',
16 | '.jsx',
17 | '.ts',
18 | '.tsx',
19 | 'vue'
20 | ]
21 | const exclude = [
22 | '**/node_modules/**',
23 | '**/dist/**'
24 | ]
25 |
26 | const globals = {
27 | vue: 'Vue',
28 | '@popperjs/core': 'Popperjs'
29 | }
30 |
31 | module.exports = {
32 | input: inputPath,
33 | output: [{
34 | file: outputPath('umd'),
35 | format: 'umd',
36 | name: 'index',
37 | exports: 'named',
38 | globals
39 | }, {
40 | file: outputPath('esm'),
41 | format: 'es',
42 | name: 'index.module',
43 | exports: 'named',
44 | globals
45 | }],
46 | plugins: [
47 | vue({
48 | preprocessStyles: true,
49 | postcssPlugins: [cssnano()]
50 | }),
51 | nodeResolve({
52 | extensions
53 | }),
54 | commonjs(),
55 | postcss(),
56 | json(),
57 | babel({
58 | exclude,
59 | babelHelpers: 'runtime',
60 | extensions
61 | }),
62 | dts({
63 | rollupTypes: true
64 | }),
65 | terser({
66 | compress: {
67 | // drop_console: true
68 | }
69 | }),
70 | filesize()
71 | ],
72 | external: ['vue', '@popperjs/core']
73 | }
74 |
--------------------------------------------------------------------------------
/src/ColorPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
24 |
31 |
32 |
33 |
46 |
47 |
48 |
49 |
50 |
51 |
385 |
386 |
418 |
--------------------------------------------------------------------------------
/src/add-color-item/AddColorItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
39 |
40 |
67 |
--------------------------------------------------------------------------------
/src/add-color-item/index.ts:
--------------------------------------------------------------------------------
1 | import AddColorItem from './AddColorItem.vue'
2 | export default AddColorItem
3 |
--------------------------------------------------------------------------------
/src/color-item/ColorItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
94 |
95 |
101 |
--------------------------------------------------------------------------------
/src/color-item/index.ts:
--------------------------------------------------------------------------------
1 | import ColorItem from './ColorItem.vue'
2 | export default ColorItem
3 |
--------------------------------------------------------------------------------
/src/constant.ts:
--------------------------------------------------------------------------------
1 | export type Theme = 'light' | 'dark'
2 | export type Format = 'rgb' | 'hex' | 'hsl' | 'hsv'
3 | export const ALPHA_FORMAT_MAP = {
4 | rgb: 'RGBA',
5 | hex: 'HEX',
6 | hsl: 'HSLA',
7 | hsv: 'HSVA'
8 | }
9 | export const FORMAT_MAP = {
10 | rgb: 'RGB',
11 | hex: 'HEX',
12 | hsl: 'HSL',
13 | hsv: 'HSV'
14 | }
15 | export const FORMAT_VALUE_MAP = {
16 | RGB: 'rgb',
17 | RGBA: 'rgb',
18 | HEX: 'hex',
19 | HSL: 'hsl',
20 | HSLA: 'hsl',
21 | HSV: 'hsv',
22 | HSVA: 'hsv'
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/usePopper.ts:
--------------------------------------------------------------------------------
1 | import { Ref, nextTick, onBeforeUnmount, ref, unref, watch } from 'vue'
2 | import type { CSSProperties } from 'vue'
3 | import { Options, Instance, createPopper, Placement, PositioningStrategy } from '@popperjs/core'
4 | interface PopperOptions {
5 | strategy?: PositioningStrategy
6 | placement?:Placement
7 | defaultStyle?:Partial
8 | }
9 | let instance: Instance = null
10 | const usePopper = (target: Ref, popper: Ref, popperOptions?:PopperOptions) => {
11 | const style = ref>({})
12 | const { placement, defaultStyle, strategy } = popperOptions || {}
13 | const options: Options = {
14 | strategy: strategy || 'absolute',
15 | placement: placement || 'auto',
16 | onFirstUpdate: () => {
17 | instance.update()
18 | },
19 | modifiers: [
20 | {
21 | name: 'offset',
22 | options: {
23 | offset: [0, 5]
24 | }
25 | },
26 | {
27 | name: 'computeStyles',
28 | options: {
29 | gpuAcceleration: false,
30 | adaptive: true
31 | }
32 | },
33 | {
34 | name: 'flip',
35 | options: {
36 | allowedAutoPlacements: ['top', 'bottom']
37 | }
38 | },
39 | {
40 | name: 'applyStyles',
41 | enabled: false
42 | },
43 | {
44 | name: 'updateState',
45 | enabled: true,
46 | phase: 'write',
47 | requires: ['computeStyles'],
48 | fn: ({ state }) => {
49 | const { styles, placement } = state
50 | const { popper } = styles
51 | style.value = {
52 | ...popper as CSSProperties,
53 | ...defaultStyle,
54 | transformOrigin: placement === 'top' ? 'center bottom' : 'center top'
55 | }
56 | }
57 | }
58 | ]
59 | }
60 | watch(() => [unref(target), unref(popper)], ([target, popper], [oldTarget, oldPopper]) => {
61 | if (!target || !popper) return
62 | if (oldTarget === target && oldPopper === oldTarget) return
63 | instance?.destroy()
64 | const _target = target.$el || target
65 | const _popper = popper.$el || popper
66 | nextTick(() => {
67 | instance = createPopper(_target, _popper, options)
68 | })
69 | })
70 | onBeforeUnmount(() => {
71 | if (instance) {
72 | instance?.destroy()
73 | instance = null
74 | }
75 | })
76 | return {
77 | instance,
78 | style
79 | }
80 | }
81 |
82 | export default usePopper
83 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import ColorPicker from './ColorPicker.vue'
2 |
3 | export default ColorPicker
4 |
5 | export * from './constant'
6 |
--------------------------------------------------------------------------------
/src/picker/Alpha.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
132 |
133 |
149 |
--------------------------------------------------------------------------------
/src/picker/Colors.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
49 |
50 |
61 |
--------------------------------------------------------------------------------
/src/picker/Hue.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
115 |
116 |
132 |
--------------------------------------------------------------------------------
/src/picker/Picker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
250 |
251 |
278 |
--------------------------------------------------------------------------------
/src/picker/Saturation.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
116 |
117 |
150 |
--------------------------------------------------------------------------------
/src/picker/index.ts:
--------------------------------------------------------------------------------
1 | import Picker from './Picker.vue'
2 | export default Picker
3 |
--------------------------------------------------------------------------------
/src/picker/input-value/FormatValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
85 |
86 |
170 |
--------------------------------------------------------------------------------
/src/picker/input-value/InputValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
18 |
19 |
20 |
21 |
82 |
83 |
108 |
--------------------------------------------------------------------------------
/src/picker/input-value/index.ts:
--------------------------------------------------------------------------------
1 | import InputValue from './InputValue.vue'
2 | export default InputValue
3 |
--------------------------------------------------------------------------------
/src/shims-vue.ts:
--------------------------------------------------------------------------------
1 | // 声明 vue 文件
2 | declare module '*.vue' {
3 | import type { DefineComponent } from 'vue'
4 | const component: DefineComponent<{}, {}, any>
5 | export default component
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { Format } from './constant'
2 |
3 | export const hsv2hsl = (h: number, s: number, v: number) => {
4 | return [
5 | h,
6 | (s * v) / ((h = (2 - s) * v) < 1 ? h : 2 - h) || 0,
7 | h / 2
8 | ]
9 | }
10 |
11 | const INT_HEX_MAP = { 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F' }
12 |
13 | const hexOne = (value: number) => {
14 | value = Math.min(Math.round(value), 255)
15 | const high = Math.floor(value / 16)
16 | const low = value % 16
17 | return `${INT_HEX_MAP[high] || high}${INT_HEX_MAP[low] || low}`
18 | }
19 |
20 | export const rgb2hex = ({ r, g, b }) => {
21 | if (isNaN(r) || isNaN(g) || isNaN(b)) return ''
22 |
23 | return `#${hexOne(r)}${hexOne(g)}${hexOne(b)}`
24 | }
25 |
26 | const isOnePointZero = (n: unknown) => {
27 | return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1
28 | }
29 |
30 | const isPercentage = (n: unknown) => {
31 | return typeof n === 'string' && n.indexOf('%') !== -1
32 | }
33 |
34 | const bound01 = (value: number | string, max: number | string) => {
35 | if (isOnePointZero(value)) value = '100%'
36 |
37 | const processPercent = isPercentage(value)
38 | value = Math.min(max as number, Math.max(0, parseFloat(`${value}`)))
39 |
40 | if (processPercent) {
41 | value = parseInt(`${value * (max as number)}`, 10) / 100
42 | }
43 |
44 | if (Math.abs(value - (max as number)) < 0.000001) {
45 | return 1
46 | }
47 |
48 | return (value % (max as number)) / parseFloat(max as string)
49 | }
50 |
51 | export const hsv2rgb = (h, s, v) => {
52 | h = bound01(h, 360) * 6
53 | s = bound01(s, 100)
54 | v = bound01(v, 100)
55 |
56 | const i = Math.floor(h)
57 | const f = h - i
58 | const p = v * (1 - s)
59 | const q = v * (1 - f * s)
60 | const t = v * (1 - (1 - f) * s)
61 | const mod = i % 6
62 | const r = [v, q, p, p, t, v][mod]
63 | const g = [t, v, v, q, p, p][mod]
64 | const b = [p, p, t, v, v, q][mod]
65 |
66 | return {
67 | r: Math.round(r * 255),
68 | g: Math.round(g * 255),
69 | b: Math.round(b * 255)
70 | }
71 | }
72 |
73 | export const roundToTwoDecimals = (n: number) => {
74 | const numberString = n.toString()
75 | const regex = /\.(\d{1,2})(\d*)/
76 | const match = numberString.match(regex)
77 | if (match && match[2].length > 0) {
78 | return parseFloat(n.toFixed(2))
79 | }
80 | return n
81 | }
82 |
83 | export const hsvFormat = ({ h, s, v, a }, format: Format, useAlpha: boolean) => {
84 | if (useAlpha) {
85 | if (['hsl', 'hsv', 'rga'].includes(format)) a = roundToTwoDecimals(a)
86 | switch (format) {
87 | case 'hsl': {
88 | const hsl = hsv2hsl(h, s / 100, v / 100)
89 | return `hsla(${(h).toFixed(0)}, ${Math.round(hsl[1] * 100)}%, ${Math.round(hsl[2] * 100)}%, ${a})`
90 | }
91 | case 'hsv': {
92 | return `hsva(${(h).toFixed(0)}, ${Math.round(s)}%, ${Math.round(v)}%, ${a})`
93 | }
94 | case 'rgb': {
95 | const { r, g, b } = hsv2rgb(h, s, v)
96 | return `rgba(${r}, ${g}, ${b}, ${a})`
97 | }
98 | case 'hex':
99 | default:
100 | return `${rgb2hex(hsv2rgb(h, s, v))}${hexOne(a * 255)}`
101 | }
102 | } else {
103 | switch (format) {
104 | case 'hsl': {
105 | const hsl = hsv2hsl(h, s / 100, v / 100)
106 | return `hsl(${(h).toFixed(0)}, ${Math.round(hsl[1] * 100)}%, ${Math.round(hsl[2] * 100)}%)`
107 | }
108 | case 'hsv': {
109 | return `hsv(${(h).toFixed(0)}, ${Math.round(s)}%, ${Math.round(v)}%)`
110 | }
111 | case 'rgb': {
112 | const { r, g, b } = hsv2rgb(h, s, v)
113 | return `rgb(${r}, ${g}, ${b})`
114 | }
115 | case 'hex':
116 | default :
117 | return rgb2hex(hsv2rgb(h, s, v))
118 | }
119 | }
120 | }
121 |
122 | export const rgb2hsv = ({ r, g, b }) => {
123 | r = bound01(r, 255)
124 | g = bound01(g, 255)
125 | b = bound01(b, 255)
126 |
127 | const max = Math.max(r, g, b)
128 | const min = Math.min(r, g, b)
129 | let h
130 | const v = max
131 |
132 | const d = max - min
133 | const s = max === 0 ? 0 : d / max
134 |
135 | if (max === min) {
136 | h = 0 // achromatic
137 | } else {
138 | switch (max) {
139 | case r: {
140 | h = (g - b) / d + (g < b ? 6 : 0)
141 | break
142 | }
143 | case g: {
144 | h = (b - r) / d + 2
145 | break
146 | }
147 | case b: {
148 | h = (r - g) / d + 4
149 | break
150 | }
151 | }
152 | h /= 6
153 | }
154 |
155 | return { h: h * 360, s: s * 100, v: v * 100 }
156 | }
157 |
158 | export const hsl2hsv = ({ h, s, l }) => {
159 | s = s / 100
160 | l = l / 100
161 | let sMin = s
162 | const lMin = Math.max(l, 0.01)
163 |
164 | l *= 2
165 | s *= l <= 1 ? l : 2 - l
166 | sMin *= lMin <= 1 ? lMin : 2 - lMin
167 | const v = (l + s) / 2
168 | const sv =
169 | l === 0 ? (2 * sMin) / (lMin + sMin) : (2 * s) / (l + s)
170 |
171 | return {
172 | h: +h,
173 | s: sv * 100,
174 | v: v * 100
175 | }
176 | }
177 |
178 | export const hex2rgb = (hex: string) => {
179 | const temp = [] as number []
180 | if (hex.match(/^#([0-9a-fA-f]{3,4})$/g)) {
181 | for (let i = 1; i < hex.length; i++) {
182 | temp.push(parseInt('0x' + hex[i].repeat(2)))
183 | }
184 | } else if (hex.match(/^#([0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/g)) {
185 | for (let i = 1; i < hex.length; i = i + 2) {
186 | temp.push(parseInt('0x' + hex.slice(i, i + 2)))
187 | }
188 | }
189 | const [r, g, b, a] = temp
190 | return {
191 | r,
192 | g,
193 | b,
194 | a
195 | }
196 | }
197 |
198 | export const colorFormat = (color: unknown, format: Format, useAlpha: boolean) => {
199 | if (typeof color === 'string' && color !== '') {
200 | const hsv = transformHsva(color, checkColorFormat(color), useAlpha)
201 | const filterHsv = filterHsva(hsv)
202 | if (filterHsv == null) return ''
203 | return hsvFormat(filterHsv, format, useAlpha)
204 | }
205 | return ''
206 | }
207 |
208 | const pickUpRgb = (rgb:string) => {
209 | const [r, g, b, a] = rgb.match(/(\d(\.\d+)?)+/g)
210 | return {
211 | r,
212 | g,
213 | b,
214 | a
215 | }
216 | }
217 |
218 | const pickUpHsl = (hsl: string) => {
219 | const [h, s, l, a] = hsl.match(/(\d(\.\d+)?)+/g)
220 | return {
221 | h,
222 | s: parseFloat(s),
223 | l: parseFloat(l),
224 | a
225 | }
226 | }
227 |
228 | const pickUpHsv = (hsv: string) => {
229 | const [h, s, v, a] = hsv.match(/(\d(\.\d+)?)+/g)
230 | return {
231 | h: parseFloat(h),
232 | s: parseFloat(s),
233 | v: parseFloat(v),
234 | a: parseFloat(a)
235 | }
236 | }
237 |
238 | export const transformHsva = (color: string, format: Format, useAlpha = true): { h: number, s: number, v: number, a: number } => {
239 | if (useAlpha) {
240 | switch (format) {
241 | case 'rgb': {
242 | const { r, g, b, a } = pickUpRgb(color)
243 | return { ...rgb2hsv({ r, g, b }), a: +a }
244 | }
245 | case 'hsv': {
246 | const { h, s, v, a } = pickUpHsv(color)
247 | return { h, s, v, a }
248 | }
249 | case 'hsl': {
250 | const { h, s, l, a } = pickUpHsl(color)
251 | return { ...hsl2hsv({ h, s, l }), a: +a }
252 | }
253 | case 'hex':
254 | default:
255 | {
256 | const { r, g, b, a } = hex2rgb(color)
257 | return { ...rgb2hsv({ r, g, b }), a: a / 255 }
258 | }
259 | }
260 | } else {
261 | const a = 1
262 | switch (format) {
263 | case 'rgb': {
264 | return { ...rgb2hsv(pickUpRgb(color)), a }
265 | }
266 | case 'hsv': {
267 | const { h, s, v } = pickUpHsv(color)
268 | return { h, s, v, a: 1 }
269 | }
270 | case 'hsl': {
271 | return { ...hsl2hsv(pickUpHsl(color)), a }
272 | }
273 | case 'hex':
274 | default:
275 | return { ...rgb2hsv(hex2rgb(color)), a }
276 | }
277 | }
278 | }
279 |
280 | export const checkColor = (color: string, format, useAlpha = true) => {
281 | if (useAlpha) {
282 | switch (format) {
283 | case 'hex':
284 | return color.match(/^#([0-9a-fA-F]{8})$/g)
285 | case 'rgb':
286 | return color.match(/^rgba\((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9]),(\s*)(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9]),(\s*)(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9]),(\s*)(0\.\d{1,2}|1|0)\)/g)
287 | case 'hsl':
288 | return color.match(/^hsla\((((([0-9]|([1-9][0-9])|([0-2][0-9][0-9])|([3][0-5][0-9])|([0]{1}))|360).[0-9]?[0-9])|(([0-9]|([1-9][0-9])|([0-2][0-9][0-9])|([3][0-5][0-9])|([0]{1}))|360)),(\s*)([0-9]?[0-9]|100)%,(\s*)([0-9]?[0-9]|100)%,(\s*)(0\.\d{1,2}|1|0)\)/g)
289 | }
290 | } else {
291 | switch (format) {
292 | case 'hex':
293 | return color.match(/^#([0-9a-fA-F]{6})$/g)
294 | case 'rgb':
295 | return color.match(/^rgb\((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9]),(\s*)(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9]),(\s*)(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\)/g)
296 | case 'hsl':
297 | return color.match(/^hsl\((((([0-9]|([1-9][0-9])|([0-2][0-9][0-9])|([3][0-5][0-9])|([0]{1}))|360).[0-9]?[0-9])|(([0-9]|([1-9][0-9])|([0-2][0-9][0-9])|([3][0-5][0-9])|([0]{1}))|360)),(\s*)([0-9]?[0-9]|100)%,(\s*)([0-9]?[0-9]|100)%\)/g)
298 | }
299 | }
300 | }
301 |
302 | export const checkColorValue = (color: string, format: Format, useAlpha: boolean) => {
303 | if (useAlpha) {
304 | switch (format) {
305 | case 'rgb':
306 | return (/^[rR][gG][Bb][Aa][(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\s]*,){3}[\s]*(1|1.0|0|0.[0-9]|0.[0-9][0-9])[\s]*[)]{1}$/).test(color)
307 | case 'hsv':
308 | return (/^[hH][Ss][Vv][Aa][(]([\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*,){2}([\s]*(1|1.0|0|0.[0-9]|0.[0-9][0-9])[\s]*)[)]$/).test(color)
309 | case 'hsl':
310 | return (/^[hH][Ss][Ll][Aa][(]([\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*,){2}([\s]*(1|1.0|0|0.[0-9]|0.[0-9][0-9])[\s]*)[)]$/).test(color)
311 | case 'hex':
312 | default:
313 | return (/^#([0-9a-fA-f]{4}|[0-9a-fA-F]{8})$/g).test(color)
314 | }
315 | } else {
316 | switch (format) {
317 | case 'rgb':
318 | return (/^[rR][gG][Bb][(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\s]*,){2}[\s]*(2[0-4]\d|25[0-5]|[01]?\d\d?)[\s]*[)]{1}$/).test(color)
319 | case 'hsv':
320 | return (/^[hH][Ss][Vv][(]([\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*)[)]$/).test(color)
321 | case 'hsl':
322 | return (/^[hH][Ss][Ll][(]([\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*,)([\s]*((100|[0-9][0-9]?)%|0)[\s]*)[)]$/).test(color)
323 | case 'hex':
324 | default:
325 | return (/^#([0-9a-fA-f]{3}|[0-9a-fA-F]{6})$/g).test(color)
326 | }
327 | }
328 | }
329 |
330 | export const checkColorFormat = (color: string) => {
331 | if (color.match(/^#/)) return 'hex'
332 | if (color.match(/^rgb/)) return 'rgb'
333 | if (color.match(/^hsl/)) return 'hsl'
334 | if (color.match(/^hsv/)) return 'hsv'
335 | return 'hex'
336 | }
337 |
338 | export const filterHsva = ({ h, s, v, a }: { h: number, s:number, v:number, a: number } | null) => {
339 | if (isNaN(h) && isNaN(s) && isNaN(v)) return null
340 | if (isNaN(h)) h = 0
341 | if (isNaN(s)) s = 0
342 | if (isNaN(v)) v = 0
343 | if (isNaN(a)) a = 1
344 | return { h, s, v, a }
345 | }
346 |
347 | export const checkHsva = (hsva: { h: number, s:number, v:number, a: number } | null) => {
348 | if (!hsva) return false
349 | const { h, s, v, a } = hsva
350 | if (isNaN(h)) return false
351 | if (isNaN(s)) return false
352 | if (isNaN(v)) return false
353 | if (isNaN(a)) return false
354 | return true
355 | }
356 |
--------------------------------------------------------------------------------
/test/add-color-item.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from '@jest/globals'
2 | import { mount } from '@vue/test-utils'
3 | import AddColorItem from '../src/add-color-item'
4 | describe('add color item', () => {
5 | it('size', () => {
6 | const size = 26
7 | const wrapper = mount(AddColorItem, {
8 | props: {
9 | size
10 | }
11 | })
12 | const element = wrapper.find('.add-color-item').element as HTMLCanvasElement
13 | expect(element.style.width).toBe(`${size}px`)
14 | expect(element.style.height).toBe(`${size}px`)
15 | })
16 |
17 | it('selected', async () => {
18 | const wrapper = mount(AddColorItem, {
19 | props: {
20 | selected: true
21 | }
22 | })
23 | await wrapper.vm.$nextTick()
24 | const element = wrapper.find('.add-color-item').element as HTMLCanvasElement
25 | expect(element.style.boxShadow).toContain('#1890ff')
26 | })
27 |
28 | it('dark theme and selected', async () => {
29 | const wrapper = mount(AddColorItem, {
30 | props: {
31 | selected: true
32 | },
33 | global: {
34 | provide: {
35 | theme: {
36 | theme: 'dark'
37 | }
38 | }
39 | }
40 | })
41 | await wrapper.vm.$nextTick()
42 | const element = wrapper.find('.add-color-item').element as HTMLCanvasElement
43 | expect(element.style.boxShadow).toContain('#2681ff')
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/test/color-item.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from '@jest/globals'
2 | import { mount } from '@vue/test-utils'
3 | import ColorItem from '../src/color-item'
4 | describe('color item', () => {
5 | it('size', () => {
6 | const size = 26
7 | const wrapper = mount(ColorItem, {
8 | props: {
9 | size
10 | }
11 | })
12 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
13 | expect(element.style.width).toBe(`${size}px`)
14 | expect(element.style.height).toBe(`${size}px`)
15 | })
16 | it('border', () => {
17 | const wrapper = mount(ColorItem, {
18 | props: {
19 | border: false
20 | }
21 | })
22 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
23 | expect(element.style.border).toBe('')
24 | })
25 |
26 | it('value', async () => {
27 | const value = '#333333'
28 | const wrapper = mount(ColorItem, {
29 | props: {
30 | value
31 | }
32 | })
33 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
34 | expect(element.getContext('2d')?.fillStyle).toBe(value)
35 | const changeValue = '#ffffff'
36 | wrapper.setProps({ value: changeValue })
37 | await wrapper.vm.$nextTick()
38 | expect(element.getContext('2d')?.fillStyle).toBe(changeValue)
39 | })
40 |
41 | it('borderRadius', () => {
42 | const borderRadius = 10
43 | const wrapper = mount(ColorItem, {
44 | props: {
45 | borderRadius
46 | }
47 | })
48 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
49 | expect(element.style.borderRadius).toBe(`${borderRadius}px`)
50 | })
51 |
52 | it('theme', () => {
53 | const wrapper = mount(ColorItem, {
54 | global: {
55 | provide: {
56 | theme: {
57 | theme: 'dark'
58 | }
59 | }
60 | }
61 | })
62 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
63 | expect(element.style.border).toContain('#434345')
64 | })
65 |
66 | it('selected', () => {
67 | const wrapper = mount(ColorItem, {
68 | props: {
69 | selected: true
70 | }
71 | })
72 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
73 | expect(element.style.boxShadow).toContain('#1890ff')
74 | })
75 |
76 | it('selected and dark theme', () => {
77 | const wrapper = mount(ColorItem, {
78 | props: {
79 | selected: true
80 | },
81 | global: {
82 | provide: {
83 | theme: {
84 | theme: 'dark'
85 | }
86 | }
87 | }
88 | })
89 | const element = wrapper.find('.color-item').element as HTMLCanvasElement
90 | expect(element.style.boxShadow).toContain('#2681ff')
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictNullChecks": false,
4 | "moduleResolution": "node",
5 | "esModuleInterop": true,
6 | "experimentalDecorators": true,
7 | "jsx": "preserve",
8 | "noUnusedParameters": false,
9 | "noUnusedLocals": true,
10 | "noImplicitAny": false,
11 | "target": "es6",
12 | "skipLibCheck": true,
13 | "allowJs": true,
14 | "declaration": true,
15 | "resolveJsonModule": true,
16 | "baseUrl": ".",
17 | "lib": [
18 | "esnext",
19 | "dom",
20 | "dom.iterable",
21 | "scripthost"
22 | ]
23 | },
24 | "include": [
25 | "src/*"
26 | , "src/hooks/usePopper.ts" ],
27 | "exclude": [
28 | "node_modules/**",
29 | "**/__tests__/**",
30 | "**/*.spec.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------