[Element Plus 源码解析] ColorPicker 颜色选择器

4,770 阅读3分钟

一、组件介绍

官网链接:ColorPicker 颜色选择器 | Element (gitee.io)

ColorPicker颜色选择器组件,用于颜色选择,支持多种颜色格式。

1.1 属性

  • modelValue/v-model: string类型,绑定的颜色值;
  • color-format: string类型,颜色值的格式,如hsl / hsv / hex / rgb
  • show-alpha:boolean类型,是否支持透明度选择,若为true,则展示一个透明度的滑动选择条,默认为false;
  • predefine: array类型,预定义颜色列表,默认为空;
  • size: string类型,尺寸,可选值medium/small/mini;
  • disabled: boolean类型,是否禁用,默认false;
  • popper-class: string类型,下拉弹框的类名;

1.2 事件

  • change:颜色值变化时触发

二、源码分析

2.1 template

<template>
  <!-- 使用el-popper组件展示弹出框内容 -->
  <el-popper
    ref="popper"
    v-model:visible="showPicker"
    effect="light"
    manual-mode
    trigger="click"
    :show-arrow="false"
    :fallback-placements="['bottom', 'top', 'right', 'left']"
    :offset="0"
    transition="el-zoom-in-top"
    :gpu-acceleration="false"
    :popper-class="`el-color-picker__panel el-color-dropdown ${popperClass}`"
    :stop-popper-mouse-event="false"
  >
    <!-- 弹出内容 -->
    <template #default>
      <!-- click-outside指令,点击外部区域时,关闭弹出内容 -->
      <div v-click-outside="hide">
        <div class="el-color-dropdown__main-wrapper">
          <!-- 颜色滑动条,纵向 -->
          <!-- 注意,color属性并不是一个颜色值,而是一个Color的实例 -->
          <hue-slider ref="hue" class="hue-slider" :color="color" vertical />
          <!-- 颜色选择板,矩形 -->
          <sv-panel ref="svPanel" :color="color" />
        </div>
        <!-- 透明度滑动条,横向 -->
        <alpha-slider v-if="showAlpha" ref="alpha" :color="color" />
        <!-- 预定义颜色块 -->
        <predefine v-if="predefine" ref="predefine" :color="color" :colors="predefine" />
        <!-- 色值输入框,确认/清除按钮 -->
        <div class="el-color-dropdown__btns">
          <span class="el-color-dropdown__value">
            <el-input
              v-model="customInput"
              :validate-event="false"
              size="mini"
              @keyup.enter="handleConfirm"
              @blur="handleConfirm"
            />
          </span>
          <el-button
            size="mini"
            type="text"
            class="el-color-dropdown__link-btn"
            @click="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>
    </template>
    <!-- 触发内容,常驻显示 -->
    <template #trigger>
      <div
        :class="[
          'el-color-picker',
          colorDisabled ? 'is-disabled' : '',
          colorSize ? `el-color-picker--${colorSize}` : ''
        ]"
      >
        <!-- disabled状态的掩盖层,起到禁止点击的效果 -->
        <div v-if="colorDisabled" class="el-color-picker__mask"></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>
            <!-- 当前没有选中值时,展示为X -->
            <span
              v-if="!modelValue && !showPanelColor"
              class="el-color-picker__empty el-icon-close"
            ></span>
          </span>
          <!-- 向下箭头 -->
          <span
            v-show="modelValue || showPanelColor"
            class="el-color-picker__icon el-icon-arrow-down"
          ></span>
        </div>
      </div>
    </template>
  </el-popper>
</template>

2.2 script部分

setup(props, { emit }) {
    const ELEMENT = useGlobalConfig()
    const elForm = inject(elFormKey, {} as ElFormContext)
    const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
    // 子组件的ref
    const hue = ref(null)
    const svPanel = ref(null)
    const alpha = ref(null)
    const popper = ref(null)
    // data
    // Color是一个Class,内部定义了一些颜色相关的属性和方法,这里实例化一个Color对象
    const color = reactive(new Color({
      enableAlpha: props.showAlpha,
      format: props.colorFormat,
    }))
    const showPicker = ref(false)
    const showPanelColor = ref(false)
    const customInput = ref('')
    // computed
    // 计算属性,当前展示的颜色
    const displayedColor = computed(() => {
      // 如果没有传值 且 未选中任何颜色,则显示透明
      if (!props.modelValue && !showPanelColor.value) {
        return 'transparent'
      }
      // 否则,转换成rgb/rgba形式
      return displayedRgb(color, props.showAlpha)
    })
    // trigger部分的size: 传入的size > form-item的size > 全局设置的size
    const colorSize = computed(() => {
      return props.size || elFormItem.size || ELEMENT.size
    })
    // 是否禁用
    const colorDisabled = computed(() => {
      return props.disabled || elForm.disabled
    })
    // 当前颜色
    const currentColor = computed(() => {
      return !props.modelValue && !showPanelColor.value ? '' : color.value
    })
    // watch
    // 监测传入值的变化
    watch(() => props.modelValue, newVal => {
      // 新值为空,则trigger显示为X
      if (!newVal) {
        showPanelColor.value = false
      } else if (newVal && newVal !== color.value) {
        // 调用Color的fromString方法,计算出新的颜色
        color.fromString(newVal)
      }
    })
    // 当前颜色变化
    watch(() => currentColor.value, val => {
      // 色值输入框的值同步变化
      customInput.value = val
      // 向上emit事件
      emit('active-change', val)
      // showPanelColor.value = true
    })
    // 监测color.value变化
    watch(() => color.value, () => {
      if (!props.modelValue && !showPanelColor.value) {
        showPanelColor.value = true
      }
    })

    // methods
    // 转换成可用作background-color的rgb格式
    function displayedRgb(color, showAlpha) {
      if (!(color instanceof Color)) {
        throw Error('color should be instance of _color Class')
      }
      // 调用Color的toRgb方法
      const { r, g, b } = color.toRgb()
      return showAlpha
        ? `rgba(${r}, ${g}, ${b}, ${color.get('alpha') / 100})`
        : `rgb(${r}, ${g}, ${b})`
    }

    function setShowPicker(value) {
      showPicker.value = value
    }
    // 防抖
    const debounceSetShowPicker = debounce(setShowPicker, 100)

    // v-click-outside的响应函数,关闭颜色选择弹框,重新设置颜色值
    function hide() {
      debounceSetShowPicker(false)
      resetColor()
    }
    // 重新设置颜色值
    function resetColor() {
      nextTick(() => {
        if (props.modelValue) {
          color.fromString(props.modelValue)
        } else {
          showPanelColor.value = false
        }
      })
    }
    // trigger点击事件
    function handleTrigger() {
      if (colorDisabled.value) return
      // 展示/不展示切换
      debounceSetShowPicker(!showPicker.value)
    }

    // 色值输入框的blur/change事件
    function handleConfirm() {
      // 会触发color.value的改变,进而触发上面的watch方法
      color.fromString(customInput.value)
    }
    // “确定” 按钮事件
    function confirmValue() {
      // 值为当前color.value
      const value = color.value
      // v-model事件
      emit(UPDATE_MODEL_EVENT, value)
      // change事件
      emit('change', value)
      // 在form中时,触发el.form.change事件
      elFormItem.formItemMitt?.emit('el.form.change', value)
      // 关闭弹框
      debounceSetShowPicker(false)
      // check if modelValue change, if not change, then reset color.
      nextTick(() => {
        const newColor = new Color({
          enableAlpha: props.showAlpha,
          format: props.colorFormat,
        })
        newColor.fromString(props.modelValue)
        if (!color.compare(newColor)) {
          resetColor()
        }
      })
    }
    // “清除”颜色 事件
    function clear() {
      // 关闭弹框
      debounceSetShowPicker(false)
      // v-model change事件
      emit(UPDATE_MODEL_EVENT, null)
      emit('change', null)
      if (props.modelValue !== null) {
        elFormItem.formItemMitt?.emit('el.form.change', null)
      }
      // 重设颜色
      resetColor()
    }

    onMounted(() => {
      // 如果有传入值,则用传入值进行初始化
      if (props.modelValue) {
        color.fromString(props.modelValue)
        customInput.value = currentColor.value
      }
    })
    // 显示/隐藏弹框时,调用子组件的update方法,设置子组件的数据
    watch(() => showPicker.value, () => {
      nextTick(() => {
        hue.value?.update()
        svPanel.value?.update()
        alpha.value?.update()
      })
    })
    // 向子组件提供当前的色值
    provide<IUseOptions>(OPTIONS_KEY, {
      currentColor,
    })
  },

2.3 总结:

  1. 颜色有多种格式,涉及到相互转换等计算方法,代码中使用Color class,定义了相关属性及转换计算方法,在父组件中实例化Color,并将实例化对象作为参数传入子组件,子组件可调用实例的方法进行运算;父组件对color.value等进行监听,形成联动;