头图

Implement a color picker from scratch (implemented in native JavaScript)

夕水
中文

Ready to work

Project directory and file creation

First of all, we don't need to build the project environment, we still use the simplest way, that is, the introduction method to create this project, which makes it easier for us to write and test. Create an empty directory, name it ColorPicker , create a js file, which is color-picker.js , then create a index.html file and create a style file color-picker.css . Now you should see that your project directory is as follows:

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

In your index.html , initialize the html document structure, and then import the color-picker.js file, as shown below:

<!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>

After making these preparations, let us move on to the next step.

Structure and layout

Module analysis

We use the following figure to analyze the module we want to implement, as shown in the following figure:

As shown in the figure above, we can split a color selector into multiple modules, so we roughly get a structure as follows:

  • Color block
  • Color panel
  • Hue column
  • Transparency column
  • Input box
  • Clear and OK buttons
  • List of predefined color elements

In this way, we can clearly see which modules the entire color picker has. We currently only need to consider the development of basic module functions, and then start to expand and improve on the basis of follow-up. Okay, let's move on to the next step and build the basic structure of the page.

Color block module

Through analysis, we should know that the color block is divided into two situations. The first is that when there is a color value, the color block should be a left and right arrow with the background color of the color value. As shown in the figure below:

Without color value, our color block should be as shown in the following figure:

In this way, we have determined the structural elements of the color block as follows:

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

Here we must use a color value to determine which structure to use, we will talk about this later. Let's first determine that the element structure of the color block should be as follows. Of course, the class name here can also be customized by yourself.

tips: I am here to have my own characteristics, so I added the prefix ew- If you use your own custom class name, you need to pay attention to it when you write styles and manipulate DOM elements later, and you need to change it.

Also note that &times; is a HTML character entity. We only need to know that it will eventually be displayed as X . I will not go into details here. For more knowledge of HTML character entities, you can go to HTML character entity
Check.

Next, let us complete the style of the color block. We first complete the outermost box element. As you can see, the outermost layer will have a custom width and height, then a border, and nothing else. In this way, we know what kind of CSS code to write. Here we still adopt the style written by ourselves. Let's make a record:

  • The border color of the color block box is #dcdee2
  • The font color of the color block box is #535353
  • The color block box has rounded corners of 4px
  • Patch box has vertical 4px the pitch, 7px -spacing of about
  • The color block box has a font size of 14px
  • The color block box has 1.5 , note that there is no unit
tips: 1.5 times the line height is a relative value, it is determined according to the font size set by the browser, for example, the browser font size is 16px, then 1.5 times the line height is 16px * 1.5 = 24px line height

Seeing the above requirements, we should know which CSS attribute we are going to use to achieve, and we must have a clear understanding in our mind.

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

We have finished writing the style of the outermost box element. Next, we will start writing a style when there is no color value. In fact, it is similar to the outermost color block box. The only thing to note is that we will use js to set its width, height and row height. Because it changes dynamically, but here we can fix a value first, and then make changes later.

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

The next step is to implement a style with color values. This is a bit difficult. The difficulty lies in how we implement a down arrow like a drop-down arrow. By analyzing the structural elements of the page, it is not difficult to see that, in fact, the down arrow here is obviously made up of two elements. That is to say, an element is just a horizontal line rotated by 45deg. The same reason, another An element is nothing more than the opposite direction of rotation. And we can see that the two horizontal lines are centered vertically and horizontally. Here, we must have quickly thought of the flexible box layout. Only two attributes are needed to center the element vertically and horizontally. That is, the two attributes justify-content:center and align-items:center Therefore, after such an analysis, our implementation here is not difficult.

2D coordinate system

3D coordinate system

As follows:

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

In this way, the page structure and style of the color block module have been completed by us, let us continue.

Color panel

The color panel is also the most difficult part of the entire color picker. Now let's analyze the structure. First, we can see that it has a container element, this container element has a shadow effect, and the background color is white. One knowledge point to know here is the box model, which is the box-sizing attribute, which has 2 attribute values: content-box,border-box . In fact, in actual development, we use border-box most. Let's look at the document box-sizing .

Through the document description, we know the meaning of this attribute. Then we need to pay attention to the box model of the color panel container element. Here, it is the standard box model, that is, we only need to include the width and height of the content separately. Therefore, we summarize as follows:

  • 1px solid border #ebeeff
  • The box model is the standard box model
  • Shadow effect document
  • 7px inner margin
  • 5px rounded corners
tips: Here is a suspense, why use the standard box model.

So far, our container element analysis is complete, and then we start to write structure and style.

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

Now let's determine which elements are in the container element. First is a color panel, and the color panel contains a container element. We can see that the color panel is very similar to the effect of three background colors superimposed, no doubt, bold Say, yes, it is the three background colors superimposed, so we need a container element, and then the container element contains 2 panel elements, the background color of the container element plus 2 panel elements superimposed is this Kind of effect. A white background and a black one can be superimposed to see the effect we want.
For example, let's take a look at an example:

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

There may be another point of knowledge involved here, that is, gradient colors. I won't go into details here. If you are interested, please document .

So our structure should be as follows:

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

According to the previous example, we will soon be able to write this color panel, but we still have one less, which is the drag element within the color panel area, or we can call it a cursor element.

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

Okay, now I can answer the question left before, why use the standard box model instead of the IE standard box model. This is because here we will dynamically calculate the drag distance of the cursor element through js. If it is the IE standard box model, the size of the border and the size of the spacing will be considered. This undoubtedly increases the difficulty for us to calculate the drag distance, so in order to For simplicity, we use the standard box model.

Now let’s add this cursor element, because it changes dynamically in the color panel. Usually we want to move an element in the parent element. Then we obviously think of the absolute positioning of the child element and the parent element. For a positioning static , we usually use relative positioning, and this is no exception. This is what we give .ew-color-picker-panel add a relative positioning position: relative; reason.

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

It should be noted here that the width and height set by the cursor element will affect our subsequent calculations, so what is the width and height set here, the subsequent calculations must take its width and height into account. This will be discussed in detail later, now, we Let's write the style of the element.

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

The cursor element, we look like a small circle, so the width and height we give is not a lot, only 4px. Since it is a circle, we all know that we can use border-radius to 50% to turn an element into a circle. Next is the shadow part, so that our small circle is realized. Of course, we don't have to achieve such an effect, but in order to restore the color selector itself and facilitate subsequent calculations, we still use the original style.

Color scale column

Next, let's take a look at the realization of the color scale column, which is the hue column. Seeing this picture, we should be able to clearly distinguish the excellent step column contains 2 parts, the first part is the column part, called bar, and the second part is the dragging slider part, called thumb . Then we add a container element to contain the color scale column and the transparent column, so we can determine the structure of the color scale column as follows:

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

Then we determine the realization of the style. First, the entire color scale column is laid out vertically, so we should know that it has a fixed width, and then the height is equivalent to the color panel rectangle, and its background color is achieved by a gradient color , Is actually a mixture of seven colors of red, orange, yellow, green, blue, blue and purple, which is similar to a rainbow. Each of these colors has a different proportion. Secondly, we also need to know that the slider part needs to be dynamically dragged. Here we can imagine that the color scale column can be arranged horizontally or vertically. At present, we first implement the vertical layout (in order to distinguish the container element, add a class name ew-is-vertical). So the dynamically changing part of the slider should be the top value. Now let's look at the style:

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

So far, even if our color scale column is realized, let's look at the realization of transparency column.

Transparency column

The implementation principle of the transparency column is very similar to the color scale column. First of all, we can see that the transparency column will have a transparent background. This background is obviously a picture. Secondly, it will also have a background color bar, depending on the current color scale. In which hue the column is in, there is also a slider like the color scale column, and also has a vertical layout and a horizontal layout. Change the top value. So we get the structure as follows:

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

Here, one thing we need to pay attention to is that the background color of the background color bar changes dynamically, which will be discussed later. For the background color bar, we also achieve it through linear gradients. Let's take a look at the style:

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

Okay, so far, our transparency column has also been realized. Next, let's look at the realization of the input box.

Input box and button

The input box is relatively simple, I think there is nothing to say, this input box can also be customized, its structure is nothing more than the following:

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

It is lined up with the clear and OK button elements, so we use a container element to wrap them, the structure should be as follows:

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

Then there is nothing to analyze the styles, they are all basic styles, we continue to write the code. as follows:

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

We have completed the input box and button. Next, let's look at the predefined color elements.

Predefined colors

The pre-defined color element is also relatively simple to implement. It is a container element and then contains multiple sub-elements. What may be a little bit more difficult is that the styles of the sub-elements are divided into four cases. The first is the default style, and the second is In addition to the styles that are forbidden to click, we also added a difference between the color transparency, and finally the selected style. Not to mention, we can first write 4 sub-elements to represent the styles of the four situations. as follows:

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

Next, let's look at the realization of the style:

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

This is the end of the style and layout. The next thing is our focus, which is to implement the function of the color selector.

JavaScript

Tool method

First use an empty object to manage tool methods. as follows:

const util = Object.create(null);

Then there are the following methods:

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"];

Encapsulation of animation function

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

Some color manipulation algorithms

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

We have completed the tools and methods, and the next step is to formally complete our main line function logic.

The definition of the constructor

First of all, of course, to complete our constructor. We regard a color selector as a construction instance, and therefore, we create a constructor.

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

Okay, next, let us complete the first step to verify the parameters passed in by the user. We are divided into two cases. The first is if the user passed in a DOM element string or a DOM element, then We have to define a default configuration object. If the user passes in a custom object, then we will not adopt the default object. Before verification, let us first think about the error conditions that may need to be handled, that is, if the parameters passed in by the user do not conform to the rules, do we need to return some error prompts to the user to know, now let us define these error rules Bar. As follows:

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

These check errors are constants and cannot be modified, so we use capital letters to represent them. Next we need to do a check in the constructor.

Definition and verification of configuration attributes

1. Verify whether it is an instantiation

Just judge new.target , as shown below:

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

2. Define a function startInit, in which to judge specific attributes. As follows:

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

Next, let's look at the initConfig function, as shown below:

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

Then let's take a look at the default configuration object properties:

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

Next, we look at the beforeInit function, as shown below:

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

Among them, the isNotDom method, we first define:

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

Finally, let's look at the init function, as shown below:

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

In this way, our initialization work is complete. Let’s review what we did during initialization. I summarize as follows:

  • Some error constants are defined for prompting.
  • Validation of the parameters passed in by the user is divided into two cases. The first is a string or a DOM element, and the second is a custom object, in which the el attribute must be specified as a DOM element.
  • The default configuration object is defined, and some private variables are defined.
  • The size of the color block box was standardized once.

Next, we actually render a rendering function of a color picker, the render function.

render function

The core idea of the render function is very simple. In fact, it is to create a bunch of elements and add them to the elements. But we need to pay attention to a few points, such as the predefined color array, the default color value, and the size of the color block box, as well as the display and hiding of the alpha column. As follows:

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 function

Next, let's take a look at what logic we want to implement. First of all, we need to determine an initial value of the color object, represented by hsva, we create an initColor function, the code is as follows:

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

This is the first logic we want to implement, that is, to initialize the color value. This color value object will run through the entire color picker instance, and all logic changes will be around it. Next, we then internally store some DOM elements or some private object attributes and configuration objects passed in by the user, which can facilitate our subsequent operations.

Now let's analyze it again, we can roughly get the main logic as follows:

  • Initialize some DOM elements and color values that need to be manipulated later, as well as the left and top offsets of the panel
  • Predefined color logic
  • Initialize the animation logic of the color panel
  • Color block box processing logic
  • Input box logic
  • Disable logic
  • Click outside the target area to close the logic of the color panel
  • The logic of the clear button and the OK button
  • The click logic of the color panel and the element drag logic of the color panel

We will expand on these kinds of logic next. As follows:

    // 初始化逻辑
    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;

Then we start to initialize the predefined color logic:

    // 预定义颜色逻辑
    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);
                }
            });
        })
    }

Then we start to initialize the animation logic:

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

Next, there are some of our functional logic. Let's implement t

阅读 1.4k

eveningwater
每天学习一点点,就可以进步一点点,工作能带来的不仅是技术知识点,还有与人的相处,沟通与交流。这是...

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性...

4k 声望
5.5k 粉丝
0 条评论

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性...

4k 声望
5.5k 粉丝
文章目录
宣传栏