Add ColorPicker component.

pull/2748/merge
Furybean 2017-02-08 00:11:53 +08:00 committed by 杨奕
parent 080a996f3f
commit 42bd58d9fe
45 changed files with 1593 additions and 5 deletions

View File

@ -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"
}

View File

@ -0,0 +1,100 @@
<script>
export default {
data() {
return {
color1: '#ff0',
color2: null,
color3: 'rgba(128, 33, 22, 0.8)',
color4: null
};
}
}
</script>
<style scoped>
.demo-box.demo-color-picker .source {
padding: 0;
}
.demo-box.demo-color-picker .block {
padding: 30px 24px;
overflow: hidden;
border-bottom: solid 1px #EFF2F6;
&:last-child {
border-bottom: none;
}
}
.demo-box.demo-color-picker .demonstration {
display: inline-block;
font-size: 14px;
width: 25%;
color: #8492a6;
line-height: 44px;
}
</style>
## 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
<div class="block">
<span class="demonstration">Default value</span>
<el-color-picker v-model="color1"></el-color-picker>
</div>
<div class="block">
<span class="demonstration">Empty</span>
<el-color-picker v-model="color2"></el-color-picker>
</div>
<style>
</style>
<script>
export default {
data() {
return {
color1: '#ff0',
color2: null
}
}
};
</script>
```
:::
### 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
<div class="block">
<span class="demonstration">Default value</span>
<el-color-picker v-model="color3" show-alpha></el-color-picker>
</div>
<div class="block">
<span class="demonstration">Empty</span>
<el-color-picker v-model="color4" show-alpha></el-color-picker>
</div>
<script>
export default {
data() {
return {
color3: 'rgba(128, 33, 22, 0.8)',
color4: null
}
}
};
</script>
```
:::
### 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 |

View File

@ -0,0 +1,100 @@
<script>
export default {
data() {
return {
color1: '#ff0',
color2: null,
color3: 'rgba(128, 33, 22, 0.8)',
color4: null
};
}
}
</script>
<style scoped>
.demo-box.demo-color-picker .source {
padding: 0;
}
.demo-box.demo-color-picker .block {
padding: 30px 24px;
overflow: hidden;
border-bottom: solid 1px #EFF2F6;
&:last-child {
border-bottom: none;
}
}
.demo-box.demo-color-picker .demonstration {
display: inline-block;
font-size: 14px;
width: 25%;
color: #8492a6;
line-height: 44px;
}
</style>
## ColorPicker
ColorPicker 是一个颜色选择器,该组件是用来解决某些场景下需要选择颜色的需求。
### 选择颜色
:::demo ColorPicker 用法与 DatePicker 类似,需要使用 v-model 来与 Vue 实例中的一个变量进行双向绑定,绑定的变量需要是字符串类型。
```html
<div class="block">
<span class="demonstration">有默认值</span>
<el-color-picker v-model="color1"></el-color-picker>
</div>
<div class="block">
<span class="demonstration">无默认值</span>
<el-color-picker v-model="color2"></el-color-picker>
</div>
<style>
</style>
<script>
export default {
data() {
return {
color1: '#ff0',
color2: null
}
}
};
</script>
```
:::
### 选择颜色和透明度
:::demo ColorPicker 支持普通颜色,也支持带 Alpha 通道的颜色,通过 show-alpha 属性即可控制是否支持透明度的使用。
```html
<div class="block">
<span class="demonstration">有默认值</span>
<el-color-picker v-model="color3" show-alpha></el-color-picker>
</div>
<div class="block">
<span class="demonstration">无默认值</span>
<el-color-picker v-model="color4" show-alpha></el-color-picker>
</div>
<script>
export default {
data() {
return {
color3: 'rgba(128, 33, 22, 0.8)',
color4: null
}
}
};
</script>
```
:::
### Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------- |---------- |------------- |-------- |
| show-alpha | 是否显示透明度 Slider。 | Boolean | — | false |
| color-format | 写入 v-model 的颜色的格式。在 show-alpha 为 true 的情况下,默认值为 rgb否则为 hex。 | string | hsl, hsv, hex, rgb | hex |

View File

@ -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"
}
]
}

View File

@ -1,6 +1,11 @@
<template>
<div>
<el-button>Test</el-button>
<el-button @click="value = '#fff'">Change Value</el-button>
<div>Value: {{ value }}</div>
<el-color-picker v-model="value"></el-color-picker>
<div>Value2: {{ value2 }}</div>
<el-color-picker v-model="value2" show-alpha></el-color-picker>
</div>
</template>
@ -13,7 +18,10 @@
},
data() {
return {};
return {
value: '#bfcbd9',
value2: null
};
}
};
</script>

View File

@ -0,0 +1,6 @@
var cooking = require('cooking');
var gen = require('../../build/gen-single-config');
cooking.set(gen(__dirname, 'ElColorPicker'));
module.exports = cooking.resolve();

View File

@ -0,0 +1,8 @@
import ColorPicker from './src/main';
/* istanbul ignore next */
ColorPicker.install = function(Vue) {
Vue.component(ColorPicker.name, ColorPicker);
};
export default ColorPicker;

View File

@ -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": {}
}

View File

@ -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
// <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
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));
}
}
}
};

View File

@ -0,0 +1,137 @@
<template>
<div class="el-color-alpha-slider" :class="{ 'is-vertical': vertical }">
<div class="el-color-alpha-slider__bar"
@click="handleClick"
ref="bar"
:style="{
background: background
}">
</div>
<div class="el-color-alpha-slider__thumb"
ref="thumb"
:style="{
left: thumbLeft + 'px',
top: thumbTop + 'px'
}">
</div>
</div>
</template>
<style>
</style>
<script>
import draggable from '../draggable';
export default {
name: 'el-color-alpha-slider',
props: {
color: {
required: true
},
vertical: {
type: Boolean
}
},
watch: {
'color._alpha'() {
this.update();
},
'color.value'() {
this.update();
}
},
methods: {
handleClick(event) {
const thumb = this.$refs.thumb;
const target = event.target;
if (target !== thumb) {
this.handleDrag(event);
}
},
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
const { thumb } = this.$refs;
if (!this.vertical) {
let left = event.clientX - rect.left;
left = Math.max(thumb.offsetWidth / 2, left);
left = Math.min(left, rect.width - thumb.offsetWidth / 2);
this.color._alpha = Math.round((left - thumb.offsetWidth / 2) / (rect.width - thumb.offsetWidth) * 100);
} else {
let top = event.clientY - rect.top;
top = Math.max(thumb.offsetHeight / 2, top);
top = Math.min(top, rect.height - thumb.offsetHeight / 2);
this.color._alpha = Math.round((top - thumb.offsetHeight / 2) / (rect.height - thumb.offsetHeight) * 100);
}
},
getThumbLeft() {
if (this.vertical) return 0;
const el = this.$el;
const alpha = this.color._alpha;
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(alpha * (el.offsetWidth - thumb.offsetWidth / 2) / 100);
},
getThumbTop() {
if (!this.vertical) return 0;
const el = this.$el;
const alpha = this.color._alpha;
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(alpha * (el.offsetHeight - thumb.offsetHeight / 2) / 100);
},
getBackground() {
if (this.color && this.color.value) {
const { r, g, b } = this.color.toRgb();
return `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)`;
}
return null;
},
update() {
this.thumbLeft = this.getThumbLeft();
this.thumbTop = this.getThumbTop();
this.background = this.getBackground();
}
},
data() {
return {
thumbLeft: 0,
thumbTop: 0,
background: null
};
},
mounted() {
const { bar, thumb } = this.$refs;
const dragConfig = {
drag: (event) => {
this.handleDrag(event);
},
end: (event) => {
this.handleDrag(event);
}
};
draggable(bar, dragConfig);
draggable(thumb, dragConfig);
this.update();
}
};
</script>

View File

@ -0,0 +1,121 @@
<template>
<div class="el-color-hue-slider" :class="{ 'is-vertical': vertical }">
<div class="el-color-hue-slider__bar" @click="handleClick" ref="bar"></div>
<div class="el-color-hue-slider__thumb"
:style="{
left: thumbLeft + 'px',
top: thumbTop + 'px'
}"
ref="thumb">
</div>
</div>
</template>
<style>
</style>
<script>
import draggable from '../draggable';
export default {
name: 'el-color-hue-slider',
props: {
color: {
required: true
},
vertical: {
type: Boolean
}
},
data() {
return {
thumbLeft: 0,
thumbTop: 0
};
},
watch: {
'color.value'() {
this.update();
}
},
methods: {
handleClick(event) {
const thumb = this.$refs.thumb;
const target = event.target;
if (target !== thumb) {
this.handleDrag(event);
}
},
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
const { thumb } = this.$refs;
let hue;
if (!this.vertical) {
let left = event.clientX - rect.left;
left = Math.min(left, rect.width - thumb.offsetWidth / 2);
left = Math.max(thumb.offsetWidth / 2, left);
hue = Math.round((left - thumb.offsetWidth / 2) / (rect.width - thumb.offsetWidth) * 360);
} else {
let top = event.clientY - rect.top;
top = Math.min(top, rect.height - thumb.offsetHeight / 2);
top = Math.max(thumb.offsetHeight / 2, top);
hue = Math.round((top - thumb.offsetHeight / 2) / (rect.height - thumb.offsetHeight) * 360);
}
this.color.set('hue', hue);
},
getThumbLeft() {
if (this.vertical) return 0;
const el = this.$el;
const hue = this.color.get('hue');
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(hue * (el.offsetWidth - thumb.offsetWidth / 2) / 360);
},
getThumbTop() {
if (!this.vertical) return 0;
const el = this.$el;
const hue = this.color.get('hue');
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(hue * (el.offsetHeight - thumb.offsetHeight / 2) / 360);
},
update() {
this.thumbLeft = this.getThumbLeft();
this.thumbTop = this.getThumbTop();
}
},
mounted() {
const { bar, thumb } = this.$refs;
const dragConfig = {
drag: (event) => {
this.handleDrag(event);
},
end: (event) => {
this.handleDrag(event);
}
};
draggable(bar, dragConfig);
draggable(thumb, dragConfig);
this.update();
}
};
</script>

View File

@ -0,0 +1,74 @@
<template>
<transition name="el-zoom-in-top" @after-leave="doDestroy">
<div
class="el-color-dropdown"
v-show="showPopper">
<div class="el-color-dropdown__main-wrapper">
<hue-slider ref="hue" :color="color" vertical style="float: right;"></hue-slider>
<sv-panel ref="sl" :color="color"></sv-panel>
</div>
<alpha-slider v-if="showAlpha" ref="alpha" :color="color"></alpha-slider>
<div class="el-color-dropdown__btns">
<a href="JavaScript:" class="el-color-dropdown__link-btn" @click="$emit('clear')">{{ t('el.colorpicker.clear') }}</a>
<button class="el-color-dropdown__btn" @click="confirmValue">{{ t('el.colorpicker.confirm') }}</button>
</div>
</div>
</transition>
</template>
<style>
</style>
<script type="babel">
import SvPanel from './sv-panel';
import HueSlider from './hue-slider';
import AlphaSlider from './alpha-slider';
import Popper from 'element-ui/src/utils/vue-popper';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Locale from 'element-ui/src/mixins/locale';
export default {
name: 'el-color-picker-dropdown',
mixins: [Popper, Locale],
components: {
SvPanel,
HueSlider,
AlphaSlider
},
directives: { Clickoutside },
props: {
color: {
required: true
},
showAlpha: {
type: Boolean
}
},
methods: {
confirmValue() {
this.$emit('pick');
}
},
mounted() {
this.$parent.popperElm = this.popperElm = this.$el;
this.referenceElm = this.$parent.$el;
},
watch: {
showPopper(val) {
if (val === true) {
this.$nextTick(() => {
const { sl, hue, alpha } = this.$refs;
sl && sl.update();
hue && hue.update();
alpha && alpha.update();
});
}
}
}
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<div class="el-color-svpanel"
:style="{
backgroundColor: background
}">
<div class="el-color-svpanel__white"></div>
<div class="el-color-svpanel__black"></div>
<div class="el-color-svpanel__cursor"
:style="{
top: cursorTop + 'px',
left: cursorLeft + 'px'
}">
<div></div>
</div>
</div>
</template>
<style>
</style>
<script type="babel">
import draggable from '../draggable';
export default {
name: 'el-sl-panel',
props: {
color: {
required: true
}
},
watch: {
'color.value'() {
this.update();
}
},
methods: {
update() {
const saturation = this.color.get('saturation');
const value = this.color.get('value');
const el = this.$el;
let { width, height } = el.getBoundingClientRect();
if (!height) height = width * 3 / 4;
this.cursorLeft = saturation * width / 100;
this.cursorTop = (100 - value) * height / 100;
this.background = 'hsl(' + this.color.get('hue') + ', 100%, 50%)';
}
},
mounted() {
const handleDrag = (event) => {
const el = this.$el;
const rect = el.getBoundingClientRect();
let left = event.clientX - rect.left;
let top = event.clientY - rect.top;
left = Math.max(0, left);
left = Math.min(left, rect.width);
top = Math.max(0, top);
top = Math.min(top, rect.height);
this.cursorLeft = left;
this.cursorTop = top;
this.color.set({
saturation: left / rect.width * 100,
value: 100 - top / rect.height * 100
});
};
draggable(this.$el, {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
}
});
this.update();
},
data() {
return {
cursorTop: 0,
cursorLeft: 0,
background: 'hsl(0, 100%, 50%)'
}
}
}
</script>

View File

@ -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);
}
});
}

View File

@ -0,0 +1,95 @@
<template>
<div class="el-color-picker" v-clickoutside="hide">
<div class="el-color-picker__trigger" @click.stop="showPicker = !showPicker">
<span class="el-color-picker__color" :class="{ 'is-alpha': showAlpha }">
<span class="el-color-picker__color-inner"
:style="{
backgroundColor: value ? value : 'transparent'
}"></span>
<span class="el-color-picker__empty el-icon-close" v-if="!value"></span>
</span>
<span class="el-color-picker__icon el-icon-caret-bottom"></span>
</div>
<picker-dropdown
ref="dropdown"
class="el-color-picker__panel"
v-model="showPicker"
@pick="confirmValue"
@clear="clearValue"
:color="color"
:show-alpha="showAlpha">
</picker-dropdown>
</div>
</template>
<style>
</style>
<script>
import Color from './color';
import PickerDropdown from './components/picker-dropdown.vue';
import Clickoutside from 'element-ui/src/utils/clickoutside';
export default {
name: 'ElColorPicker',
props: {
value: {
type: String
},
showAlpha: {
type: Boolean
},
colorFormat: {
type: String
}
},
directives: { Clickoutside },
watch: {
value(val) {
if (val && val !== this.color.value) {
this.color.fromString(val);
}
}
},
methods: {
confirmValue(value) {
this.$emit('input', this.color.value);
this.showPicker = false;
},
clearValue() {
this.$emit('input', null);
this.showPicker = false;
},
hide() {
this.showPicker = false;
}
},
mounted() {
const value = this.value;
if (value) {
this.color.fromString(value);
}
this.popperElm = this.$refs.dropdown.$el;
},
data() {
const color = new Color({
enableAlpha: this.showAlpha,
format: this.colorFormat
});
return {
color,
showPicker: false
};
},
components: {
PickerDropdown
}
};
</script>

View File

@ -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);
}
}
}

View File

@ -60,3 +60,4 @@
@import "./collapse.css";
@import "./collapse-item.css";
@import "./cascader.css";
@import "./color-picker.css";

View File

@ -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
};

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Изчисти'
},
datepicker: {
now: 'Сега',
today: 'Днес',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Ryd'
},
datepicker: {
now: 'Nu',
today: 'I dag',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Leeren'
},
datepicker: {
now: 'Jetzt',
today: 'Heute',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Καθαρισμός'
},
datepicker: {
now: 'Τώρα',
today: 'Σήμερα',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Clear'
},
datepicker: {
now: 'Now',
today: 'Today',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'Confirmar',
clear: 'Limpiar'
},
datepicker: {
now: 'Ahora',
today: 'Hoy',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'باشد',
clear: 'خذف'
},
datepicker: {
now: 'اکنون',
today: 'امروز',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Tyhjennä'
},
datepicker: {
now: 'Nyt',
today: 'Tänään',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Effacer'
},
datepicker: {
now: 'Maintenant',
today: 'Auj.',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'YA',
clear: 'Kosongkan'
},
datepicker: {
now: 'Sekarang',
today: 'Hari ini',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Pulisci'
},
datepicker: {
now: 'Ora',
today: 'Oggi',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'はい',
clear: 'クリア'
},
datepicker: {
now: '現在',
today: '今日',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: '확인',
clear: '초기화'
},
datepicker: {
now: '지금',
today: '오늘',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Tøm'
},
datepicker: {
now: 'Nå',
today: 'I dag',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'Bevestig',
clear: 'Legen'
},
datepicker: {
now: 'Nu',
today: 'Vandaag',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Wyczyść'
},
datepicker: {
now: 'Teraz',
today: 'Dzisiaj',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'Confirmar',
clear: 'Limpar'
},
datepicker: {
now: 'Agora',
today: 'Hoje',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'Confirmar',
clear: 'Limpar'
},
datepicker: {
now: 'Agora',
today: 'Hoje',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Очистить'
},
datepicker: {
now: 'Сейчас',
today: 'Сегодня',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Zmazať'
},
datepicker: {
now: 'Teraz',
today: 'Dnes',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Töm'
},
datepicker: {
now: 'Nu',
today: 'Idag',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'ตกลง',
clear: 'ล้างข้อมูล'
},
datepicker: {
now: 'ตอนนี้',
today: 'วันนี้',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Temizle'
},
datepicker: {
now: 'Şimdi',
today: 'Bugün',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: 'OK',
clear: 'Xóa'
},
datepicker: {
now: 'Hiện tại',
today: 'Hôm nay',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: '确定',
clear: '清空'
},
datepicker: {
now: '此刻',
today: '今天',

View File

@ -1,5 +1,9 @@
export default {
el: {
colorpicker: {
confirm: '確認',
clear: '清空'
},
datepicker: {
now: '現在',
today: '今天',

View File

@ -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: `
<el-color-picker v-model="color" show-alpha></el-color-picker>
`,
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: `
<el-color-picker v-model="color"></el-color-picker>
`,
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: `
<el-color-picker v-model="color"></el-color-picker>
`,
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: `
<el-color-picker v-model="color"></el-color-picker>
`,
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);
});
});