element color-picker源码
src/main.vue
<template> <div :class="[ 'el-color-picker', colorDisabled ? 'is-disabled' : '', colorSize ? `el-color-picker--${ colorSize }` : '' ]" v-clickoutside="hide"> <div class="el-color-picker__mask" v-if="colorDisabled"></div> <div class="el-color-picker__trigger" @click="handleTrigger"> <span class="el-color-picker__color" :class="{ 'is-alpha': showAlpha }"> <span class="el-color-picker__color-inner" :style="{ backgroundColor: displayedColor }"></span> <span class="el-color-picker__empty el-icon-close" v-if="!value && !showPanelColor"></span> </span> <span class="el-color-picker__icon el-icon-arrow-down" v-show="value || showPanelColor"></span> </div> <picker-dropdown ref="dropdown" :class="['el-color-picker__panel', popperClass || '']" v-model="showPicker" @pick="confirmValue" @clear="clearValue" :color="color" :show-alpha="showAlpha" :predefine="predefine"> </picker-dropdown> </div> </template> <script> import Color from './color'; import PickerDropdown from './components/picker-dropdown.vue'; import Clickoutside from 'element-ui/src/utils/clickoutside'; import Emitter from 'element-ui/src/mixins/emitter'; export default { name: 'ElColorPicker', mixins: [Emitter], props: { // value / v-model 绑定值 string value: String, // 是否支持透明度选择 showAlpha: Boolean, // 写入 v-model 的颜色的格式 string hsl / hsv / hex / rgb hex(show-alpha 为 false)/ rgb(show-alpha 为 true) colorFormat: String, // 是否禁用 disabled: Boolean, // 尺寸 size: String, // ColorPicker 下拉框的类名 popperClass: String, // 预定义颜色 predefine: Array }, inject: { elForm: { default: '' }, elFormItem: { default: '' } }, // 自定义指令,点击元素外面事件 directives: { Clickoutside }, computed: { // 背景色调用了 displayedColor 这个 computed 属性: displayedColor() { if (!this.value && !this.showPanelColor) { return 'transparent'; } return this.displayedRgb(this.color, this.showAlpha); }, _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, colorSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; }, colorDisabled() { return this.disabled || (this.elForm || {}).disabled; } }, watch: { value(val) { if (!val) { this.showPanelColor = false; } else if (val && val !== this.color.value) { this.color.fromString(val); } }, color: { // 深度监视 deep: true, // 自定义监听事件 handler() { this.showPanelColor = true; } }, // 监听背景色改变,转换成rgb颜色往上传递, displayedColor(val) { if (!this.showPicker) return; // 新建一个color类 const currentValueColor = new Color({ enableAlpha: this.showAlpha, // props接收的是否支持透明度选择 format: this.colorFormat, // props接收的写入v-model颜色格式 }); currentValueColor.fromString(this.value); // 转换成rgb格式 const currentValueColorRgb = this.displayedRgb(currentValueColor, this.showAlpha); if (val !== currentValueColorRgb) { // 面板中当前显示的颜色发生改变时触发 this.$emit('active-change', val); } } }, methods: { // 展开/关闭下拉面板 handleTrigger() { if (this.colorDisabled) return; this.showPicker = !this.showPicker; }, // 点击面板确定按钮 confirmValue() { const value = this.color.value; // 当绑定值变化时触发 this.$emit('input', value); this.$emit('change', value); // 往上通知el-form this.dispatch('ElFormItem', 'el.form.change', value); this.showPicker = false; }, // 点击面板清空按钮 clearValue() { this.$emit('input', null); this.$emit('change', null); if (this.value !== null) { this.dispatch('ElFormItem', 'el.form.change', null); } this.showPanelColor = false; this.showPicker = false; this.resetColor(); }, // 点击元素外自定义指令 hide() { this.showPicker = false; this.resetColor(); }, // 重置颜色值 resetColor() { this.$nextTick(_ => { if (this.value) { this.color.fromString(this.value); } else { this.showPanelColor = false; } }); }, // 将color转变为rgb(a)模式 displayedRgb(color, showAlpha) { if (!(color instanceof Color)) { throw Error('color should be instance of Color Class'); } const { r, g, b } = color.toRgb(); return showAlpha ? `rgba(${ r }, ${ g }, ${ b }, ${ color.get('alpha') / 100 })` : `rgb(${ r }, ${ g }, ${ b })`; } }, mounted() { const value = this.value; if (value) { this.color.fromString(value); } this.popperElm = this.$refs.dropdown.$el; }, data() { // 新建color类,并赋值 const color = new Color({ enableAlpha: this.showAlpha, format: this.colorFormat }); return { color, showPicker: false, showPanelColor: false }; }, components: { PickerDropdown } }; </script>
src/components/picker-dropdown.vue
<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"> <!-- 接收父组件传递的color对象,传递给子组件 --> <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> <!-- predefine, 父组件props接收额预定义颜色 --> <predefine v-if="predefine" :color="color" :colors="predefine"></predefine> <div class="el-color-dropdown__btns"> <span class="el-color-dropdown__value"> <el-input v-model="customInput" @keyup.native.enter="handleConfirm" @blur="handleConfirm" :validate-event="false" size="mini"> </el-input> </span> <el-button size="mini" type="text" class="el-color-dropdown__link-btn" @click="$emit('clear')"> {{ t('el.colorpicker.clear') }} </el-button> <el-button plain size="mini" class="el-color-dropdown__btn" @click="confirmValue"> {{ t('el.colorpicker.confirm') }} </el-button> </div> </div> </transition> </template> <script> import SvPanel from './sv-panel'; import HueSlider from './hue-slider'; import AlphaSlider from './alpha-slider'; import Predefine from './predefine'; import Popper from 'element-ui/src/utils/vue-popper'; import Locale from 'element-ui/src/mixins/locale'; import ElInput from 'element-ui/packages/input'; import ElButton from 'element-ui/packages/button'; export default { name: 'el-color-picker-dropdown', mixins: [Popper, Locale], components: { SvPanel, HueSlider, AlphaSlider, ElInput, ElButton, Predefine }, props: { color: { required: true }, showAlpha: Boolean, predefine: Array }, data() { return { customInput: '' }; }, computed: { // 中间传递作用,接收父组件value值传 currentColor() { const parent = this.$parent; return !parent.value && !parent.showPanelColor ? '' : parent.color.value; } }, methods: { // 点击确认按钮 confirmValue() { this.$emit('pick'); }, // 按Enter键或失焦的时候 handleConfirm() { // 转换input框中的值 this.color.fromString(this.customInput); } }, 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(); }); } }, // 子组件监听此变量改变事件 currentColor: { immediate: true, handler(val) { this.customInput = val; } } } }; </script>
src/color.js
/* 其实 color.js 主要是定义了一个 Color 类,简单说下其中一些方法的作用: set 用于设置 Color 中的变量。 get 用于获取 _hue _saturation _value _alpha 这四个值。 toRgb 方法将当前颜色的值(除了透明度)以 RGB 的形式返回。 fromString 方法将传入的颜色值解析成 HSV 格式,并赋值给 _hue _saturation _value 和 _alpha。 doOnChange 方法将会计算颜色值组成字符串传给 value。 */ // hsv 转 hsl 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> // 是否为 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]; }; // hsl 转 hsv const hsl2hsv = function (hue, sat, light) { sat = sat / 100; light = light / 100; let smin = sat; const lmin = Math.max(light, 0.01); let sv; let v; light *= 2; sat *= (light <= 1) ? light : 2 - light; smin *= lmin <= 1 ? lmin : 2 - lmin; v = (light + sat) / 2; sv = light === 0 ? (2 * smin) / (lmin + smin) : (2 * sat) / (light + sat); return { h: hue, s: sv * 100, v: v * 100 }; }; // `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] // rgb 转 hsv 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: h * 360, s: s * 100, v: 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] // hsv 转 rgb 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(); } // 获取属性值 _hue get (prop) { return this['_' + prop]; } // 颜色值转为 rgb 返回 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 = Math.max(0, Math.min(360, h)); this._saturation = Math.max(0, Math.min(100, s)); this._value = Math.max(0, Math.min(100, 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 === 4) { this._alpha = Math.floor(parseFloat(parts[3]) * 100); } else if (parts.length === 3) { this._alpha = 100; } if (parts.length >= 3) { const { h, s, v } = hsl2hsv(parts[0], parts[1], parts[2]); fromHSV(h, s, v); } } 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 === 4) { this._alpha = Math.floor(parseFloat(parts[3]) * 100); } else if (parts.length === 3) { this._alpha = 100; } if (parts.length >= 3) { fromHSV(parts[0], parts[1], parts[2]); } } 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 === 4) { this._alpha = Math.floor(parseFloat(parts[3]) * 100); } else if (parts.length === 3) { this._alpha = 100; } if (parts.length >= 3) { const { h, s, v } = rgb2hsv(parts[0], parts[1], parts[2]); fromHSV(h, s, v); } } else if (value.indexOf('#') !== -1) { const hex = value.replace('#', '').trim(); if (!/^(?:[0-9a-fA-F]{3}){1,2}$/.test(hex)) return; 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 || hex.length === 8) { r = parseHexChannel(hex.substring(0, 2)); g = parseHexChannel(hex.substring(2, 4)); b = parseHexChannel(hex.substring(4, 6)); } if (hex.length === 8) { this._alpha = Math.floor(parseHexChannel(hex.substring(6)) / 255 * 100); } else if (hex.length === 3 || hex.length === 6) { this._alpha = 100; } const { h, s, v } = rgb2hsv(r, g, b); fromHSV(h, s, v); } } compare (color) { return Math.abs(color._hue - this._hue) < 2 && Math.abs(color._saturation - this._saturation) < 1 && Math.abs(color._value - this._value) < 1 && Math.abs(color._alpha - this._alpha) < 1; } // 更具计算结果定义当前颜色值 value 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}, ${Math.round(hsl[1] * 100)}%, ${Math.round(hsl[2] * 100)}%, ${_alpha / 100})`; break; case 'hsv': this.value = `hsva(${_hue}, ${Math.round(_saturation)}%, ${Math.round(_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}, ${Math.round(hsl[1] * 100)}%, ${Math.round(hsl[2] * 100)}%)`; break; case 'hsv': this.value = `hsv(${_hue}, ${Math.round(_saturation)}%, ${Math.round(_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)); } } } };
src/draggable.js
// 选择器拖动效果逻辑
import Vue from 'vue';
let isDragging = false;
export default function (element, options) {
if (Vue.prototype.$isServer) return;
// 移动函数
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);
}
});
}
src/components/alpha-slider.vue src/components/hue-slider.vue src/components/sv-panel.vue三个文件差不多不重复了
<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> <script> import draggable from '../draggable'; export default { name: 'el-color-alpha-slider', props: { color: { required: true }, vertical: 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.set('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.set('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>