├── .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 | 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 | 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 | 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 | 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 | 48 | 49 | 53 | ``` 54 | 55 | 56 | ## Alpha 57 | 58 | Use `show-alpha` 59 | 60 | ```vue 61 | 64 | 65 | 69 | ``` 70 | 71 | ## Format 72 | 73 | `format` is set to `rgb` 74 | 75 | ```vue 76 | 79 | 80 | 85 | ``` 86 | 87 | ## Format Options 88 | 89 | `format-options` is set to `['rgb', 'hex', 'hsl', 'hsv']` 90 | 91 | ```vue 92 | 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 | 120 | 121 | 128 | ``` 129 | 130 | ## Predefined Colors 131 | 132 | Use `colors` 133 | 134 | ```vue 135 | 138 | 139 | 156 | ``` 157 | 158 | ## Theme 159 | 160 | Use `theme` 161 | 162 | ```vue 163 | 166 | 171 | ``` 172 | 173 | ## Control Picker 174 | 175 | 176 | Use show-picker 177 | 178 |
179 | {{ showPicker ? 'close' : 'open' }} 180 |
181 |
182 | 183 | ```vue 184 | 188 | 193 | 207 | ``` 208 | 209 | 210 | ## Add Color 211 | 212 | Use `add-color` 213 | 214 | ```vue 215 | 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 | 18 | 19 | 23 | ``` -------------------------------------------------------------------------------- /docs/zh/use.md: -------------------------------------------------------------------------------- 1 | 37 | 38 | # 快速上手 39 | 40 | ## 基本使用 41 | 42 | `value` 设置为 `#ff4500` 43 | 44 | ```vue 45 | 48 | 49 | 53 | ``` 54 | 55 | 56 | ## 使用透明度 57 | 58 | 使用 `show-alpha` 59 | 60 | ```vue 61 | 64 | 65 | 69 | ``` 70 | 71 | ## 设置格式化 72 | 73 | `format` 设置为 `rgb` 74 | 75 | ```vue 76 | 79 | 80 | 85 | ``` 86 | 87 | 88 | ## 设置格式选项 89 | 90 | `format-options` 设置为 `['rgb', 'hex', 'hsl', 'hsv']` 91 | 92 | ```vue 93 | 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 | 121 | 122 | 129 | ``` 130 | 131 | ## 设置预定义颜色 132 | 133 | 使用 `colors` 134 | 135 | ```vue 136 | 139 | 140 | 157 | ``` 158 | 159 | ## 使用主题 160 | 161 | 使用 `theme` 162 | 163 | ```vue 164 | 167 | 172 | ``` 173 | 174 | ## 控制拾取器 175 | 176 | 177 | 使用 show-picker 178 | 179 |
{{ showPicker ? '关闭' : '打开' }}
180 |
181 | 182 | ```vue 183 | 187 | 192 | 206 | ``` 207 | 208 | 209 | ## 添加颜色 210 | 211 | 使用 `add-color` 212 | 213 | ```vue 214 | 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 | 50 | 51 | 385 | 386 | 418 | -------------------------------------------------------------------------------- /src/add-color-item/AddColorItem.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 7 | 8 | 132 | 133 | 149 | -------------------------------------------------------------------------------- /src/picker/Colors.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 49 | 50 | 61 | -------------------------------------------------------------------------------- /src/picker/Hue.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 115 | 116 | 132 | -------------------------------------------------------------------------------- /src/picker/Picker.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 250 | 251 | 278 | -------------------------------------------------------------------------------- /src/picker/Saturation.vue: -------------------------------------------------------------------------------- 1 | 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 | 16 | 17 | 85 | 86 | 170 | -------------------------------------------------------------------------------- /src/picker/input-value/InputValue.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------