乐趣区

关于前端:从零开始实现一个颜色选择器原生JavaScript实现

筹备工作

我的项目目录与文件创建

首先,咱们无需搭建我的项目的环境,咱们还是间接用最简略的形式,也就是引入的形式来创立这个我的项目,这样也就不便了咱们一边编写一边测试。创立一个空目录,命名为 ColorPicker, 创立一个js 文件,即 color-picker.js, 而后创立一个index.html 文件以及创立一个款式文件color-picker.css。当初你应该能够看到你的我的项目目录是如下所示:

ColorPicker
│  index.html
│  color-picker.js
│  color-picker.css

在你的 index.html 中,初始化 html 文档构造,而后引入这个 color-picker.js 文件,如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>color-picker</title>
    <link rel="stylesheet" href="./color-picker.css" />
  </head>
  <body></body>
  <script src="./color-picker.js"></script>
</html>

做好这些筹备工作之后,让咱们持续下一步。

构造与布局

模块剖析

咱们通过如下一张图来剖析咱们要实现的模块,如下图所示:

正如上图所示,咱们能够将一个色彩选择器拆分成多个模块,所以咱们大抵失去了一个构造如下:

  • 色彩色块
  • 色彩面板
  • 色调柱
  • 透明度柱
  • 输入框
  • 清空与确定按钮
  • 预约义色彩元素列表

这样一来,咱们能够清晰的看到整个色彩选择器都有哪些模块。咱们目前只须要思考开发出根本的模块性能,而后后续就在根底上开始进行扩大和欠缺。好的,让咱们持续下一步,搭建页面的根本构造。

色块模块

通过剖析,咱们应该晓得,色块分成两种状况,第一种就是有色彩值时,色块应该是一个背景色为该色彩值的左右箭头。就像如下图所示:

而无色彩值,咱们的色块应该是如下图所示:

如此一来,咱们就确定了色块的构造元素,如下:

<div class="ew-color-picker-box">
  <!-- 有色彩值, 这里咱们并没有应用任何图标,用 css 来实现一个看起来就像下拉箭头一样 -->
  <div class="ew-color-picker-arrow">
    <div class="ew-color-picker-arrow-left">
      <div class="ew-color-picker-arrow-right"></div>
      <!-- 无色彩值 -->
      <div class="ew-color-picker-no">&times;</div>
    </div>
  </div>
</div>

这里咱们必定是通过一个色彩值来确定应用哪一个构造的,这个后续咱们再说。咱们当初就先确定色块的元素构造应该是如下这样呢。当然这里的类名也能够是本人轻易自定义。

tips: 我这里是为了有本人的特色,所以加了 ew- 前缀名。如果你本人应用本人自定义的类名,那么你后续编写款式和操作 DOM 元素的时候须要留神,要去更改。

还有留神 &times; 它是 HTML 字符实体,咱们只须要晓得它最终会显示为X 就行了,这里不会去细讲,欲了解更多 HTML 字符实体常识,能够返回 HTML 字符实体
查看。

接下来,让咱们实现色块的款式编写。咱们先实现最外层的盒子元素。能够看到,最外层的它会有一个自定义的宽高,而后就是一个边框,其它的就没有什么了,这样一来,咱们就晓得了该编写什么样的 CSS 代码。这里咱们还是采纳自身写好的款式。咱们做个记录:

  • 色块盒子的边框色彩为#dcdee2
  • 色块盒子的字体色彩为#535353
  • 色块盒子有 4px 的圆角
  • 色块盒子有高低 4px 的内间距,7px的左右内间距
  • 色块盒子有 14px 的字体大小
  • 色块盒子有 1.5 的行高,留神没有单位

tips:1.5 倍行高是一个相对值,它是依据浏览器设置的字体大小来决定的,例如浏览器字体大小为 16px, 那么 1.5 倍行高就是 16px * 1.5 = 24px 的行高

看到以上几点要求,咱们应该晓得,咱们要采纳哪个 CSS 属性来实现,脑海中要有一个清晰的意识。

.ew-color-picker-box {
  /* 边框色彩为 #dcdee2 */
  border: 1px solid #dcdee2;
  /* 边框有 4px 的圆角 */
  border-radius: 4px;
  /* 4px 的高低内间距,7px 的左右内间距 */
  padding: 4px 7px;
}

最外层的盒子元素的款式,咱们曾经编写实现了,接下来,咱们开始编写没有色彩值的时候的一个款式。实际上它和最外层的色块盒子款式差不多,惟一须要留神的就是,咱们后续将通过 js 来设置它的宽高以及行高了。因为它是动静扭转的,不过这里咱们能够先固定一个值,而后后续再做更改。

.ew-color-picker-box > .ew-color-box-no {
  width: 40px;
  height: 40px;
  font-size: 20px;
  line-height: 40px;
  color: #5e535f;
  border: 1px solid #e2dfe2;
  border-radius: 2px;
}

接下来就是实现有色彩值的款式了,这个要有一点难度,难点在于咱们如何去实现一个相似下拉框箭头一样的下箭头。咱们通过剖析页面构造元素,不难看出,实际上咱们这里的下箭头很显著是通过两个元素来拼凑成的,也就是说一个元素只是一根旋转了 45deg 的横线,同样的情理,另一个元素无非是旋转的方向相同罢了。并且咱们能够看到这两根横线是垂直程度居中的,这里,咱们必定很快就想到了弹性盒子布局,只须要两个属性就能够让元素垂直程度居中。即 justify-content:centeralign-items:center这两个属性。所以,通过这样一剖析,咱们这里的实现就不难了。

2D 坐标系

3D 坐标系

如下所示:

.ew-color-picker-box-arrow {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 40px;
  height: 40px;
  margin: auto;
  z-index: 3;
}
.ew-color-picker-box-arrow-left {
  width: 12px;
  height: 1px;
  display: inline-block;
  background-color: #fff;
  position: relative;
  transform: rotate(45deg);
}
.ew-color-picker-box-arrow-right {
  width: 12px;
  height: 1px;
  display: inline-block;
  background-color: #fff;
  position: relative;
  transform: rotate(-45deg);
  right: 3px;
}

如此一来,色块模块的页面构造和款式就这样被咱们实现了,让咱们持续。

色彩面板

色彩面板也是整个色彩选择器中最难的局部,当初咱们来剖析一下构造。首先,咱们能够看到,它有一个容器元素,这个容器元素有点暗影成果,背景色是红色。这里须要晓得的一个知识点就是盒子模型,也就是 box-sizing 属性,它有 2 个属性值:content-box,border-box。事实上在理论开发中,咱们用到最多的是border-box。咱们来看文档 box-sizing。

通过文档形容,咱们晓得了这个属性的意思。那么这里这个色彩面板容器元素的盒子模型咱们就须要留神了,在这里,它是 规范盒子模型,也就是咱们只是独自蕴含内容的宽高就行了。因而,咱们总结如下:

  • 1px 的实线边框 #ebeeff
  • 盒子模型为规范盒子模型
  • 暗影成果文档
  • 7px 的内边距
  • 5px 的圆角

tips: 这里留一个悬念,为什么要应用规范盒子模型。

到此为止,咱们的容器元素就剖析实现了,接下来开始编写构造与款式。

<div class="ew-color-picker">
  <!-- 当然外面的构造后续再剖析 -->
</div>
.ew-color-picker {
  min-width: 320px;
  box-sizing: content-box;
  border: 1px solid #ebeeff;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  border-radius: 5px;
  z-index: 10;
  padding: 7px;
  text-align: left;
}

当初咱们再来确定容器元素中都有哪些元素,首先是一个色彩面板,色彩面板又蕴含一个容器元素,咱们能够看到,色彩面板很像是三种背景色叠加进去的成果,不必狐疑,大胆的说,是的没错,就是三种背景色叠加进去的,所以咱们就须要一个容器元素,而后容器元素外面又蕴含 2 个面板元素,容器元素的背景色加上 2 个面板元素叠加进去就是这种成果。一个红色的背景加一个彩色的就能叠加看到咱们想要的成果。
比方咱们先来看看一个示例:

<div class="panel">
  <div class="white-panel"></div>
  <div class="black-panel"></div>
</div>
.panel {
  width: 280px;
  height: 180px;
  position: relative;
  border: 1px solid #fff;
  background-color: rgb(255, 166, 0);
}
.panel > div.white-panel,
.panel > div.black-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}
.white-panel {background: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0));
}
.black-panel {background: linear-gradient(0deg, #000, transparent);
}

这里可能又波及到一个知识点,那就是突变色彩,这里就不做细讲,感兴趣的可查看文档。

所以咱们的构造应该是如下:

<div class="ew-color-picker-content">
  <div class="ew-color-picker-panel">
    <div class="ew-color-picker-white-panel"></div>
    <div class="ew-color-picker-black-panel"></div>
  </div>
</div>

依据后面那个示例,咱们很快就能写出这个色彩面板了,不过咱们还少了一个,也就是在色彩面板区域之内的拖动元素,或者咱们能够称之为游标元素。

.ew-color-picker-panel {
  width: 280px;
  height: 180px;
  position: relative;
  border: 1px solid #fff;
  background-color: rgb(255, 166, 0);
  cursor: pointer;
}
.ew-color-picker-panel > div.ew-color-picker-white-panel,
.ew-color-picker-panel > div.ew-color-picker-black-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}
.ew-color-picker-white-panel {background: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0));
}
.ew-color-picker-black-panel {background: linear-gradient(0deg, #000, transparent);
}

好了,当初我能够答复之前那个留下的问题了,为什么要应用规范盒子模型而不是 IE 规范盒子模型。这是因为这里咱们会通过 js 动静去计算游标元素拖动的间隔,如果是 IE 规范盒子模型,则会思考边框的大小以及间距的大小,这无疑给咱们计算拖动间隔减少了难度,所以为了简便化,咱们应用的是规范盒子模型。

当初咱们再来加上这个游标元素吧,因为它是在色彩面板内动静扭转的,通常咱们要让一个元素在父元素当中进行挪动,那么咱们很显著就想到了子元素应用相对定位,父元素加一个除了动态定位 static 以外的定位,通常咱们用绝对定位,这里也不例外。这也就是咱们给 .ew-color-picker-panel 增加一个绝对定位 position: relative; 的起因。

<div class="ew-color-picker-content">
  <div class="ew-color-picker-panel">
    <!-- 省略了一些内容,游标元素增加 -->
    <div class="ew-color-picker-panel-cursor"></div>
  </div>
</div>

这里须要留神了,游标元素设置的宽高会影响咱们后续计算,所以在这里设置的宽高是多少,后续计算就要将它的宽高思考在内,这个到前面会细讲,当初,咱们还是编写该元素的款式吧。

.ew-color-picker-panel-cursor {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  position: absolute;
  left: 100%;
  top: 0;
  transform: translate(-4px, -4px);
  box-shadow: 0 0 0 3px #fff, inset 0 0 2px 2px rgb(0 0 0 / 40%),
    /* 等价于 rgba(0,0,0,0.4)*/ 0 0 2px 3px rgb(0 0 0 / 50%); /* 等价于 rgba(0,0,0,0.5)*/
  cursor: default;
}

游标元素,咱们看起来就像是一个小圆圈,所以咱们给的宽高不是很多,只有 4px,既然是圆,咱们都晓得能够应用 border-radius50%即能够将一个元素变成圆。接下来就是暗影局部,这样就实现了咱们的小圆圈。当然咱们不肯定非要实现这样的成果,然而为了还原色彩选择器自身,也不便后续的计算,所以咱们还是采纳本来的款式。

色阶柱

接下来,咱们来看一下色阶柱也就是色调柱的实现。看到这个图,咱们应该能够很清晰的分杰出阶柱蕴含了 2 个局部,第一个局部就是柱形局部,称之为 bar, 第二个局部就是拖动滑块局部,称之为 thumb。而后咱们外加一个容器元素用于蕴含色阶柱和通明柱,所以咱们能够确定色阶柱的构造如下:

<!-- 容器元素 -->
<div class="ew-color-slider ew-is-vertical">
  <div class="ew-color-slider-bar">
    <div class="ew-color-slider-thumb"></div>
  </div>
</div>

而后咱们来确定款式的实现,首先整个色阶柱是垂直布局的,所以咱们应该晓得它就是有一个固定宽度,而后高度等价于色彩面板的矩形,它的背景色通过一种渐变色来实现,实际上就是红橙黄绿青蓝紫七种色彩的混合,也就相似彩虹。这每一种色彩都有不同的比例。其次咱们还要晓得滑块局部是须要动静拖动的。在这里咱们能够设想失去色阶柱能够是程度或者垂直布局的,目前咱们先实现垂直布局(为了辨别给容器元素加一个类名 ew-is-vertical)。所以滑块的动静扭转局部应该是 top 值。当初咱们来看款式:

.ew-color-slider,
.ew-color-slider-bar {position: relative;}
.ew-color-slider.ew-is-vertical {
  width: 28px;
  height: 100%;
  cursor: pointer;
  float: right;
}
.ew-color-slider.ew-is-vertical .ew-color-slider-bar {
  width: 12px;
  height: 100%;
  float: left;
  margin-left: 3px;
  background: linear-gradient(
    180deg,
    #f00 0,
    #ff0 17%,
    #0f0 33%,
    #0ff 50%,
    #00f 67%,
    #f0f 83%,
    #f00
  );
}
.ew-color-slider-thumb {
  background-color: #fff;
  border-radius: 4px;
  position: absolute;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
  border: 1px solid #dcdee2;
  left: 0;
  top: 0;
  box-sizing: border-box;
  position: absolute;
}

到目前为止,咱们色阶柱就算是实现了,接下来来看透明度柱的实现。

透明度柱

透明度柱的实现原理跟色阶柱很类似,首先咱们能够看到透明度柱会有一个通明的背景,这个背景很显然是一个图片,其次它还会有一个背景色条,取决于当且色阶柱处于哪种色调,而后同样还是与色阶柱一样有一个滑块,同样也是有垂直布局和程度布局,扭转 top 值。所以咱们失去构造如下所示:

<div class="ew-alpha-slider-bar">
  <!-- 背景图 -->
  <div class="ew-alpha-slider-wrapper"></div>
  <!-- 背景色 -->
  <div class="ew-alpha-slider-bg"></div>
  <!-- 滑块元素 -->
  <div class="ew-alpha-slider-thumb"></div>
</div>

在这里,咱们须要留神的一点就是背景色条的背景色是动静扭转,这将在前面会讲到。背景色条,咱们同样是通过线性突变来实现的。让咱们来看看款式吧:

.ew-alpha-slider-bar {
  width: 12px;
  height: 100%;
  float: left;

  position: relative;
}
.ew-alpha-slider-wrapper,
.ew-alpha-slider-bg {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
}
.ew-alpha-slider-bar.ew-is-vertical .ew-alpha-slider-bg {
  /* 这里先临时写死 */
  background: linear-gradient(
    to top,
    rgba(255, 0, 0, 0) 0%,
    rgba(255, 0, 0) 100%
  );
}
.ew-alpha-slider-wrapper {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==");
}
.ew-alpha-slider-thumb {
  background-color: #fff;
  border-radius: 4px;
  position: absolute;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
  border: 1px solid #dcdee2;
  left: 0;
  top: 0;
  box-sizing: border-box;
  position: absolute;
}

好了,到目前为止,咱们的透明度柱也就实现了, 接下来咱们来看输入框的实现。

输入框与按钮

输入框比较简单,我想没什么好说的,这个输入框也能够自定义,它的构造无非就是如下:

<input class="ew-color-input" />

它和清空与确定按钮元素排在一行,因而咱们用一个容器元素来包裹它们,构造应该如下:

<div class="ew-color-drop-container">
  <input class="ew-color-input" />
  <div class="ew-color-drop-btn-group">
    <button type="button" class="ew-color-drop-btn ew-color-clear"> 清空 </button>
    <button type="button" class="ew-color-drop-btn ew-color-sure"> 确定 </button>
  </div>
</div>

而后款式也没有什么好剖析的,都是一些根底款式,咱们持续编写代码。如下:

.ew-color-drop-container {
  margin-top: 6px;
  padding-top: 4px;
  min-height: 28px;
  border-top: 1px solid #cdcdcd;
  position: relative;
}
.ew-color-input {
  display: inline-block;
  padding: 8px 12px;
  border: 1px solid #e9ebee;
  border-radius: 4px;
  outline: none;
  width: 160px;
  height: 28px;
  line-height: 28px;
  border: 1px solid #dcdfe6;
  padding: 0 5px;
  -webkit-transition: border-color 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  transition: border-color 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  border-radius: 5px;
  background-color: #fff;
}
.ew-color-drop-btn-group {
  position: absolute;
  right: 0;
  top: 5px;
}
.ew-color-drop-btn {
  padding: 5px;
  font-size: 12px;
  border-radius: 3px;
  -webkit-transition: 0.1s;
  transition: 0.1s;
  font-weight: 500;
  margin: 0;
  white-space: nowrap;
  color: #606266;
  border: 1px solid #dcdfe6;
  letter-spacing: 1px;
  text-align: center;
  cursor: pointer;
}
.ew-color-clear {
  color: #4096ef;
  border-color: transparent;
  background-color: transparent;
  padding-left: 0;
  padding-right: 0;
}
.ew-color-clear:hover {color: #66b1ff;}
.ew-color-sure {margin-left: 10px;}
.ew-color-sure {
  border-color: #4096ef;
  color: #4096ef;
}

输入框和按钮咱们就曾经实现了,接下来咱们再来看预约义色彩元素呢。

预约义色彩

预约义色彩元素实现起来也比较简单,就是一个容器元素,而后蕴含多个子元素,可能略微难一点的就是子元素的款式咱们分为四种状况,第一种就是默认的款式,第二种就是禁止点击的款式,除此之外,咱们还加了一个色彩透明度之间的区别,而后最初就是选中款式。不多说,咱们能够先写 4 个子元素来别离代表四种状况的款式。如下:

<div class="ew-pre-define-color-container">
  <div class="ew-pre-define-color" tabindex="0"></div>
  <div class="ew-pre-define-color ew-has-alpha" tabindex="1"></div>
  <div
    class="ew-pre-define-color ew-pre-define-color-disabled"
    tabindex="2"
  ></div>
  <div
    class="ew-pre-define-color ew-pre-define-color-active"
    tabindex="3"
  ></div>
</div>

接下来,咱们来看款式的实现:

.ew-pre-define-color-container {
  width: 280px;
  font-size: 12px;
  margin-top: 8px;
}
.ew-pre-define-color-container::after {
  content: "";
  display: table;
  height: 0;
  visibility: hidden;
  clear: both;
}
.ew-pre-define-color-container .ew-pre-define-color {
  margin: 0 0 8px 8px;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  border: 1px solid #9b979b;
  cursor: pointer;
  float: left;
}
.ew-pre-define-color-container .ew-pre-define-color:hover {opacity: 0.8;}
.ew-pre-define-color-active {box-shadow: 0 0 3px 2px #409eff;}
.ew-pre-define-color:nth-child(10n + 1) {margin-left: 0;}
.ew-pre-define-color.ew-has-alpha {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==");
}
.ew-pre-define-color.ew-pre-define-color-disabled {cursor: not-allowed;}

款式和布局就到此结束了,接下来才是咱们的重点,也就是实现色彩选择器的性能。

JavaScript

工具办法

首先用一个空对象来管理工具办法。如下:

const util = Object.create(null);

而后有如下办法:

const util = Object.create(null);
const _toString = Object.prototype.toString;
let addMethod = (instance, method, func) => {instance.prototype[method] = func;
  return instance;
};
["Number", "String", "Function", "Undefined", "Boolean"].forEach((type) => (util["is" + type] = (value) => typeof value === type.toLowerCase())
);
util.addMethod = addMethod;
["Object", "Array", "RegExp"].forEach((type) =>
    (util["isDeep" + type] = (value) =>
      _toString.call(value).slice(8, -1).toLowerCase() === type.toLowerCase())
);
util.isShallowObject = (value) =>
  typeof value === "object" && !util.isNull(value);
util["ewObjToArray"] = (value) =>
  util.isShallowObject(value) ? Array.prototype.slice.call(value) : value;
util.isNull = (value) => value === null;
util.ewAssign = function (target) {if (util.isNull(target)) return;
  const _ = Object(target);
  for (let j = 1, len = arguments.length; j < len; j += 1) {const source = arguments[j];
    if (source) {for (let key in source) {if (Object.prototype.hasOwnProperty.call(source, key)) {_[key] = source[key];
        }
      }
    }
  }
  return _;
};
util.addClass = (el, className) => el.classList.add(className);
util.removeClass = (el, className) => el.classList.remove(className);
util.hasClass = (el, className) => {let _hasClass = (value) =>
    new RegExp("" + el.className +" ").test(" "+ value +" ");
  if (util.isDeepArray(className)) {return className.some((name) => _hasClass(name));
  } else {return _hasClass(className);
  }
};
util["setCss"] = (el, prop, value) => el.style.setProperty(prop, value);
util.setSomeCss = (el, propValue = []) => {if (propValue.length) {propValue.forEach((p) => util.setCss(el, p.prop, p.value));
  }
};
util.isDom = (el) =>
  util.isShallowObject(HTMLElement)
    ? el instanceof HTMLElement
    : (el &&
        util.isShallowObject(el) &&
        el.nodeType === 1 &&
        util.isString(el.nodeName)) ||
      el instanceof HTMLCollection ||
      el instanceof NodeList;
util.ewError = (value) =>
  console.error("[ewColorPicker warn]\n" + new Error(value));
util.ewWarn = (value) => console.warn("[ewColorPicker warn]\n" + value);
util.deepCloneObjByJSON = (obj) => JSON.parse(JSON.stringify(obj));
util.deepCloneObjByRecursion = function f(obj) {if (!util.isShallowObject(obj)) return;
  let cloneObj = util.isDeepArray(obj) ? [] : {};
  for (let k in obj) {cloneObj[k] = util.isShallowObject(obj[k]) ? f(obj[k]) : obj[k];
  }
  return cloneObj;
};
util.getCss = (el, prop) => window.getComputedStyle(el, null)[prop];
util.$ = (ident) => {if (!ident) return null;
  return document[ident.indexOf("#") > -1 ? "querySelector" : "querySelectorAll"
  ](ident);
};
util["on"] = (element, type, handler, useCapture = false) => {if (element && type && handler) {element.addEventListener(type, handler, useCapture);
  }
};
util["off"] = (element, type, handler, useCapture = false) => {if (element && type && handler) {element.removeEventListener(type, handler, useCapture);
  }
};
util["getRect"] = (el) => el.getBoundingClientRect();
util["baseClickOutSide"] = (element, isUnbind = true, callback) => {const mouseHandler = (event) => {const rect = util.getRect(element);
    const target = event.target;
    if (!target) return;
    const targetRect = util.getRect(target);
    if (
      targetRect.x >= rect.x &&
      targetRect.y >= rect.y &&
      targetRect.width <= rect.width &&
      targetRect.height <= rect.height
    )
      return;
    if (util.isFunction(callback)) callback();
    if (isUnbind) {
      // 提早解除绑定
      setTimeout(() => {util.off(document, util.eventType[0], mouseHandler);
      }, 0);
    }
  };
  util.on(document, util.eventType[0], mouseHandler);
};
util["clickOutSide"] = (context, config, callback) => {const mouseHandler = (event) => {const rect = util.getRect(context.$Dom.picker);
    let boxRect = null;
    if (config.hasBox) {boxRect = util.getRect(context.$Dom.box);
    }
    const target = event.target;
    if (!target) return;
    const targetRect = util.getRect(target);
    // 利用 rect 来判断用户点击的中央是否在色彩选择器面板区域之内
    if (config.hasBox) {
      if (
        targetRect.x >= rect.x &&
        targetRect.y >= rect.y &&
        targetRect.width <= rect.width
      )
        return;
      // 如果点击的是盒子元素
      if (
        targetRect.x >= boxRect.x &&
        targetRect.y >= boxRect.y &&
        targetRect.width <= boxRect.width &&
        targetRect.height <= boxRect.height
      )
        return;
      callback();} else {
      if (
        targetRect.x >= rect.x &&
        targetRect.y >= rect.y &&
        targetRect.width <= rect.width &&
        targetRect.height <= rect.height
      )
        return;
      callback();}
    setTimeout(() => {util.off(document, util.eventType[0], mouseHandler);
    }, 0);
  };
  util.on(document, util.eventType[0], mouseHandler);
};
util["createUUID"] = () =>
  (Math.random() * 10000000).toString(16).substr(0, 4) +
  "-" +
  new Date().getTime() +
  "-" +
  Math.random().toString().substr(2, 5);
util.removeAllSpace = (value) => value.replace(/\s+/g, "");
util.isJQDom = (dom) =>
  typeof window.jQuery !== "undefined" && dom instanceof jQuery;
//the event
util.eventType = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)
  ? ["touchstart", "touchmove", "touchend"]
  : ["mousedown", "mousemove", "mouseup"];

动画函数的封装

const animation = {};
function TimerManager() {this.timers = [];
  this.args = [];
  this.isTimerRun = false;
}
TimerManager.makeTimerManage = function (element) {
  const elementTimerManage = element.TimerManage;
  if (!elementTimerManage || elementTimerManage.constructor !== TimerManager) {element.TimerManage = new TimerManager();
  }
};
const methods = [
  {
    method: "add",
    func: function (timer, args) {this.timers.push(timer);
      this.args.push(args);
      this.timerRun();},
  },
  {
    method: "timerRun",
    func: function () {if (!this.isTimerRun) {let timer = this.timers.shift(),
          args = this.args.shift();
        if (timer && args) {
          this.isTimerRun = true;
          timer(args[0], args[1]);
        }
      }
    },
  },
  {
    method: "next",
    func: function () {
      this.isTimerRun = false;
      this.timerRun();},
  },
];
methods.forEach((method) =>
  util.addMethod(TimerManager, method.method, method.func)
);
function runNext(element) {
  const elementTimerManage = element.TimerManage;
  if (elementTimerManage && elementTimerManage.constructor === TimerManager) {elementTimerManage.next();
  }
}
function registerMethods(type, element, time) {
  let transition = "";
  if (type.indexOf("slide") > -1) {
    transition = "height" + time + "ms";
    util.setCss(element, "overflow", "hidden");
    upAndDown();} else {
    transition = "opacity" + time + "ms";
    inAndOut();}
  util.setCss(element, "transition", transition);
  function upAndDown() {const isDown = type.toLowerCase().indexOf("down") > -1;
    if (isDown) util.setCss(element, "display", "block");
    const getPropValue = function (item, prop) {let v = util.getCss(item, prop);
      return util.removeAllSpace(v).length ? parseInt(v) : Number(v);
    };
    const elementChildHeight = [].reduce.call(
      element.children,
      (res, item) => {
        res +=
          item.offsetHeight +
          getPropValue(item, "margin-top") +
          getPropValue(item, "margin-bottom");
        return res;
      },
      0
    );
    let totalHeight = Math.max(element.offsetHeight, elementChildHeight + 10);
    let currentHeight = isDown ? 0 : totalHeight;
    let unit = totalHeight / (time / 10);
    if (isDown) util.setCss(element, "height", "0px");
    let timer = setInterval(() => {
      currentHeight = isDown ? currentHeight + unit : currentHeight - unit;
      util.setCss(element, "height", currentHeight + "px");
      if (currentHeight >= totalHeight || currentHeight <= 0) {clearInterval(timer);
        util.setCss(element, "height", totalHeight + "px");
        runNext(element);
      }
      if (!isDown && currentHeight <= 0) {util.setCss(element, "display", "none");
        util.setCss(element, "height", "0");
      }
    }, 10);
  }
  function inAndOut() {const isIn = type.toLowerCase().indexOf("in") > -1;
    let timer = null;
    let unit = (1 * 100) / (time / 10);
    let curAlpha = isIn ? 0 : 100;
    util.setSomeCss(element, [
      {
        prop: "display",
        value: isIn ? "none" : "block",
      },
      {
        prop: "opacity",
        value: isIn ? 0 : 1,
      },
    ]);
    let handleFade = function () {
      curAlpha = isIn ? curAlpha + unit : curAlpha - unit;
      if (element.style.display === "none" && isIn)
        util.setCss(element, "display", "block");
      util.setCss(element, "opacity", (curAlpha / 100).toFixed(2));
      if (curAlpha >= 100 || curAlpha <= 0) {if (timer) clearTimeout(timer);
        runNext(element);
        if (curAlpha <= 0) util.setCss(element, "display", "none");
        util.setCss(element, "opacity", curAlpha >= 100 ? 1 : 0);
      } else {timer = setTimeout(handleFade, 10);
      }
    };
    handleFade();}
}
["slideUp", "slideDown", "fadeIn", "fadeOut"].forEach((method) => {animation[method] = function (element) {TimerManager.makeTimerManage(element);
    element.TimerManage.add(function (element, time) {return registerMethods(method, element, time);
    }, arguments);
  };
});

一些色彩操作的算法

const colorRegExp = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
// RGB color
const colorRegRGB =
  /[rR][gG][Bb][Aa]?[\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}[\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?[\s]*(0\.\d{1,2}|1|0)?[\)]{1}/g;
// RGBA color
const colorRegRGBA =
  /^[rR][gG][Bb][Aa][\(]([\\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\\s]*,){3}[\\s]*(1|1.0|0|0?.[0-9]{1,2})[\\s]*[\)]{1}$/;
// hsl color
const colorRegHSL =
  /^[hH][Ss][Ll][\(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*)[\)]$/;
// HSLA color
const colorRegHSLA =
  /^[hH][Ss][Ll][Aa][\(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,){2}([\\s]*(1|1.0|0|0?.[0-9]{1,2})[\\s]*)[\)]$/;
/**
 * hex to rgba
 * @param {*} hex
 * @param {*} alpha
 */
function colorHexToRgba(hex, alpha) {
  let a = alpha || 1,

  
    hColor = hex.toLowerCase(),
    hLen = hex.length,
    rgbaColor = [];
  if (hex && colorRegExp.test(hColor)) {
    //the hex length may be 4 or 7,contained the symbol of #
    if (hLen === 4) {
      let hSixColor = "#";
      for (let i = 1; i < hLen; i++) {let sColor = hColor.slice(i, i + 1);
        hSixColor += sColor.concat(sColor);
      }
      hColor = hSixColor;
    }
    for (let j = 1, len = hColor.length; j < len; j += 2) {rgbaColor.push(parseInt("0X" + hColor.slice(j, j + 2), 16));
    }
    return util.removeAllSpace("rgba(" + rgbaColor.join(",") + "," + a + ")");
  } else {return util.removeAllSpace(hColor);
  }
}
/**
 * rgba to hex
 * @param {*} rgba
 */
function colorRgbaToHex(rgba) {const hexObject = { 10: "A", 11: "B", 12: "C", 13: "D", 14: "E", 15: "F"},
    hexColor = function (value) {value = Math.min(Math.round(value), 255);
      const high = Math.floor(value / 16),
        low = value % 16;
      return "" + (hexObject[high] || high) + (hexObject[low] || low);
    };
  const value = "#";
  if (/rgba?/.test(rgba)) {
    let values = rgba
        .replace(/rgba?\(/, "")
        .replace(/\)/, "")
        .replace(/[\s+]/g, "")
        .split(","),
      color = "";
    values.map((value, index) => {if (index <= 2) {color += hexColor(value);
      }
    });
    return util.removeAllSpace(value + color);
  }
}
/**
 * hsva to rgba
 * @param {*} hsva
 * @param {*} alpha
 */
function colorHsvaToRgba(hsva, alpha) {
  let r,
    g,
    b,
    a = hsva.a; //rgba(r,g,b,a)
  let h = hsva.h,
    s = (hsva.s * 255) / 100,
    v = (hsva.v * 255) / 100; //hsv(h,s,v)
  if (s === 0) {r = g = b = v;} else {
    let t = v,
      p = ((255 - s) * v) / 255,
      q = ((t - p) * (h % 60)) / 60;
    if (h === 360) {
      r = t;
      g = b = 0;
    } else if (h < 60) {
      r = t;
      g = p + q;
      b = p;
    } else if (h < 120) {
      r = t - q;
      g = t;
      b = p;
    } else if (h < 180) {
      r = p;
      g = t;
      b = p + q;
    } else if (h < 240) {
      r = p;
      g = t - q;
      b = t;
    } else if (h < 300) {
      r = p + q;
      g = p;
      b = t;
    } else if (h < 360) {
      r = t;
      g = p;
      b = t - q;
    } else {r = g = b = 0;}
  }
  if (alpha >= 0 || alpha <= 1) a = alpha;
  return util.removeAllSpace(
    "rgba(" +
      Math.ceil(r) +
      "," +
      Math.ceil(g) +
      "," +
      Math.ceil(b) +
      "," +
      a +
      ")"
  );
}
/**
 * hsla to rgba
 * 换算公式:https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSL%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2
 * @param {*} hsla
 */
function colorHslaToRgba(hsla) {
  let h = hsla.h,
    s = hsla.s / 100,
    l = hsla.l / 100,
    a = hsla.a;
  let r, g, b;
  if (s === 0) {r = g = b = l;} else {let compareRGB = (p, q, t) => {if (t > 1) t = t - 1;
      if (t < 0) t = t + 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * 6 * (2 / 3 - t);
      return p;
    };
    let q = l >= 0.5 ? l + s - l * s : l * (1 + s),
      p = 2 * l - q,
      k = h / 360;
    r = compareRGB(p, q, k + 1 / 3);
    g = compareRGB(p, q, k);
    b = compareRGB(p, q, k - 1 / 3);
  }
  return util.removeAllSpace(`rgba(${Math.ceil(r * 255)},${Math.ceil(g * 255)},${Math.ceil(b * 255)},${a})`
  );
}
/**
 * rgba to hsla
 * 换算公式:https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSL%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2
 * @param {*} rgba
 */
function colorRgbaToHsla(rgba) {
  const rgbaArr = rgba
    .slice(rgba.indexOf("(") + 1, rgba.lastIndexOf(")"))
    .split(",");
  let a = rgbaArr.length < 4 ? 1 : Number(rgbaArr[3]);
  let r = parseInt(rgbaArr[0]) / 255,
    g = parseInt(rgbaArr[1]) / 255,
    b = parseInt(rgbaArr[2]) / 255;
  let max = Math.max(r, g, b),
    min = Math.min(r, g, b);
  let h,
    s,
    l = (max + min) / 2;

  if (max === min) {h = s = 0;} else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r:
        h = (g - b) / d + (g >= b ? 0 : 6);
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      case b:
        h = (r - g) / d + 4;
        break;
    }
  }
  return {
    colorStr: util.removeAllSpace(
      "hsla(" +
        Math.ceil(h * 60) +
        "," +
        Math.ceil(s * 100) +
        "%," +
        Math.ceil(l * 100) +
        "%," +
        a +
        ")"
    ),
    colorObj: {
      h,
      s,
      l,
      a,
    },
  };
}
/**
 * rgba to hsva
 * @param {*} rgba
 */
function colorRgbaToHsva(rgba) {
  const rgbaArr = rgba
    .slice(rgba.indexOf("(") + 1, rgba.lastIndexOf(")"))
    .split(",");
  let a = rgbaArr.length < 4 ? 1 : Number(rgbaArr[3]);
  let r = parseInt(rgbaArr[0]) / 255,
    g = parseInt(rgbaArr[1]) / 255,
    b = parseInt(rgbaArr[2]) / 255;
  let h, s, v;
  let min = Math.min(r, g, b);
  let max = (v = Math.max(r, g, b));
  let diff = max - min;
  if (max === 0) {s = 0;} else {s = 1 - min / max;}
  if (max === min) {h = 0;} else {switch (max) {
      case r:
        h = (g - b) / diff + (g < b ? 6 : 0);
        break;
      case g:
        h = 2.0 + (b - r) / diff;
        break;
      case b:
        h = 4.0 + (r - g) / diff;
        break;
    }
    h = h * 60;
  }

  s = s * 100;
  v = v * 100;
  return {
    h,
    s,
    v,
    a,
  };
}
/*
 * 任意色值(甚至是 CSS 色彩关键字)转换为 RGBA 色彩的办法
 * 此办法 IE9+ 浏览器反对,基于 DOM 个性实现
 * @param {*} color
 */
function colorToRgba(color) {const div = document.createElement("div");
  util.setCss(div, "background-color", color);
  document.body.appendChild(div);
  const c = util.getCss(div, "background-color");
  document.body.removeChild(div);
  let isAlpha = c.match(/,/g) && c.match(/,/g).length > 2;
  let result = isAlpha
    ? c
    : c.slice(0, 2) + "ba" + c.slice(3, c.length - 1) + ", 1)";
  return util.removeAllSpace(result);
}
/**
 * 判断是否是合格的色彩值
 * @param {*} color
 */
function isValidColor(color) {
  // https://developer.mozilla.org/zh-CN/docs/Web/CSS/color_value#%E8%89%B2%E5%BD%A9%E5%85%B3%E9%94%AE%E5%AD%97
  let isTransparent = color === "transparent";
  return (colorRegExp.test(color) ||
    colorRegRGB.test(color) ||
    colorRegRGBA.test(color) ||
    colorRegHSL.test(color) ||
    colorRegHSLA.test(color) ||
    (colorToRgba(color) !== "rgba(0,0,0,0)" && !isTransparent) ||
    isTransparent
  );
}
/**
 *
 * @param {*} color
 * @returns
 */
function isAlphaColor(color) {
  return (colorRegRGB.test(color) ||
    colorRegRGBA.test(color) ||
    colorRegHSL.test(color) ||
    colorRegHSLA.test(color)
  );
}

工具办法这些咱们曾经实现了,接下来就是正式实现咱们的主线性能逻辑了。

构造函数的定义

首先当然是实现咱们的构造函数呢,咱们把一个色彩选择器看做是一个结构实例,也因而,咱们创立一个构造函数。

function ewColorPicker(options){// 次要逻辑}

好的,接下来,让咱们实现第一步,校验用户传入的参数,咱们分为 2 种状况,第一种是如果用户传入的是一个 DOM 元素字符串或者是一个 DOM 元素,那么咱们就要定义一个默认的配置对象,如果用户传入的是一个自定义的对象,那么咱们将不采取默认对象。在校验之前,咱们先思考一下可能须要解决的谬误状况,也就是说如果用户传入的参数不合乎规定,咱们是不是须要返回一些谬误提醒给用户晓得,当初让咱们来定义一下这些谬误规定吧。如下所示:

const NOT_DOM_ELEMENTS = ['html','head','meta','title','link','style','script','body'];
const ERROR_VARIABLE = {
    DOM_OBJECT_ERROR:'can not find the element by el property,make sure to pass a correct value!',
    DOM_ERROR:'can not find the element,make sure to pass a correct param!',
    CONFIG_SIZE_ERROR:'the value must be a string which is one of the normal,medium,small,mini,or must be an object and need to contain width or height property!',
    DOM_NOT_ERROR:'Do not pass these elements:' + NOT_DOM_ELEMENTS.join(',') + 'as a param,pass the correct element such as div!',
    PREDEFINE_COLOR_ERROR:'"predefineColor" is a array that is need to contain color value!',
    CONSTRUCTOR_ERROR:'ewColorPicker is a constructor and should be called with the new keyword!',
    DEFAULT_COLOR_ERROR:'the"defaultColor"is not an invalid color,make sure to use the correct color!'
};

这些校验谬误都是常量,不容许被批改的,所以咱们用大写字母来示意。接下来咱们就须要在构造函数里做一个校验了。

配置属性的定义与校验

1. 校验是否是实例化

判断 new.target 就能够了,如下所示:

if(util.isUndefined(new.target))return ewError(ERROR_VARIABLE.CONSTRUCTOR_ERROR);

2. 定义一个函数 startInit,在这个函数里对具体的属性做判断。如下所示:

function startInit(context,options){let initOptions = initConfig(config);
    if(!initOptions)return;
    // 缓存配置对象属性
    context.config = initOptions.config;
    // 定义公有属性
    context._private = {
        boxSize: {
            b_width: null,
            b_height: null
        },
        pickerFlag: false,
        colorValue: "",
    };
    // 在初始化之前所作的操作
    context.beforeInit(initOptions.element,initOptions.config,initOptions.error);
}

接下来,咱们来看 initConfig 函数,如下所示:

export function initConfig(config){
    // 默认的配置对象属性 
    const defaultConfig = {...colorPickerConfig};
    let element,error,mergeConfig = null;
    // 如果第二个参数传的是字符串,或 DOM 对象,则初始化默认的配置
    if (util.isString(config) || util.isDom(config) || util.isJQDom(config)) {
        mergeConfig = defaultConfig;
        element = util.isJQDom(config) ? config.get(0) : config;
        error = ERROR_VARIABLE.DOM_ERROR;
    } // 如果是对象,则自定义配置,自定义配置选项如下:
    else if (util.isDeepObject(config) && (util.isString(config.el) || util.isDom(config.el) || util.isJQDom(config.el))) {mergeConfig = util.ewAssign(defaultConfig, config);
        element = util.isJQDom(config.el) ? config.el.get(0) : config.el;
        error = ERROR_VARIABLE.DOM_OBJECT_ERROR;
    } else {if(util.isDeepObject(config)){error = ERROR_VARIABLE.DOM_OBJECT_ERROR;}else{error = ERROR_VARIABLE.DOM_ERROR;}
    }
    return {
        element,
        config:mergeConfig,
        error
    }
}

而后咱们来看看默认的配置对象属性:

export const emptyFun = function () {};
const baseDefaultConfig = {
    alpha: false,
    size: "normal",
    predefineColor: [],
    disabled: false,
    defaultColor: "",
    pickerAnimation: "height",
    pickerAnimationTime:200,
    sure: emptyFun,
    clear: emptyFun,
    togglePicker: emptyFun,
    changeColor: emptyFun,
    isClickOutside: true,
}

接下来,咱们来看 beforeInit 函数,如下所示:

function beforeInit(element, config, errorText) {let ele = util.isDom(element) ? element : util.isString(element) ? util.$(element) : util.isJQDom(element) ? element.get(0) : null;
    if (!ele) return util.ewError(errorText);
    ele = ele.length ? ele[0] : ele;
    if (!ele.tagName) return util.ewError(errorText);
    if (!isNotDom(ele)) {if(!this._color_picker_uid){this._color_picker_uid = util.createUUID();
        }
        this.init(ele, config);
    }
}

其中,isNotDom 办法,咱们先定义好:

const isNotDom = ele => {if (NOT_DOM_ELEMENTS.indexOf(ele.tagName.toLowerCase()) > -1) {util.ewError(ERROR_VARIABLE.DOM_NOT_ERROR);
        return true;
    }
    return false;
}

最初,咱们来看 init 函数,如下所示:

function init(element, config) {
    let b_width, b_height;
    // 自定义色彩选择器的类型
    if (util.isString(config.size)) {switch (config.size) {
            case 'normal':
                b_width = b_height = '40px';
                break;
            case 'medium':
                b_width = b_height = '36px';
                break;
            case 'small':
                b_width = b_height = '32px';
                break;
            case 'mini':
                b_width = b_height = '28px';
                break;
            default:
                b_width = b_height = '40px';
                break;
        }
    } else if (util.isDeepObject(config.size)) {b_width = config.size.width && (util.isNumber(config.size.width) || util.isString(config.size.width)) ? (parseInt(config.size.width) <= 25 ? 25 :  parseInt(config.size.width))+ 'px' : '40px';
        b_height = config.size.height && (util.isNumber(config.size.height) || util.isString(config.size.height)) ? (parseInt(config.size.height) <= 25 ? 25 : parseInt(config.size.height)) + 'px' : '40px';
    } else {return util.ewError(ERROR_VARIABLE.CONFIG_SIZE_ERROR);
    }
    this._private.boxSize.b_width = b_width;
    this._private.boxSize.b_height = b_height;
    // 渲染选择器
    this.render(element, config);
}

如此一来,咱们的初始化的工作才算是实现,回顾一下,咱们在初始化的时候做了哪些操作。我总结如下:

  • 定义了一些谬误的常量,用于提醒。
  • 验证用户传入的参数,分为 2 种状况,第一种是字符串或者 DOM 元素,第二种是自定义对象,其中必须指定 el 属性为一个 DOM 元素。
  • 定义了默认配置对象,定义了一些公有变量。
  • 对色块盒子的大小做了一次规范化。

接下来,就是咱们理论渲染一个色彩选择器的渲染函数,即 render 函数。

render 函数

render 函数的外围思路十分的简略,实际上就是创立一堆元素,而后增加到元素当中去。只不过咱们须要留神几点,例如预约义色彩数组,默认色彩值,以及色块盒子的大小,还有就是 alpha 柱的显隐。如下所示:

ewColorPicker.prototype.render = function(element,config){
    let predefineColorHTML = '',
        alphaBar = '',
        hueBar = '',
        predefineHTML = '',
        boxDisabledClassName = '',
        boxBackground = '',
        boxHTML = '',
        clearHTML = '',
        sureHTML = '',
        inputHTML = '',
        btnGroupHTML = '',
        dropHTML = '',
        openChangeColorModeHTML = '',
        openChangeColorModeLabelHTML = '',
        horizontalSliderHTML = '',
        verticalSliderHTML = '';
    const p_c = config.predefineColor;
    if (!util.isDeepArray(p_c)) return util.ewError(ERROR_VARIABLE.PREDEFINE_COLOR_ERROR);
    if (p_c.length) {p_c.map((color,index) => {let isValidColorString = util.isString(color) && isValidColor(color);
            let isValidColorObj = util.isDeepObject(color) && color.hasOwnProperty('color') && isValidColor(color.color);
            let renderColor = isValidColorString ? color : isValidColorObj ? color.color : '';
            let renderDisabled = isValidColorObj ? setPredefineDisabled(color.disabled) : '';
            predefineColorHTML += `
            <div class="ew-pre-define-color${hasAlpha(renderColor)}${renderDisabled}" tabindex=${index}>
                <div class="ew-pre-define-color-item" style="background-color:${renderColor};"></div>
            </div>`;
        })
    };
    // 关上色彩选择器的方框
    const colorBox = config.defaultColor ? `<div class="ew-color-picker-arrow" style="width:${this._private.boxSize.b_width};height:${this._private.boxSize.b_height};">
        <div class="ew-color-picker-arrow-left"></div>
        <div class="ew-color-picker-arrow-right"></div>
    </div>` : `<div class="ew-color-picker-no" style="width:${this._private.boxSize.b_width};height:${this._private.boxSize.b_height};line-height:${this._private.boxSize.b_height};">&times;</div>`;
    // 透明度
    if (config.alpha) {
        alphaBar = `<div class="ew-alpha-slider-bar">
            <div class="ew-alpha-slider-wrapper"></div>
            <div class="ew-alpha-slider-bg"></div>
            <div class="ew-alpha-slider-thumb"></div>
        </div>`;
    }
    // hue
    if (config.hue) {hueBar = `<div class="ew-color-slider-bar"><div class="ew-color-slider-thumb"></div></div>`;}
    if (predefineColorHTML) {predefineHTML = `<div class="ew-pre-define-color-container">${predefineColorHTML}</div>`;
    }
    if (config.disabled || config.boxDisabled) boxDisabledClassName = 'ew-color-picker-box-disabled';
    if (config.defaultColor){if(!isValidColor(config.defaultColor)){return util.ewError(ERROR_VARIABLE.DEFAULT_COLOR_ERROR)
        }else{config.defaultColor = colorToRgba(config.defaultColor);
        }
    };
    this._private.color = config.defaultColor;
    if (!config.disabled && this._private.color) boxBackground = `background:${this._private.color}`;
    // 盒子款式
    const boxStyle = `width:${this._private.boxSize.b_width};height:${this._private.boxSize.b_height};${boxBackground}`;
    if (config.hasBox) {boxHTML = `<div class="ew-color-picker-box ${boxDisabledClassName}" tabIndex="0" style="${boxStyle}">${colorBox}</div>`;
    }
    if (config.hasClear) {clearHTML = `<button class="ew-color-clear ew-color-drop-btn">${ config.clearText}</button>`;
    }
    if (config.hasSure) {sureHTML = `<button class="ew-color-sure ew-color-drop-btn">${ config.sureText}</button>`;
    }
    if (config.hasClear || config.hasSure) {btnGroupHTML = `<div class="ew-color-drop-btn-group">${clearHTML}${sureHTML}</div>`;
    }
    if (config.hasColorInput) {inputHTML = '<input type="text"class="ew-color-input">';}
    if (config.openChangeColorMode) {if (!config.alpha || !config.hue) return util.ewError(ERROR_VARIABLE.COLOR_MODE_ERROR);
        openChangeColorModeHTML = `<div class="ew-color-mode-container">
        <div class="ew-color-mode-up"></div>
        <div class="ew-color-mode-down"></div>
        </div>`;
        openChangeColorModeLabelHTML = `<label class="ew-color-mode-title">${this.colorMode[1]}</label>`;
    }
    if (config.hasColorInput || config.hasClear || config.hasSure) {
        dropHTML = config.openChangeColorMode ? `<div class="ew-color-drop-container ew-has-mode-container">
        ${openChangeColorModeLabelHTML}${inputHTML}${openChangeColorModeHTML}
        </div><div class="ew-color-drop-container">
        ${btnGroupHTML}
        </div>` : `<div class="ew-color-drop-container">
        ${inputHTML}${btnGroupHTML}
        </div>`;
    }
    this.isAlphaHorizontal = config.alphaDirection === 'horizontal';
    this.isHueHorizontal = config.hueDirection === 'horizontal';
    if(this.isAlphaHorizontal && this.isHueHorizontal){horizontalSliderHTML = hueBar + alphaBar;}else if(!this.isAlphaHorizontal && !this.isHueHorizontal){verticalSliderHTML = alphaBar + hueBar;}else{if(this.isHueHorizontal){
            horizontalSliderHTML = hueBar;
            verticalSliderHTML = alphaBar;
        } else{
            horizontalSliderHTML = alphaBar;
            verticalSliderHTML = hueBar;
        }
    }
    if(horizontalSliderHTML){horizontalSliderHTML = `<div class="ew-color-slider ew-is-horizontal">${ horizontalSliderHTML}</div>`
    }
    if(verticalSliderHTML){verticalSliderHTML = `<div class="ew-color-slider ew-is-vertical">${ verticalSliderHTML}</div>`;
    }
    // 色彩选择器
    const html = `${boxHTML}
        <div class="ew-color-picker">
            <div class="ew-color-picker-content">
                ${verticalSliderHTML}
                <div class="ew-color-panel" style="background:red;">
                    <div class="ew-color-white-panel"></div>
                    <div class="ew-color-black-panel"></div>
                    <div class="ew-color-cursor"></div>
                </div>
            </div>
            ${horizontalSliderHTML}
            ${dropHTML}
            ${predefineHTML}
        </div>`;
    element.setAttribute("color-picker-id",this._color_picker_uid);
    element.innerHTML = `<div class="ew-color-picker-container">${html}</div>`;
    this.startMain(element, config);
}

startMain 函数

接下来,咱们来看看咱们要实现哪些逻辑。首先咱们须要确定一个初始值的色彩对象,用 hsva 来示意,咱们创立一个 initColor 函数,代码如下所示:

function initColor(context, config) {if (config.defaultColor) {context.hsvaColor = colorRegRGBA.test(config.defaultColor) ? colorRgbaToHsva(config.defaultColor) : colorRgbaToHsva(colorToRgba(config.defaultColor));
    } else {
        context.hsvaColor = {
            h: 0,
            s: 100,
            v: 100,
            a: 1
        };
    }
}

这是咱们要实现的第一个逻辑,也就是初始化色彩值,这个色彩值对象将贯通整个色彩选择器实例,所有的逻辑更改也会围绕它开展。接下来,咱们再外部存储一些 DOM 元素或者一些公有对象属性以及用户传入的配置对象,这样能够不便咱们之后操作。

当初咱们再来剖析一下,咱们能够大抵失去次要的逻辑有:

  • 初始化一些后续须要操作的 DOM 元素与色彩值以及面板的 left 与 top 偏移
  • 预约义色彩逻辑
  • 初始化色彩面板的动画逻辑
  • 色块盒子的解决逻辑
  • 输入框逻辑
  • 禁用逻辑
  • 点击指标区域之外敞开色彩面板的逻辑
  • 清空按钮与确定按钮的逻辑
  • 色彩面板的点击逻辑与色彩面板的元素拖拽逻辑

咱们接下来将围绕这几种逻辑一起开展。如下所示:

    // 初始化逻辑
    let scope = this;
    this.$Dom = Object.create(null);
    this.$Dom.rootElement = ele;
    this.$Dom.picker = getELByClass(ele, 'ew-color-picker');
    this.$Dom.pickerPanel = getELByClass(ele, 'ew-color-panel');
    this.$Dom.pickerCursor = getELByClass(ele, 'ew-color-cursor');
    this.$Dom.verticalSlider = getELByClass(ele, 'ew-is-vertical');
    // 清空按钮逻辑
    this.$Dom.pickerClear = getELByClass(ele, 'ew-color-clear');
    this.$Dom.hueBar = getELByClass(ele, 'ew-color-slider-bar');
    this.$Dom.hueThumb = getELByClass(ele, 'ew-color-slider-thumb');
    this.$Dom.preDefineItem = getELByClass(ele, 'ew-pre-define-color', true);
    this.$Dom.box = getELByClass(ele, 'ew-color-picker-box');
    // 输入框逻辑
    this.$Dom.pickerInput = getELByClass(ele, 'ew-color-input');
    // 确定按钮逻辑
    this.$Dom.pickerSure = getELByClass(ele, 'ew-color-sure');
    initColor(this, config);
    // 初始化面板的 left 偏移和 top 偏移
    const panelWidth = this.panelWidth = parseInt(util.getCss(this.$Dom.pickerPanel, 'width'));
    const panelHeight = this.panelHeight = parseInt(util.getCss(this.$Dom.pickerPanel, 'height'));
    const rect = util.getRect(ele);
    this.panelLeft = rect.left;
    this.panelTop = rect.top + rect.height;

接着咱们开始初始化预约义色彩逻辑:

    // 预约义色彩逻辑
    if (this.$Dom.preDefineItem.length) {initPreDefineHandler(util.ewObjToArray(this.$Dom.preDefineItem), scope);
    }
    function initPreDefineHandler(items, context) {
        // get the siblings
        const siblings = el => Array.prototype.filter.call(el.parentElement.children, child => child !== el);
        items.map(item => {
            const clickHandler = event => {util.addClass(item, 'ew-pre-define-color-active');
                siblings(item).forEach(sibling => util.removeClass(sibling, 'ew-pre-define-color-active'))
                const bgColor = util.getCss(event.target, 'background-color');
                context.hsvaColor = colorRgbaToHsva(bgColor);
                setColorValue(context, context.panelWidth, context.panelHeight, true);
                changeElementColor(context);
            };
            const blurHandler = event => util.removeClass(event.target, 'ew-pre-define-color-active');
            [{type: "click", handler: clickHandler}, {type: "blur", handler: blurHandler}].forEach(t => {if (!context.config.disabled && util.ewObjToArray(item.classList).indexOf('ew-pre-define-color-disabled') === -1) {util.on(item, t.type, t.handler);
                }
            });
        })
    }

而后咱们开始初始化动画逻辑:

  initAnimation(scope);
  function initAnimation(context) {
      // 色彩选择器关上的动画初始设置
      const expression = getAnimationType(context);
      util.setCss(context.$Dom.picker, (expression ? 'display' : 'opacity'), (expression ? 'none' : 0))
      let pickerWidth = 0, sliderWidth = 0, sliderHeight = 0;
      let isVerticalAlpha = !context.isAlphaHorizontal;
      let isVerticalHue = !context.isHueHorizontal;
      let isHue = context.config.hue;
      let isAlpha = context.config.alpha;
      if (isAlpha && isHue && isVerticalAlpha && isVerticalHue) {
          pickerWidth = 320;
          sliderWidth = 28;
      } else if (isVerticalAlpha && isAlpha && (!isVerticalHue || !isHue) || (isVerticalHue && isHue && (!isVerticalAlpha || !isAlpha))) {
          pickerWidth = 300;
          sliderWidth = sliderHeight = 14;
      } else {
          pickerWidth = 280;
          sliderHeight = isAlpha && isHue && !isVerticalHue && !isVerticalAlpha ? 30 : 14;
      }
      util.setCss(context.$Dom.picker, 'min-width', pickerWidth + 'px');
      if (context.$Dom.horizontalSlider) {util.setCss(context.$Dom.horizontalSlider, 'height', sliderHeight + 'px');
      }
      if (context.$Dom.verticalSlider) {util.setCss(context.$Dom.verticalSlider, 'width', sliderWidth + 'px');
      }
  }

接下来,就是咱们的一些性能逻辑了,让咱们一一来实现吧,首先咱们须要的实现的是点击色块关上或者敞开色彩选择器面板。如下所示:

// 色块
    if (!config.disabled){util.on(this.$Dom.box, 'click', () => handlePicker(ele, scope, (flag) => {if (flag && scope.config.isClickOutside) {initColor(this, config);
            setColorValue(scope, scope.panelWidth, scope.panelHeight, false);
            handleClickOutSide(scope, scope.config);
        }
      }));
    }

这里的逻辑也不简单,就是判断是否禁用,而后为盒子元素增加点击事件,在这里外围的性能就是 handlePicker 办法,咱们能够看到传入 3 个参数,第一个参数为以后根容器元素,第二个参数则是以后执行上下文对象,第三个参数则是一个回调函数,用来做一些细节解决。setColorValue办法临时先不作阐明,而 initColor 办法咱们后面曾经讲过,handleClickOutSide办法咱们将在讲完 handlePicker 办法之后再做介绍,当初让咱们先来看一下 handlePicker 这个办法吧。

export function handlePicker(el, scope,callback) {
    scope._private.pickerFlag = !scope._private.pickerFlag;
    openAndClose(scope);
    initColor(scope, scope.config);
    setColorValue(scope, scope.panelWidth, scope.panelHeight, false);
    if (util.isFunction(scope.config.togglePicker)){scope.config.togglePicker(el, scope._private.pickerFlag,scope);
    }
    if(util.isFunction(callback))callback(scope._private.pickerFlag);
}

能够看到,这个办法的外围操作是扭转色彩选择器的状态,最重要的就是 openAndClose 办法呢,让咱们一起来看一下吧,

export function openAndClose(scope) {
    const time = scope.config.pickerAnimationTime;
    scope._private.pickerFlag ? open(getAnimationType(scope), scope.$Dom.picker,time) : close(getAnimationType(scope), scope.$Dom.picker,time);
}
export function getAnimationType(scope) {return scope.config.pickerAnimation;}

这个办法就是获取动画执行工夫,而后依据 pickerFlag 来判断是开启还是敞开色彩选择器,外围的就是 openclose办法,两者都接管 3 个参数,第一个则是动画的类型,第二个则是色彩选择器面板元素,第三个则是动画执行工夫。咱们别离来看一下:

1.open 办法

export function open(expression, picker,time = 200) {
    time = time > 10000 ? 10000 : time;
    let animation = '';
    switch(expression){
        case 'opacity':
            animation = 'fadeIn';
            break;
        default:
            animation = 'slideDown';
    }
    return ani[animation](picker, time);
}

2.close 办法

export function close(expression, picker,time = 200) {
    time = time > 10000 ? 10000 : time;
    let animation = '';
    switch(expression){
        case 'opacity':
            animation = 'fadeOut';
            break;
        default:
            animation = 'slideUp';
    }
    return ani[animation](picker, time);
}

能够看到,咱们再 openclose办法外部对工夫做了一次限度解决,而后判断动画类型来决定调用哪种动画来实现色彩选择器的开启和敞开。到这里,咱们还少实现了一个办法,那就是handleClickOutSide,让咱们来一起看一下这个办法的实现:

export function handleClickOutSide(context, config) {util.clickOutSide(context, config, () => {if (context._private.pickerFlag) {
            context._private.pickerFlag = false;
            closePicker(getAnimationType(config.pickerAnimation), context.$Dom.picker,config.pickerAnimationTime);
        }
    });
}

能够看到,咱们次要是对色彩选择器面板如果处于开启状态做的一个操作,也就是点击不蕴含盒子元素区域以外的空间,咱们都要敞开色彩选择器面板。这里设计到如何去实现判断咱们的鼠标点击是在元素的区域之外呢?有 2 种形式来实现,第一种判断咱们点击的 DOM 元素是否是色彩选择器元素以及其子元素节点即可,也就是说咱们只须要判断咱们点击的元素如果是色彩选择器面板容器元素或者是其子元素,咱们都不能敞开色彩选择器,并且当然色彩选择器面板还要处于开启中的状态。另一种就是通过坐标值的计算,判断鼠标点击的坐标区间是否在色彩选择器面板的坐标区域内,这里咱们采纳第二种实现形式,让咱们一起来看一下吧。

util["clickOutSide"] = (context, config, callback) => {const mouseHandler = (event) => {const rect = util.getRect(context.$Dom.picker);
        const boxRect = util.getRect(context.$Dom.box);
        const target = event.target;
        if (!target) return;
        const targetRect = util.getRect(target);
        // 利用 rect 来判断用户点击的中央是否在色彩选择器面板区域之内
        if (targetRect.x >= rect.x && targetRect.y >= rect.y && targetRect.width <= rect.width) return;
        // 如果点击的是盒子元素
        if (targetRect.x >= boxRect.x && targetRect.y >= boxRect.y && targetRect.width <= boxRect.width && targetRect.height <= boxRect.height) return;
        callback();
        setTimeout(() => {util.off(document, util.eventType[0], mouseHandler);
        }, 0);
    }
    util.on(document, util.eventType[0], mouseHandler);
}

能够看到,咱们是通过比拟 x 与 y 坐标的大小从而确定是否点击的区域属于色彩选择器面板区域,从而确定色彩选择器的敞开状态。当然这也是咱们默认会调用的,当然咱们也提供了一个可选项来确定是否能够通过点击元素区域之外的空间敞开色彩选择器面板。如下:

if (config.isClickOutside) {handleClickOutSide(this, config);
}

代码不简单,很容易就了解了。接下来,咱们来看 alpha 透明度的逻辑的实现。如下:

if (!config.disabled) {this.bindEvent(this.$Dom.alphaBarThumb, (scope, el, x, y) => changeAlpha(scope, y));
    util.on(this.$Dom.alphaBar, 'click', event => changeAlpha(scope, event.y));
}

能够看到,咱们这里首先须要判断是否禁用,而后咱们须要 2 种形式给透明度柱子增加事件逻辑,第一种就是拖拽透明度柱子的滑块元素所触发的拖拽事件,第二种则是点击透明度柱子的事件,这其中波及到了一个 changeAlpha 事件。咱们来看一下:

export function changeAlpha(context, position) {let value = setAlphaHuePosition(context.$Dom.alphaBar,context.$Dom.alphaBarThumb,position);
  let currentValue = value.barPosition - value.barThumbPosition <= 0 ? 0 : value.barPosition - value.barThumbPosition; 
  let alpha = context.isAlphaHorizontal ? 1 - currentValue / value.barPosition : currentValue / value.barPosition;
  context.hsvaColor.a = alpha >= 1 ? 1 : alpha.toFixed(2);
  changeElementColor(context, true);
}

这个办法又波及到了 2 个办法 setAlphaHuePositionchangeElementColor。咱们别离来看一下:

function setAlphaHuePosition(bar,thumb,position){
    const positionProp = 'y';
    const barProp = 'top';
    const barPosition = bar.offsetHeight,
          barRect = util.getRect(bar);
    const barThumbPosition = Math.max(0,Math.min(position - barRect[positionProp],barPosition));
        util.setCss(thumb,barProp,barThumbPosition +'px');
        return {
            barPosition,
            barThumbPosition
        }
}

能够看到,这里咱们次要的逻辑操作就是规范化款式解决,也就是说咱们拖动滑块扭转的是垂直方向上的 top 偏移(将来会思考退出程度方向也就是 left 偏移),所以独自抽取进去做一个公共的办法,这个 top 偏移会有一个最大值与最小值的比拟。接下来,咱们来看 changeElementColor 办法的实现:

 export function changeElementColor(scope, isAlpha) {const color = colorHsvaToRgba(scope.hsvaColor);
    let newColor = isAlpha || scope.config.alpha ? color : colorRgbaToHex(color);
    scope.$Dom.pickerInput.value = newColor;
    scope.prevInputValue = newColor;
    changeAlphaBar(scope);
    if (util.isFunction(scope.config.changeColor))scope.config.changeColor(newColor);
}

显然这个办法的外围目标就是解决色彩值的扭转,咱们有 2 个参数,第一个参数则是以后上下文,第二个参数用于判断透明度柱是否开启。先利用 colorHsvaToRgba 办法将以后的色彩值转换成 rgba 色彩,而后判断如果开启了透明度柱,则不须要进行转换,否则就须要转换成 hex 色彩模式,而后咱们把新的色彩值传给 input 元素。并且缓存了一下这个色彩值,而后这里须要留神一下,如果扭转了色彩值,则有可能透明度会扭转,因而,须要再次调用 changeAlphaBar 办法来扭转透明度柱的性能。最初咱们裸露了一个 changeColor 办法接口给用户应用。

后面还提到了一个 bindEvent 办法,咱们接下来来看一下这个 bindEvent 办法的实现。如下:

export function bindEvent(el, callback, bool) {
    const context = this;
    const callResult = event => {context.moveX = util.eventType[0].indexOf('touch') > -1 ? event.changedTouches[0].clientX : event.clientX;
        context.moveY = util.eventType[0].indexOf('touch') > -1 ? event.changedTouches[0].clientY : event.clientY;
        bool ? callback(context, context.moveX, context.moveY) : callback(context, el, context.moveX, context.moveY);
    }
    const handler = () => {const moveFn = e => { e.preventDefault(); callResult(e); }
        const upFn = () => {util.off(document, util.eventType[1], moveFn);
            util.off(document, util.eventType[2], upFn);
        }
        util.on(document, util.eventType[1], moveFn);
        util.on(document, util.eventType[2], upFn);
    }
    util.on(el, util.eventType[0], handler);
}

这个办法的外围就是在 PC 端监听 onmousedown,onmousemove,onmouseup 事件,在挪动端监听 touchstart,touchmove,touchend 事件并将以后上下文,x坐标以及 y 坐标回调进来。

接下来,让咱们持续。咱们来实现 hue 色调柱的逻辑,它的逻辑和透明度柱很类似。

if (!config.disabled) {
    //hue 的点击事件
    util.on(this.$Dom.hueBar, 'click', event => changeHue(scope, event.y))
    //hue 轨道的拖拽事件
    this.bindEvent(this.$Dom.hueBarThumb, (scope, el, x, y) => changeHue(scope, y));
}

能够看到,咱们同样是判断是否禁用,而后给色调柱增加点击事件以及给 hue 滑块增加拖拽事件。这里也就外围实现了一个 changeHue 办法。让咱们来看一下吧。

export function changeHue(context, position) {const { $Dom:{ hueBar,hueThumb,pickerPanel},_private:{hsvaColor}} = context;
    let value = setAlphaHuePosition(hueBar, hueThumb, position);
    const {barThumbPosition,barPosition} = value;
    context.hsvaColor.h = cloneColor(hsvaColor).h = parseInt(360 * barThumbPosition / barPosition);
    util.setCss(pickerPanel, 'background', colorRgbaToHex(colorHsvaToRgba(cloneColor(context.hsvaColor))));
    changeElementColor(context);
}

这个办法,咱们首先同样是获取到一个值,由后面的色彩算法咱们应该晓得,色调的角度限度在 0~360 之间,而后咱们通过 360 * barThumbPosition / barPosition 失去了色调也就是 h 的相干值。而后咱们须要批改色彩面板的背景款式。而后调用 changeElementColor 办法(这个在后面曾经讲过)。后面咱们遗留了一个办法,叫做changeAlphaBar, 让咱们来看一下这个办法做了什么。

export function changeAlphaBar(scope) {if (!scope.$Dom.alphaBarBg) return;
    let position = 'to top';
    util.setCss(scope.$Dom.alphaBarBg, 'background', 'linear-gradient('+ position +',' + colorHsvaToRgba(scope.hsvaColor,0) + '0%,' + colorHsvaToRgba(scope.hsvaColor,1) + '100%)');
}

能够看到,实际上咱们就是对透明度柱的背景色做了一个批改。因为咱们的透明度柱子不肯定存在(因为由用户自定义是否显示),所以这里咱们是须要做一个判断的。

接下来,让咱们持续来实现一下 色彩面板 组件的相干逻辑性能。其实它的逻辑与透明度柱和色调柱一样,都是分为拖拽和点击。如下所示:

// 色彩面板点击事件
util.on(this.$Dom.pickerPanel, 'click', event => onClickPanel(scope, event));
// 色彩面板拖拽元素拖拽事件
this.bindEvent(this.$Dom.pickerCursor, (scope, el, x, y) => {const left = Math.max(0, Math.min(x - scope._private.panelLeft, panelWidth));
    const top = Math.max(0, Math.min(y - scope._private.panelTop, panelHeight));
    changeCursorColor(scope, left + 4, top + 4, panelWidth, panelHeight);
});

咱们先来看点击逻辑,同样的是监听面板的点击事件,而后调用 onClickPanel 办法,咱们来看一下这个办法的实现。

export function onClickPanel(scope, eve) {if (eve.target !== scope.$Dom.pickerCursor) {
        // 临界值解决
        const moveX = eve.layerX;
        const moveY = eve.layerY;
        const {_private:{ panelWidth,panelHeight}} = context;
        const left = moveX >= panelWidth - 1 ? panelWidth : moveX <= 0 ? 0 : moveX;
        const top = moveY >= panelHeight - 2 ? panelHeight : moveY <= 0 ? 0 : moveY;
        changeCursorColor(scope, left + 4, top + 4,panelWidth,panelHeight)
    }
}

能够看到,咱们所做的操作就是获取一个 x 坐标和 y 坐标,而后去设置 拖拽游标 的 left 和 top 偏移,这里会有临界值的解决。略微宽度减 1 和高度减 2 是做一层偏差解决。而后再次调用 changeCursorColor 办法,咱们持续来看这个办法的实现。

export function changeCursorColor(scope, left, top, panelWidth, panelHeight) {util.setSomeCss(scope.$Dom.pickerCursor, [{ prop: 'left', value: left + 'px'}, {prop: 'top', value: top + 'px'}])
    const s = parseInt(100 * (left - 4) / panelWidth);
    const v = parseInt(100 * (panelHeight - (top - 4)) / panelHeight);
    // 须要减去自身的宽高来做判断
    scope.hsvaColor.s = s > 100 ? 100 : s < 0 ? 0 : s;
    scope.hsvaColor.v = v > 100 ? 100 : v < 0 ? 0 : v;
    changeElementColor(scope);
}

能够看到这个办法咱们所做的操作就是设置游标元素的偏移量,以及它的偏移量所代表的的就是 hsva 色彩模式中的 s 和 v,而后咱们再次调用 changeElementColor 办法就能够扭转色彩值了。

让咱们持续看清空按钮的事件逻辑,如下所示:

util.on(this.$Dom.pickerClear, 'click', () => onClearColor(scope));

也就是增加点击事件的监听,而后再事件的回调函数中调用 onClearColor 办法,接下来,咱们看 onClearColor 办法。如下所示:

export function onClearColor(scope) {
    scope._private.pickerFlag = false;
    closePicker(getAnimationType(scope.config.pickerAnimation),scope.$Dom.picker,scope.config.pickerAnimationTime);
    scope.config.defaultColor = scope._private.color = "";
    scope.config.clear(scope.config.defaultColor, scope);
}

能够看到咱们所做的操作比较简单,就是重置色彩选择器开启状态,而后调用敞开色彩选择器办法敞开色彩选择器, 而后重置咱们的色彩,再回调一个 clear 办法接口给用户应用。同样的情理,咱们的确定按钮的逻辑也就是如此了。如下所示:

util.on(this.$Dom.pickerSure, 'click', () => onSureColor(scope));

也就是增加点击事件的监听,而后再事件的回调函数中调用 onSureColor 办法,接下来,咱们看 onSureColor 办法。如下所示:

export function onSureColor(scope) {const result = scope.config.alpha ? colorHsvaToRgba(scope._private.hsvaColor) : colorRgbaToHex(colorHsvaToRgba(scope._private.hsvaColor));
    scope._private.pickerFlag = false;
    closePicker(getAnimationType(scope.config.pickerAnimation),scope.$Dom.picker,scope.config.pickerAnimationTime);
    scope.config.defaultColor = scope._private.color = result;
    changeElementColor(scope);
    scope.config.sure(result, scope);
}

能够看到这个操作的逻辑也比较简单,相似于清空按钮的逻辑,咱们不外乎须要设置色彩值,而后回调一个 sure 办法给用户,这个办法回调两个参数,第一个参数为以后选中的色彩值,第二个参数则是以后上下文对象。另外,咱们还须要调用 changeElementColor 办法来扭转色彩值。

接下来,让咱们持续来实现一下 input 框的相干逻辑性能,这也是咱们的最初一个逻辑。首先咱们须要确定的就是,当 input 框移开焦点的时候,就意味着更改色彩值。所以咱们监听它的移开焦点事件,而后额定封装了一个办法。当然在这之前,咱们先须要监听禁用逻辑,如下所示:

// 禁用逻辑
if (config.disabled) {if (!util.hasClass(this.$Dom.pickerInput, 'ew-input-disabled')) {util.addClass(this.$Dom.pickerInput,'ew-input-disabled');
    }
    if (!util.hasClass(this.$Dom.picker, 'ew-color-picker-disabled')) {util.addClass(this.$Dom.picker,'ew-color-picker-disabled');
    }
    this.$Dom.pickerInput.disabled = true;
    return false;
}

能够看到,以上的逻辑,咱们就是判断用户是否传入了 disabled 属性,而后判断 input 元素是否还有咱们自定义的禁用类名 ew-input-disabled, 如果没有则增加该类名,同样的,咱们为picker 也做雷同的逻辑,最初咱们将 input 元素的 disabled 属性设置为 true。接下来咱们来看blur 事件的实现:

util.on(this.$Dom.pickerInput, 'blur', event => onInputColor(scope, event.target.value));

这段代码很简略,就是增加监听事件,接下来,咱们来看 onInputColor 办法的实现。如下:

 export function onInputColor(scope, value) {if (!isValidColor(value)) return;
    // 两者相等,阐明用户没有更改色彩 
    if (util.removeAllSpace(scope.prevInputValue) === util.removeAllSpace(value))return;
    let color = scope.config.alpha ? colorRgbaToHsva(value) : colorRgbaToHsva(colorHexToRgba(value));
    scope.hsvaColor = color;
    setColorValue(scope, scope.panelWidth, scope.panelHeight,true);
}

这段代码的逻辑也不简单,首先判断输入框的值是否是合格的色彩值或者判断以后值和咱们缓存的值是否雷同,如果不是合格的色彩值或者与缓存的值雷同则不作任何操作。而后咱们再依据是否开启了透明度柱来判断是否须要调用 colorHexToRgba 办法来将色彩值转换成 rgba 色彩,而后再应用 colorRgbaToHsva 办法来将色彩值转换成 hsva 的色彩。而后再赋值。最初再调用 setColorValue 办法来赋值。接下来,咱们就来看 setColorValue 办法的实现。如下:

export function setColorValue(context, panelWidth, panelHeight,boxChange) {changeElementColor(context);
    context._private.prevInputValue = context.$Dom.pickerInput.value;
    let sliderBarHeight = 0;
    let l = parseInt(context.hsvaColor.s * panelWidth / 100),
        t = parseInt(panelHeight - context.hsvaColor.v * panelHeight / 100);
    [
        {
            el: context.$Dom.pickerCursor,
            prop: 'left',
            value: l + 4 + 'px'
        },
        {
            el: context.$Dom.pickerCursor,
            prop: 'top',
            value: t + 4 + 'px'
        },
        {
            el: context.$Dom.pickerPanel,
            prop: 'background',
            value: colorRgbaToHex(colorHsvaToRgba(cloneColor(context.hsvaColor)))
        }
    ].forEach(item => util.setCss(item.el, item.prop, item.value));
    getSliderBarPosition(context.$Dom.hueBar,(position,prop) => {util.setCss(context.$Dom.hueThumb, prop, parseInt(context.hsvaColor.h * position / 360) + 'px');
    });
    if (context.config.alpha) {getSliderBarPosition(context.$Dom.alphaBar,(position,prop) => {util.setCss(context.$Dom.alphaBarThumb, prop, position - context.hsvaColor.a * position + 'px');
        });
    }
}
export function getSliderBarPosition(bar,callback){
    let sliderPosition = bar.offsetHeight;
    let sliderProp = 'top';
    callback(sliderPosition,sliderProp);
}

这个办法的实现略微有点简单,实际上这个办法在后面咱们曾经用到过,只是没有解说。接下来,让咱们来一一剖析这个办法到底做了什么。首先,调用了 changeElementColor 办法赋值,其次缓存以后的输入框的色彩值,而后计算色彩面板游标元素的 left 和 top 偏移量,而后别离设置它们,再而后设置色彩面板的背景色。以及设置色调柱的偏移量。如果透明度柱子存在,则也要设置透明度柱子的偏移量。

到目前为止,咱们所要实现的色彩选择器的基本功能就曾经实现,接下来,咱们来对咱们的文档做一个总结。咱们从剖析每一个色彩选择器的模块开始,对应的构造及款式咱们都是一一剖析了,而后再细化到每一个性能。每一个色彩选择器的模块如下:

  • 色彩色块
  • 色彩面板
  • 色调柱
  • 透明度柱
  • 输入框
  • 清空与确定按钮
  • 预约义色彩元素列表

再而后,咱们对照每一个模块去一一实现它们的性能。在这些性能中,咱们学到了哪些货色呢?

  1. 闭包。(也就是说咱们在某一个作用域中拜访其它作用域中的变量。例如:bindEvent 办法的实现)
  2. 定时器。(如动画函数的实现)
  3. 色彩转换算法。
  4. 正则表达式。
  5. 面向对象的编程。
  6. 如何实现点击指标区域之外的逻辑性能

当然还有很多,细细品味下来,咱们应该晓得远远不止如此,然而咱们的文档的确到此为止了,后续应该还会有扩大。让咱们前面再见,感激大家的观看,祝大家可能学习欢快。

如果感觉本文不够具体,能够查看视频课程,感激反对。当然你也能够查看源码。

退出移动版