产品提了个意见:颜色选择器的颜色能不能不要默认为透明,选完颜色后,就立马确认了,容易忘记设置不透明度(如下图所示)。由于这个颜色是和所有组件的背景色绑定的,设为透明是为了组件更好的展示,改默认颜色固然不可行。其实每当选中颜色的时候,上方的色块会显示当前的颜色,如果用的多,看到这个颜色没变,大概也能知道缺了啥。不过对于用户而言,这确实是个不好的体验,于是,就有了一个想法:如果当前颜色的透明度为 0 ,选择颜色时,将不透明度置为 1,这样上方的色块也能显示当前的颜色,体验会很不错。大体思路有了,开搞!

组件内部逻辑

展开颜色面板,鼠标在上面选中颜色时,会触发内部的回调 childChange,传递的参数是下面这样的:

{
  "h": 0,
  "s": 0.49166666666666664,
  "v": 0.8055555555555556,
  "a": 0, // 不透明度
  "source": "hsva"
}

这里我们只关心 a 的值就好,需要在一个合适的时机把这个 a 由 0 修改成 1。这个修改的只是当前选中颜色,而不是组件绑定的颜色,只有点击【确认】按钮时,才会更改组件绑定的颜色。

功能实现

首先,需要检查当前颜色(v-model 绑定的值)是否透明度为 0,写了一个工具函数 isTransparent

function isTransparent(color) {
  if (color.charAt(0) === "#" && color.length > 7) {
    return color.substr(7) === "00"
  } else if (color.startsWith("rgba")) {
    return color.replace(/._,\s_([01]|0\.\d+)\)$/, "$1") === "0"
  }
  return color === "transparent" // fix: 点击清空,再次打开不生效
}

再判断触发 childChange 时,传入的 data.a 是否为 0,如果 isTransparent(color) && data.a === 0 就触发 modifyOpacity

if (isTransparent(color) && data.a === 0) {
  modifyOpacity(data)
}

function modifyOpacity(data) {
  data.a = 1
}

这里会有一个问题:如果通过拖动设置透明度的滑块,将透明度改为 0 时,同样也会修改 data.a 的值,就会有一个 BUG,当透明度滑到 0 时,滑块会立马跳到 1 的位置。所以只写这两个判断还不能达到要求,我们还需要一个合理的触发时机。

仔细想一想,修改 data.a 的值,应该在每次打开颜色面板后,只触发一次,后续的颜色选择,就不应该再触发了。那么触发的时机就和面板的 visible 相关联,我们需要一个标识 canModifyOpacity 一起约束上面的判断条件:

if (isTransparent(color) && data.a === 0 && canModifyOpacity) {
  modifyOpacity(data)
}

CodeReview 的时候,同事提了一个意见,说上面的那个判断顺序换成下面这样,是不是好点?

if (canModifyOpacity && data.a === 0 && isTransparent(color))

首次展开面板选择颜色时,canModifyOpacity 一定为真,无论在前还是在后,都没有任何影响,并且第一次主要判断的是当前颜色不透明度是否为 0 ,所以把它摆在了第一位,这很合理。当在展开后的面板再次选择颜色时,canModifyOpacity 一定为假,后续的判断都是多余的,所以最优的判断顺序应该是:

if (canModifyOpacity && isTransparent(color) && data.a === 0)

首次展开面板,isTransparent(color) 优先级最高,所以先于 data.a === 0;后续再选择颜色,canModifyOpacity 为假,没必要再进行后面的判断了。

canModifyOpacity 初始值为 true,并且在触发 modifyOpacity 后,需要把 canModifyOpacity 置为 false,这里需要调整一下 modifyOpacity

canModifyOpacity = true
if (canModifyOpacity && isTransparent(color) && data.a === 0) {
  modifyOpacity(data)
}

function modifyOpacity(data) {
  data.a = 1
  canModifyOpacity = false
}

在每次打开颜色面板后,需要将 canModifyOpacity 置为 true,需要监听 ColorPickervisible

ColorPicker.$watch("visible", val => {
  if (val) {
    canModifyOpacity = true
  }
})

基本逻辑都已实现,下面就是 childChange 的劫持:

// 先存一份原函数
const originChildChange = ColorPicker.childChange
// 写自己的新函数
const newChildChange = function (data) {
  // v-model 绑定的值,传入 prop 的 name 就是 value
  if (canModifyOpacity && isTransparent(color) && data.a === 0) {
    modifyOpacity(data)
  }
  originChildChange(data)
}
// 替换实例上的方法
ColorPicker.childChange = newChildChange.bind(ColorPicker)

整体代码

let canModifyOpacity = true
const originChildChange = ColorPicker.childChange
const newChildChange = function (data) {
  if (canModifyOpacity && isTransparent(color) && data.a === 0) {
    modifyOpacity(data)
  }
  originChildChange(data)
}
ColorPicker.childChange = newChildChange.bind(ColorPicker)
ColorPicker.$watch("visible", val => {
  if (val) {
    canModifyOpacity = true
  }
})

function modifyOpacity(data) {
  data.a = 1
  canModifyOpacity = false
}

function isTransparent(color) {
  if (color.charAt(0) === "#" && color.length > 7) {
    return color.substr(7) === "00"
  } else if (color.startsWith("rgba")) {
    return color.replace(/.*,\s*([01]|0\.\d+)\)$/, "$1") === "0"
  }
  return color === "transparent"
}

封装成指令

<template>
  <div>
    <ColorPicker v-modify-opacity />
  </div>
</template>
export default {
  directives: {
    "modify-opacity": {
      bind(el, binding, vnode) {
        const vm = vnode.componentInstance
        let canModifyOpacity = true
        const originChildChange = vm.childChange
        const newChildChange = function (data) {
          if (canModifyOpacity && isTransparent(color) && data.a === 0) {
            modifyOpacity(data)
          }
          originChildChange(data)
        }
        vm.childChange = newChildChange.bind(vm)
        vm.$watch("visible", val => {
          if (val) {
            canModifyOpacity = true
          }
        })

        function modifyOpacity(data) {
          data.a = 1
          canModifyOpacity = false
        }

        function isTransparent(color) {
          if (color.charAt(0) === "#" && color.length > 7) {
            return color.substr(7) === "00"
          } else if (color.startsWith("rgba")) {
            return color.replace(/.*,\s*([01]|0\.\d+)\)$/, "$1") === "0"
          }
          return color === "transparent"
        }
      }
    }
  }
}

最终效果