element源码之el-color-picker源码阅读

519 阅读3分钟

前言

计划阅读element的源码,之后会写一系列的文章,先来看el-color-picker的

目录结构

阅读源码首先要看下项目的文件结构,下图是element的文件结构。
build是打包构建配置相关的一些文件,examples是组件使用例子,lib是打包后的文件,packages是组件代码文件,src用于存放入口文件及一些辅助工具,test是单元测试文件,types是ts类型文件,以及项目中还有一些配置文件
image.png

找到packages目录下的el-color-picker目录,就是要阅读的部分了
image.png

开始阅读

index.js

在index.js中导出了ColorPicker

import ColorPicker from './src/main';

/* istanbul ignore next */
ColorPicker.install = function(Vue) {
  Vue.component(ColorPicker.name, ColorPicker);
};

export default ColorPicker;
main.vue

main.vue是主体组件,由一个默认颜色的小方块和一个下拉颜色框组成。文档上有下面这些属性,我们看下这些属性是怎么在代码中作用的。

image.png 上面的属性都在main.vue的prop中传入了。其中value是选择颜色的默认值

props: {
    value: String,
    showAlpha: Boolean,
    colorFormat: String,
    disabled: Boolean,
    size: String,
    popperClass: String,
    predefine: Array
},

当showAlpha传入true时,会给一个透明像素的背景图

<span class="el-color-picker__color" :class="{ 'is-alpha': showAlpha }">

image.png color-format是颜色的格式,有hsl / hsv / hex / rgb几种类型。在data中定义了color对象,我们找到color.js看color对象内部是如何实现的。

const color = new Color({
    enableAlpha: this.showAlpha,
    format: this.colorFormat
});
color.js
// hsv转hsl
const hsv2hsl = function(hue, sat, val) {};

// 是否为1.0
const isOnePointZero = function(n) {};

// 是否为百分号
const isPercentage = function(n) {};

// Take input from [0, n] and return it as [0, 1]
const bound01 = function(value, max) {};

// 十进制转十六进制的map
const INT_HEX_MAP = { 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F' };

// 转为十六进制颜色
const toHex = function({ r, g, b }) {};

// 十六进制转十进制
const HEX_INT_MAP = { A: 10, B: 11, C: 12, D: 13, E: 14, F: 15 };

// 解析十六进制
const parseHexChannel = function(hex) {};

// hsl转hsv
const hsl2hsv = function(hue, sat, light) {};

// rgb转hsv
const rgb2hsv = function(r, g, b) {};

// hsv转rgb
const hsv2rgb = function(h, s, v) {};

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];
  }
  // 转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();
    };
    // 一些逻辑...
  }

  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;
  }
  // 根据组件传入的color-format计算需要对应格式的颜色值
  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));
      }
    }
  }
};

接着回到main.vue,在computed中计算了一些属性,这些属性会用在template上

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

根据colorSize属性返回的true/false来绑定了一个对应的类名,disabled属性也是同样的

<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>
css样式

在main.vue中并没有看到css样式,那写在了哪里呢?在packages目录下有个theme-chalk目录,底下的src就是每个组件对应的样式。因为里边的css不像我们平时写的简单的css,对于css样式的分析,这里先不写,后续会专门写一篇分析element的css是如何设计的。

picker-dropdown.vue

picker-dropdown就是下拉选择颜色的组件,由以下组件组成 image.png
hue-slider的实现其实很简单,用css属性就可以实现

// html
<div class="hue-slider"></div>

// css
.hue-slider {
  width: 27px;
  height: 350px;
  background: linear-gradient(to bottom, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red 100%);
}

image.png
sv-panel由3个div组成

// html
<div class="panel">
    <div class="color"></div>
    <div class="mask"></div>
</div>

// css
.color {
  width: 450px;
  height: 350px;
  position: absolute;
  background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.mask {
  width: 450px;
  height: 350px;
  position: absolute;
  background: linear-gradient(to top, #000, transparent);
}
.panel {
  width: 450px;
  height: 350px;
  position: relative;
  background: #00ff7f;
}

image.png
还有个透明度选择alpha-slider,也是差不多的
上面三个组件UI都是用css的line-gradient属性来实现的,line-gradient需要两个参数,具体的可以参考[菜鸟教程](www.runoob.com/cssref/func…)
剩余一个predefine组件就是预定义的颜色遍历展示了

计算逻辑

在移动滑块和点击位置时是如何进行颜色的切换和选择的呢?我们看下主要逻辑
在hue-slider、sv-panel、alpha-slider组件中,鼠标滑动或者点击时,会获取鼠标在当前元素中的位置,根据位置信息去计算颜色值,再进行赋值,从而得到结果。 image.png