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>

 

posted on 2019-05-29 14:23  心痛随缘  阅读(1851)  评论(0编辑  收藏  举报