From 42bd58d9feb7fcfad597ebac47ef754837740e36 Mon Sep 17 00:00:00 2001 From: Furybean Date: Wed, 8 Feb 2017 00:11:53 +0800 Subject: [PATCH] Add ColorPicker component. --- components.json | 3 +- examples/docs/en-US/color-picker.md | 100 ++++++ examples/docs/zh-CN/color-picker.md | 100 ++++++ examples/nav.config.json | 8 + examples/play/component.vue | 12 +- packages/color-picker/cooking.conf.js | 6 + packages/color-picker/index.js | 8 + packages/color-picker/package.json | 15 + packages/color-picker/src/color.js | 289 ++++++++++++++++++ .../src/components/alpha-slider.vue | 137 +++++++++ .../src/components/hue-slider.vue | 121 ++++++++ .../src/components/picker-dropdown.vue | 74 +++++ .../color-picker/src/components/sv-panel.vue | 97 ++++++ packages/color-picker/src/draggable.js | 34 +++ packages/color-picker/src/main.vue | 95 ++++++ packages/theme-default/src/color-picker.css | 250 +++++++++++++++ packages/theme-default/src/index.css | 1 + src/index.js | 7 +- src/locale/lang/bg.js | 4 + src/locale/lang/da.js | 4 + src/locale/lang/de.js | 4 + src/locale/lang/el.js | 4 + src/locale/lang/en.js | 4 + src/locale/lang/es.js | 4 + src/locale/lang/fa.js | 4 + src/locale/lang/fi.js | 4 + src/locale/lang/fr.js | 4 + src/locale/lang/id.js | 4 + src/locale/lang/it.js | 4 + src/locale/lang/ja.js | 4 + src/locale/lang/ko.js | 4 + src/locale/lang/nb-NO.js | 4 + src/locale/lang/nl.js | 4 + src/locale/lang/pl.js | 4 + src/locale/lang/pt-br.js | 4 + src/locale/lang/pt.js | 4 + src/locale/lang/ru-RU.js | 4 + src/locale/lang/sk.js | 4 + src/locale/lang/sv-SE.js | 4 + src/locale/lang/th.js | 4 + src/locale/lang/tr-TR.js | 4 + src/locale/lang/vi.js | 4 + src/locale/lang/zh-CN.js | 4 + src/locale/lang/zh-TW.js | 4 + test/unit/specs/color-picker.spec.js | 137 +++++++++ 45 files changed, 1593 insertions(+), 5 deletions(-) create mode 100644 examples/docs/en-US/color-picker.md create mode 100644 examples/docs/zh-CN/color-picker.md create mode 100644 packages/color-picker/cooking.conf.js create mode 100644 packages/color-picker/index.js create mode 100644 packages/color-picker/package.json create mode 100644 packages/color-picker/src/color.js create mode 100644 packages/color-picker/src/components/alpha-slider.vue create mode 100644 packages/color-picker/src/components/hue-slider.vue create mode 100644 packages/color-picker/src/components/picker-dropdown.vue create mode 100644 packages/color-picker/src/components/sv-panel.vue create mode 100644 packages/color-picker/src/draggable.js create mode 100644 packages/color-picker/src/main.vue create mode 100644 packages/theme-default/src/color-picker.css create mode 100644 test/unit/specs/color-picker.spec.js diff --git a/components.json b/components.json index 314321185..bddb07f05 100644 --- a/components.json +++ b/components.json @@ -59,5 +59,6 @@ "carousel-item": "./packages/carousel-item/index.js", "collapse": "./packages/collapse/index.js", "collapse-item": "./packages/collapse-item/index.js", - "cascader": "./packages/cascader/index.js" + "cascader": "./packages/cascader/index.js", + "color-picker": "./packages/color-picker/index.js" } diff --git a/examples/docs/en-US/color-picker.md b/examples/docs/en-US/color-picker.md new file mode 100644 index 000000000..cfa4ceb06 --- /dev/null +++ b/examples/docs/en-US/color-picker.md @@ -0,0 +1,100 @@ + + + + +## ColorPicker + +ColorPicker is a color picker component that is used to solve the need to select a color in certain scenes. + +### Color + +:::demo ColorPicker usage is similar to DatePicker and requires v-model to bind a variable in a Vue instance. The bind variable's data type needs to be a string. +```html +
+ Default value + +
+
+ Empty + +
+ + + + +``` +::: + +### Color and alpha + +:::demo ColorPicker supports normal colors, also supports alpha-channel colors, through the show-alpha attribute to control whether to support the use of transparency. +```html +
+ Default value + +
+
+ Empty + +
+ + +``` +::: + +### Attributes +| Attribute | Description | Type | Accepted Values | Default | +|---------- |-------- |---------- |------------- |-------- | +| show-alpha | Whether to display the alpha slider. | Boolean | — | false | +| color-format | Write the v-model's color format. In the case of show-alpha is true, the default value is rgb, otherwise hex. | string | hsl, hsv, hex, rgb | hex | \ No newline at end of file diff --git a/examples/docs/zh-CN/color-picker.md b/examples/docs/zh-CN/color-picker.md new file mode 100644 index 000000000..6465dd3e7 --- /dev/null +++ b/examples/docs/zh-CN/color-picker.md @@ -0,0 +1,100 @@ + + + + +## ColorPicker + +ColorPicker 是一个颜色选择器,该组件是用来解决某些场景下需要选择颜色的需求。 + +### 选择颜色 + +:::demo ColorPicker 用法与 DatePicker 类似,需要使用 v-model 来与 Vue 实例中的一个变量进行双向绑定,绑定的变量需要是字符串类型。 +```html +
+ 有默认值 + +
+
+ 无默认值 + +
+ + + + +``` +::: + +### 选择颜色和透明度 + +:::demo ColorPicker 支持普通颜色,也支持带 Alpha 通道的颜色,通过 show-alpha 属性即可控制是否支持透明度的使用。 +```html +
+ 有默认值 + +
+
+ 无默认值 + +
+ + +``` +::: + +### Attributes +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +|---------- |-------- |---------- |------------- |-------- | +| show-alpha | 是否显示透明度 Slider。 | Boolean | — | false | +| color-format | 写入 v-model 的颜色的格式。在 show-alpha 为 true 的情况下,默认值为 rgb,否则为 hex。 | string | hsl, hsv, hex, rgb | hex | \ No newline at end of file diff --git a/examples/nav.config.json b/examples/nav.config.json index dae15b449..140d006fe 100644 --- a/examples/nav.config.json +++ b/examples/nav.config.json @@ -219,6 +219,10 @@ { "path": "/collapse", "title": "Collapse 折叠面板" + }, + { + "path": "/color-picker", + "title": "ColorPicker" } ] } @@ -445,6 +449,10 @@ { "path": "/collapse", "title": "Collapse" + }, + { + "path": "/color-picker", + "title": "ColorPicker" } ] } diff --git a/examples/play/component.vue b/examples/play/component.vue index d4711e944..4a257ab76 100644 --- a/examples/play/component.vue +++ b/examples/play/component.vue @@ -1,6 +1,11 @@ @@ -13,7 +18,10 @@ }, data() { - return {}; + return { + value: '#bfcbd9', + value2: null + }; } }; diff --git a/packages/color-picker/cooking.conf.js b/packages/color-picker/cooking.conf.js new file mode 100644 index 000000000..e19746a23 --- /dev/null +++ b/packages/color-picker/cooking.conf.js @@ -0,0 +1,6 @@ +var cooking = require('cooking'); +var gen = require('../../build/gen-single-config'); + +cooking.set(gen(__dirname, 'ElColorPicker')); + +module.exports = cooking.resolve(); diff --git a/packages/color-picker/index.js b/packages/color-picker/index.js new file mode 100644 index 000000000..d79e21e08 --- /dev/null +++ b/packages/color-picker/index.js @@ -0,0 +1,8 @@ +import ColorPicker from './src/main'; + +/* istanbul ignore next */ +ColorPicker.install = function(Vue) { + Vue.component(ColorPicker.name, ColorPicker); +}; + +export default ColorPicker; diff --git a/packages/color-picker/package.json b/packages/color-picker/package.json new file mode 100644 index 000000000..1da298ec2 --- /dev/null +++ b/packages/color-picker/package.json @@ -0,0 +1,15 @@ +{ + "name": "element-color-picker", + "version": "0.0.0", + "description": "A color-picker component for Vue.js.", + "keywords": [ + "element", + "vue", + "component" + ], + "main": "./lib/index.js", + "repository": "https://github.com/ElemeFE/element/tree/master/packages/color-picker", + "author": "elemefe", + "license": "MIT", + "dependencies": {} +} diff --git a/packages/color-picker/src/color.js b/packages/color-picker/src/color.js new file mode 100644 index 000000000..e4ea0911f --- /dev/null +++ b/packages/color-picker/src/color.js @@ -0,0 +1,289 @@ +const hsv2hsl = function(hue, sat, val) { + return [ + hue, + (sat * val / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue)) || 0, + hue / 2 + ]; +}; + +// Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 +// +const isOnePointZero = function(n) { + return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1; +}; + +const isPercentage = function(n) { + return typeof n === 'string' && n.indexOf('%') !== -1; +}; + +// Take input from [0, n] and return it as [0, 1] +const bound01 = function(value, max) { + if (isOnePointZero(value)) value = '100%'; + + const processPercent = isPercentage(value); + value = Math.min(max, Math.max(0, parseFloat(value))); + + // Automatically convert percentage into number + if (processPercent) { + value = parseInt(value * max, 10) / 100; + } + + // Handle floating point rounding errors + if ((Math.abs(value - max) < 0.000001)) { + return 1; + } + + // Convert into [0, 1] range if it isn't already + return (value % max) / parseFloat(max); +}; + +const INT_HEX_MAP = { 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F' }; + +const toHex = function({ r, g, b }) { + const hexOne = function(value) { + value = Math.min(Math.round(value), 255); + const high = Math.floor(value / 16); + const low = value % 16; + return '' + (INT_HEX_MAP[high] || high) + (INT_HEX_MAP[low] || low); + }; + + if (isNaN(r) || isNaN(g) || isNaN(b)) return ''; + + return '#' + hexOne(r) + hexOne(g) + hexOne(b); +}; + +const HEX_INT_MAP = { A: 10, B: 11, C: 12, D: 13, E: 14, F: 15 }; + +const parseHexChannel = function(hex) { + if (hex.length === 2) { + return (HEX_INT_MAP[hex[0].toUpperCase()] || +hex[0]) * 16 + (HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1]); + } + + return HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1]; +}; + +const hsl2hsv = function(hue, sat, light) { + sat *= light < 0.5 ? light : 1 - light; + + return [ // [hue, saturation, value] + // Range should be between 0 - 1 + hue, // Hue stays the same + 2 * sat / (light + sat), // Saturation + light + sat // Value + ]; +}; + +// `rgbToHsv` +// Converts an RGB color value to HSV +// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] +// *Returns:* { h, s, v } in [0,1] +const rgb2hsv = function(r, g, b) { + r = bound01(r, 255); + g = bound01(g, 255); + b = bound01(b, 255); + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s; + let v = max; + + const d = max - min; + s = max === 0 ? 0 : d / max; + + if (max === min) { + h = 0; // achromatic + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return { h: Math.round(h * 360), s: Math.round(s * 100), v: Math.round(v * 100) }; +}; + +// `hsvToRgb` +// Converts an HSV color value to RGB. +// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] +// *Returns:* { r, g, b } in the set [0, 255] +const hsv2rgb = function(h, s, v) { + h = bound01(h, 360) * 6; + s = bound01(s, 100); + v = bound01(v, 100); + + const i = Math.floor(h); + const f = h - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + const mod = i % 6; + const r = [v, q, p, p, t, v][mod]; + const g = [t, v, v, q, p, p][mod]; + const b = [p, p, t, v, v, q][mod]; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +}; + +export default class Color { + constructor(options) { + this._hue = 0; + this._saturation = 100; + this._value = 100; + this._alpha = 100; + + this.enableAlpha = false; + this.format = 'hex'; + this.value = ''; + + options = options || {}; + + for (let option in options) { + if (options.hasOwnProperty(option)) { + this[option] = options[option]; + } + } + + this.doOnChange(); + } + + set(prop, value) { + if (arguments.length === 1 && typeof prop === 'object') { + for (let p in prop) { + if (prop.hasOwnProperty(p)) { + this.set(p, prop[p]); + } + } + + return; + } + + this['_' + prop] = value; + this.doOnChange(); + } + + get(prop) { + return this['_' + prop]; + } + + toRgb() { + return hsv2rgb(this._hue, this._saturation, this._value); + } + + fromString(value) { + if (!value) { + this._hue = 0; + this._saturation = 100; + this._value = 100; + + this.doOnChange(); + return; + } + + const fromHSV = (h, s, v) => { + this._hue = h; + this._saturation = s; + this._value = v; + + this.doOnChange(); + }; + + if (value.indexOf('hsl') !== -1) { + const parts = value.replace(/hsla|hsl|\(|\)/gm, '') + .split(/\s|,/g, '').filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10)); + + if (parts.length > 3) { + const { h, s, v } = hsl2hsv(parts[0], parts[1], parts[2]); + fromHSV(h, s, v); + } + if (parts.length === 4) { + this._alpha = Math.floor(parseFloat(parts[3]) * 100); + } + } else if (value.indexOf('hsv') !== -1) { + const parts = value.replace(/hsva|hsv|\(|\)/gm, '') + .split(/\s|,/g, '').filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10)); + + if (parts.length >= 3) { + fromHSV(parts[0], parts[1], parts[2]); + } + + if (parts.length === 4) { + this._alpha = Math.floor(parseFloat(parts[3]) * 100); + } + } else if (value.indexOf('rgb') !== -1) { + const parts = value.replace(/rgba|rgb|\(|\)/gm, '') + .split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10)); + + if (parts.length >= 3) { + const { h, s, v } = rgb2hsv(parts[0], parts[1], parts[2]); + fromHSV(h, s, v); + } + + if (parts.length === 4) { + this._alpha = Math.floor(parseFloat(parts[3]) * 100); + } + } else if (value.indexOf('#') !== -1) { + const hex = value.replace('#', '').trim(); + let r, g, b; + + if (hex.length === 3) { + r = parseHexChannel(hex[0] + hex[0]); + g = parseHexChannel(hex[1] + hex[1]); + b = parseHexChannel(hex[2] + hex[2]); + } else if (hex.length === 6) { + r = parseHexChannel(hex.substring(0, 2)); + g = parseHexChannel(hex.substring(2, 4)); + b = parseHexChannel(hex.substring(4)); + } + + const { h, s, v } = rgb2hsv(r, g, b); + fromHSV(h, s, v); + } + } + + doOnChange() { + const { _hue, _saturation, _value, _alpha, format } = this; + + if (this.enableAlpha) { + switch (format) { + case 'hsl': + const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100); + this.value = `hsla(${ _hue }, ${ hsl[1] * 100 }%, ${ hsl[2] * 100 }%, ${ _alpha / 100})`; + break; + case 'hsv': + this.value = `hsva(${ _hue }, ${ _saturation }%, ${ _value }%, ${ _alpha / 100})`; + break; + default: + const { r, g, b } = hsv2rgb(_hue, _saturation, _value); + this.value = `rgba(${r}, ${g}, ${b}, ${ _alpha / 100 })`; + } + } else { + switch (format) { + case 'hsl': + const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100); + this.value = `hsl(${ _hue }, ${ hsl[1] * 100 }%, ${ hsl[2] * 100 }%)`; + break; + case 'hsv': + this.value = `hsv(${ _hue }, ${ _saturation }%, ${ _value }%)`; + break; + case 'rgb': + const { r, g, b } = hsv2rgb(_hue, _saturation, _value); + this.value = `rgb(${r}, ${g}, ${b})`; + break; + default: + this.value = toHex(hsv2rgb(_hue, _saturation, _value)); + } + } + } +}; diff --git a/packages/color-picker/src/components/alpha-slider.vue b/packages/color-picker/src/components/alpha-slider.vue new file mode 100644 index 000000000..08944ef54 --- /dev/null +++ b/packages/color-picker/src/components/alpha-slider.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/packages/color-picker/src/components/hue-slider.vue b/packages/color-picker/src/components/hue-slider.vue new file mode 100644 index 000000000..eafa095ef --- /dev/null +++ b/packages/color-picker/src/components/hue-slider.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/packages/color-picker/src/components/picker-dropdown.vue b/packages/color-picker/src/components/picker-dropdown.vue new file mode 100644 index 000000000..d39d0eeea --- /dev/null +++ b/packages/color-picker/src/components/picker-dropdown.vue @@ -0,0 +1,74 @@ + + + diff --git a/packages/color-picker/src/components/sv-panel.vue b/packages/color-picker/src/components/sv-panel.vue new file mode 100644 index 000000000..815180903 --- /dev/null +++ b/packages/color-picker/src/components/sv-panel.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/packages/color-picker/src/draggable.js b/packages/color-picker/src/draggable.js new file mode 100644 index 000000000..14e8bfcac --- /dev/null +++ b/packages/color-picker/src/draggable.js @@ -0,0 +1,34 @@ +let isDragging = false; + +export default function(element, options) { + const moveFn = function(event) { + if (options.drag) { + options.drag(event); + } + }; + const upFn = function(event) { + document.removeEventListener('mousemove', moveFn); + document.removeEventListener('mouseup', upFn); + document.onselectstart = null; + document.ondragstart = null; + + isDragging = false; + + if (options.end) { + options.end(event); + } + }; + element.addEventListener('mousedown', function(event) { + if (isDragging) return; + document.onselectstart = function() { return false; }; + document.ondragstart = function() { return false; }; + + document.addEventListener('mousemove', moveFn); + document.addEventListener('mouseup', upFn); + isDragging = true; + + if (options.start) { + options.start(event); + } + }); +} diff --git a/packages/color-picker/src/main.vue b/packages/color-picker/src/main.vue new file mode 100644 index 000000000..256f47dd4 --- /dev/null +++ b/packages/color-picker/src/main.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/packages/theme-default/src/color-picker.css b/packages/theme-default/src/color-picker.css new file mode 100644 index 000000000..af31a62b0 --- /dev/null +++ b/packages/theme-default/src/color-picker.css @@ -0,0 +1,250 @@ +@import "./common/var.css"; + +@component-namespace el-color { + @component hue-slider { + position: relative; + box-sizing: border-box; + width: 240px; + height: 12px; + background-color: #f00; + padding: 0 2px; + + @descendent bar { + position: relative; + background: linear-gradient( + to right, #f00 0%, + #ff0 17%, #0f0 33%, + #0ff 50%, #00f 67%, + #f0f 83%, #f00 100%); + height: 100%; + } + + @descendent thumb { + position: absolute; + cursor: pointer; + box-sizing: border-box; + left: 0; + top: 0; + width: 4px; + height: 100%; + border-radius: 1px; + background: #fff; + border: 1px solid #f0f0f0; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); + z-index: 1; + } + + @when vertical { + width: 12px; + height: 180px; + padding: 2px 0; + + .el-color-hue-slider__bar { + background: linear-gradient( + to bottom, #f00 0%, + #ff0 17%, #0f0 33%, + #0ff 50%, #00f 67%, + #f0f 83%, #f00 100%); + } + + .el-color-hue-slider__thumb { + left: 0; + top: 0; + width: 100%; + height: 4px; + } + } + } + + @component svpanel { + position: relative; + width: 240px; + height: 180px; + + @descendent white, black { + cursor: pointer; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + @descendent white { + background: linear-gradient(to right, #fff, rgba(255,255,255,0)); + } + + @descendent black { + background: linear-gradient(to top, #000, rgba(0,0,0,0)); + } + + @descendent cursor { + cursor: pointer; + position: absolute; + + > div { + cursor: head; + width: 4px; + height: 4px; + box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0,0,0,0.3), 0 0 1px 2px rgba(0,0,0,0.4); + border-radius: 50%; + transform: translate(-2px, -2px); + } + } + } + + @component alpha-slider { + position: relative; + box-sizing: border-box; + width: 240px; + height: 12px; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); + + @descendent bar { + position: relative; + background: linear-gradient( + to right, rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 1) 100%); + height: 100%; + } + + @descendent thumb { + position: absolute; + cursor: pointer; + box-sizing: border-box; + left: 0; + top: 0; + width: 4px; + height: 100%; + border-radius: 1px; + background: #fff; + border: 1px solid #f0f0f0; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); + z-index: 1; + } + + @when vertical { + width: 20px; + height: 180px; + + .el-color-alpha-slider__bar { + background: linear-gradient( + to bottom, rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 1) 100%); + } + + .el-color-alpha-slider__thumb { + left: 0; + top: 0; + width: 100%; + height: 4px; + } + } + } + + @component dropdown { + width: 260px; + + @descendent main-wrapper { + margin-bottom: 6px; + + &::after { + content: ""; + display: table; + clear: both; + } + } + + @descendent btns { + margin-top: 6px; + text-align: right; + } + + @descendent btn { + border: 1px solid #dcdcdc; + color: #333; + line-height: 24px; + border-radius: 2px; + padding: 0 20px; + cursor: pointer; + background-color: transparent; + outline: none; + font-size: 12px; + + &[disabled] { + color: #cccccc; + cursor: not-allowed; + } + } + + @descendent link-btn { + cursor: pointer; + color: var(--color-primary); + text-decoration: none; + padding: 15px; + font-size: 12px; + } + } + + @component picker { + display: inline-block; + position: relative; + + @descendent trigger { + display: inline-block; + box-sizing: border-box; + height: 36px; + padding: 6px; + border: 1px solid #bfcbd9; + border-radius: 4px; + } + + @descendent color { + position: relative; + display: inline-block; + box-sizing: border-box; + vertical-align: middle; + border: 1px solid #666; + width: 22px; + height: 22px; + text-align: center; + + @when alpha { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); + } + } + + @descendent color-inner { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + } + + @descendent empty { + font-size: 12px; + vertical-align: middle; + margin-top: -2px; + color: #666; + } + + @descendent icon { + display: inline-block; + position: relative; + vertical-align: middle; + margin-left: 8px; + width: 12px; + color: #888; + } + + @descendent panel { + position: absolute; + z-index: 10; + padding: 6px; + background-color: var(--color-white); + border: 1px solid var(--color-base-gray); + box-shadow: var(--dropdown-menu-box-shadow); + } + } +} diff --git a/packages/theme-default/src/index.css b/packages/theme-default/src/index.css index 3b0319833..2c6b134ec 100644 --- a/packages/theme-default/src/index.css +++ b/packages/theme-default/src/index.css @@ -60,3 +60,4 @@ @import "./collapse.css"; @import "./collapse-item.css"; @import "./cascader.css"; +@import "./color-picker.css"; diff --git a/src/index.js b/src/index.js index 91639305e..35b49d4c8 100644 --- a/src/index.js +++ b/src/index.js @@ -61,6 +61,7 @@ import CarouselItem from '../packages/carousel-item'; import Collapse from '../packages/collapse'; import CollapseItem from '../packages/collapse-item'; import Cascader from '../packages/cascader'; +import ColorPicker from '../packages/color-picker'; import locale from 'element-ui/src/locale'; const components = [ @@ -120,7 +121,8 @@ const components = [ CarouselItem, Collapse, CollapseItem, - Cascader + Cascader, + ColorPicker ]; const install = function(Vue, opts = {}) { @@ -214,5 +216,6 @@ module.exports = { CarouselItem, Collapse, CollapseItem, - Cascader + Cascader, + ColorPicker }; diff --git a/src/locale/lang/bg.js b/src/locale/lang/bg.js index 221683454..0ae46d74d 100644 --- a/src/locale/lang/bg.js +++ b/src/locale/lang/bg.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Изчисти' + }, datepicker: { now: 'Сега', today: 'Днес', diff --git a/src/locale/lang/da.js b/src/locale/lang/da.js index b3d8de0a6..27b91c57b 100644 --- a/src/locale/lang/da.js +++ b/src/locale/lang/da.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Ryd' + }, datepicker: { now: 'Nu', today: 'I dag', diff --git a/src/locale/lang/de.js b/src/locale/lang/de.js index bec8916e8..f5b92631c 100644 --- a/src/locale/lang/de.js +++ b/src/locale/lang/de.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Leeren' + }, datepicker: { now: 'Jetzt', today: 'Heute', diff --git a/src/locale/lang/el.js b/src/locale/lang/el.js index 1f3f2bc83..335689fe0 100644 --- a/src/locale/lang/el.js +++ b/src/locale/lang/el.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Καθαρισμός' + }, datepicker: { now: 'Τώρα', today: 'Σήμερα', diff --git a/src/locale/lang/en.js b/src/locale/lang/en.js index f7fc32a3b..904ad856f 100644 --- a/src/locale/lang/en.js +++ b/src/locale/lang/en.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Clear' + }, datepicker: { now: 'Now', today: 'Today', diff --git a/src/locale/lang/es.js b/src/locale/lang/es.js index 26b804d6b..5508fedf8 100644 --- a/src/locale/lang/es.js +++ b/src/locale/lang/es.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'Confirmar', + clear: 'Limpiar' + }, datepicker: { now: 'Ahora', today: 'Hoy', diff --git a/src/locale/lang/fa.js b/src/locale/lang/fa.js index 672f6f4d0..5433eef22 100644 --- a/src/locale/lang/fa.js +++ b/src/locale/lang/fa.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'باشد', + clear: 'خذف' + }, datepicker: { now: 'اکنون', today: 'امروز', diff --git a/src/locale/lang/fi.js b/src/locale/lang/fi.js index bc57f2ce3..69d40a262 100644 --- a/src/locale/lang/fi.js +++ b/src/locale/lang/fi.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Tyhjennä' + }, datepicker: { now: 'Nyt', today: 'Tänään', diff --git a/src/locale/lang/fr.js b/src/locale/lang/fr.js index 5e38a5feb..8bec7a4b2 100644 --- a/src/locale/lang/fr.js +++ b/src/locale/lang/fr.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Effacer' + }, datepicker: { now: 'Maintenant', today: 'Auj.', diff --git a/src/locale/lang/id.js b/src/locale/lang/id.js index 4618bd2bd..4709e0216 100644 --- a/src/locale/lang/id.js +++ b/src/locale/lang/id.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'YA', + clear: 'Kosongkan' + }, datepicker: { now: 'Sekarang', today: 'Hari ini', diff --git a/src/locale/lang/it.js b/src/locale/lang/it.js index 539a1d220..e577bd43d 100644 --- a/src/locale/lang/it.js +++ b/src/locale/lang/it.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Pulisci' + }, datepicker: { now: 'Ora', today: 'Oggi', diff --git a/src/locale/lang/ja.js b/src/locale/lang/ja.js index d93a4d0d0..5d723aecd 100644 --- a/src/locale/lang/ja.js +++ b/src/locale/lang/ja.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'はい', + clear: 'クリア' + }, datepicker: { now: '現在', today: '今日', diff --git a/src/locale/lang/ko.js b/src/locale/lang/ko.js index 7768b4247..b22ab8440 100644 --- a/src/locale/lang/ko.js +++ b/src/locale/lang/ko.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: '확인', + clear: '초기화' + }, datepicker: { now: '지금', today: '오늘', diff --git a/src/locale/lang/nb-NO.js b/src/locale/lang/nb-NO.js index 1503459da..e185dd86e 100644 --- a/src/locale/lang/nb-NO.js +++ b/src/locale/lang/nb-NO.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Tøm' + }, datepicker: { now: 'Nå', today: 'I dag', diff --git a/src/locale/lang/nl.js b/src/locale/lang/nl.js index 447a2657a..e79a12083 100644 --- a/src/locale/lang/nl.js +++ b/src/locale/lang/nl.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'Bevestig', + clear: 'Legen' + }, datepicker: { now: 'Nu', today: 'Vandaag', diff --git a/src/locale/lang/pl.js b/src/locale/lang/pl.js index ca11e02f5..b8a9d9964 100644 --- a/src/locale/lang/pl.js +++ b/src/locale/lang/pl.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Wyczyść' + }, datepicker: { now: 'Teraz', today: 'Dzisiaj', diff --git a/src/locale/lang/pt-br.js b/src/locale/lang/pt-br.js index 82a0277a7..0b6df083c 100644 --- a/src/locale/lang/pt-br.js +++ b/src/locale/lang/pt-br.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'Confirmar', + clear: 'Limpar' + }, datepicker: { now: 'Agora', today: 'Hoje', diff --git a/src/locale/lang/pt.js b/src/locale/lang/pt.js index 8efcdcb4e..1695f0822 100644 --- a/src/locale/lang/pt.js +++ b/src/locale/lang/pt.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'Confirmar', + clear: 'Limpar' + }, datepicker: { now: 'Agora', today: 'Hoje', diff --git a/src/locale/lang/ru-RU.js b/src/locale/lang/ru-RU.js index a91fb3836..8fe40a09b 100644 --- a/src/locale/lang/ru-RU.js +++ b/src/locale/lang/ru-RU.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Очистить' + }, datepicker: { now: 'Сейчас', today: 'Сегодня', diff --git a/src/locale/lang/sk.js b/src/locale/lang/sk.js index 42bb95a66..ce79ce81d 100644 --- a/src/locale/lang/sk.js +++ b/src/locale/lang/sk.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Zmazať' + }, datepicker: { now: 'Teraz', today: 'Dnes', diff --git a/src/locale/lang/sv-SE.js b/src/locale/lang/sv-SE.js index e53c90a42..1d5d4bf78 100644 --- a/src/locale/lang/sv-SE.js +++ b/src/locale/lang/sv-SE.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Töm' + }, datepicker: { now: 'Nu', today: 'Idag', diff --git a/src/locale/lang/th.js b/src/locale/lang/th.js index 044f79c65..5860f6de3 100644 --- a/src/locale/lang/th.js +++ b/src/locale/lang/th.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'ตกลง', + clear: 'ล้างข้อมูล' + }, datepicker: { now: 'ตอนนี้', today: 'วันนี้', diff --git a/src/locale/lang/tr-TR.js b/src/locale/lang/tr-TR.js index 55448e2a6..2adda5fdf 100644 --- a/src/locale/lang/tr-TR.js +++ b/src/locale/lang/tr-TR.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Temizle' + }, datepicker: { now: 'Şimdi', today: 'Bugün', diff --git a/src/locale/lang/vi.js b/src/locale/lang/vi.js index b54f459ce..6bc46f94b 100644 --- a/src/locale/lang/vi.js +++ b/src/locale/lang/vi.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: 'OK', + clear: 'Xóa' + }, datepicker: { now: 'Hiện tại', today: 'Hôm nay', diff --git a/src/locale/lang/zh-CN.js b/src/locale/lang/zh-CN.js index 5cc3050df..5056be251 100644 --- a/src/locale/lang/zh-CN.js +++ b/src/locale/lang/zh-CN.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: '确定', + clear: '清空' + }, datepicker: { now: '此刻', today: '今天', diff --git a/src/locale/lang/zh-TW.js b/src/locale/lang/zh-TW.js index 27a33b2dc..f76958d3a 100644 --- a/src/locale/lang/zh-TW.js +++ b/src/locale/lang/zh-TW.js @@ -1,5 +1,9 @@ export default { el: { + colorpicker: { + confirm: '確認', + clear: '清空' + }, datepicker: { now: '現在', today: '今天', diff --git a/test/unit/specs/color-picker.spec.js b/test/unit/specs/color-picker.spec.js new file mode 100644 index 000000000..ee8fa2e45 --- /dev/null +++ b/test/unit/specs/color-picker.spec.js @@ -0,0 +1,137 @@ +import { createTest, createVue, destroyVM } from '../util'; +import ColorPicker from 'packages/color-picker'; + +describe('ColorPicker', () => { + let vm; + + afterEach(() => { + vm.$destroy(true); + destroyVM(vm); + const dropdown = document.querySelector('.el-color-dropdown'); + if (dropdown && dropdown.parentNode) dropdown.parentNode.removeChild(dropdown); + }); + + it('should works', () => { + vm = createTest(ColorPicker, true); + expect(vm.$el).to.exist; + }); + + it('should show alpha slider when show-alpha=true', (done) => { + const vm = createVue({ + template: ` + + `, + + data() { + return { + color: null + }; + } + }, true); + + const trigger = vm.$el.querySelector('.el-color-picker__trigger'); + trigger.click(); + + setTimeout(() => { + const alphaSlider = document.querySelector('.el-color-alpha-slider'); + expect(alphaSlider).to.exist; + done(); + }, ANIMATION_TIME); + }); + + it('should show color picker when click trigger', (done) => { + vm = createTest(ColorPicker, true); + + const trigger = vm.$el.querySelector('.el-color-picker__trigger'); + trigger.click(); + + vm.$nextTick(() => { + const dropdown = document.querySelector('.el-color-dropdown'); + expect(dropdown).to.exist; + done(); + }); + }); + + const ANIMATION_TIME = 300; + + it('should pick a color when confirm button click', (done) => { + const vm = createVue({ + template: ` + + `, + + data() { + return { + color: null + }; + } + }, true); + + const trigger = vm.$el.querySelector('.el-color-picker__trigger'); + trigger.click(); + + setTimeout(() => { + const dropdown = document.querySelector('.el-color-dropdown__btn'); + dropdown.click(); + vm.$nextTick(() => { + expect(vm.color).to.equal('#FF0000'); + done(); + }); + }, ANIMATION_TIME); + }); + + it('should init the right color when open', (done) => { + const vm = createVue({ + template: ` + + `, + + data() { + return { + color: '#0f0' + }; + } + }, true); + + const trigger = vm.$el.querySelector('.el-color-picker__trigger'); + trigger.click(); + + setTimeout(() => { + const dropdown = document.querySelector('.el-color-dropdown__btn'); + dropdown.click(); + vm.$nextTick(() => { + const hueBar = document.querySelector('.el-color-hue-slider__thumb'); + const top = parseInt(hueBar.style.top, 10); + expect(top > 10).to.be.true; + done(); + }); + }, ANIMATION_TIME); + }); + + it('should clear a color when clear button click', (done) => { + const vm = createVue({ + template: ` + + `, + + data() { + return { + color: '#f00' + }; + } + }, true); + + const trigger = vm.$el.querySelector('.el-color-picker__trigger'); + trigger.click(); + + setTimeout(() => { + const clearBtn = document.querySelector('.el-color-dropdown__link-btn'); + clearBtn.click(); + setTimeout(() => { + expect(vm.color).to.equal(null); + done(); + }, 30); + }, ANIMATION_TIME); + }); +}); +