The preparatory work
Project directory and file creation
First of all, instead of building the environment for the project, we created it in the simplest way, in the lead-in way, so that we could test it as we wrote it. Create an empty directory named ColorPicker, create a JS file called color-picker.js, then create an index. HTML file and create a style file called color-picker.css. Now you should see your project directory as follows:
ColorPicker │ index. HTML │ color-picker.js │ color-picker. CSSCopy the code
In your index.html, initialize the HTML document structure and import the color-picker.js file as follows:
<! DOCTYPEhtml>
<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>
Copy the code
With that in mind, let’s move on to the next step.
Structure and layout
Module analysis
We analyze the module we want to implement with the following figure, as shown below:
As shown above, we can split a color picker into multiple modules, so we have roughly the following structure:
- The shades of color
- The color panel
- Tonal column
- The transparency of the column
- Input box
- Clear and confirm buttons
- List of predefined color elements
In this way, we can clearly see the modules of the entire color picker. We just need to think about developing the basic module functionality for now, and then start to expand and improve from there. Ok, let’s move on to building the basic structure of the page.
Patch module
Through analysis, we should know that the color block is divided into two cases, the first is when there is a color value, the color block should be a background color of the color value of the left and right arrow. As shown below:
Without the color value, our color block should look like the following:
In this way, we have identified the structural elements of the color block as follows:
<div class="ew-color-picker-box">
<! We don't use any ICONS here, we use CSS to make a drop down arrow look like -->
<div class="ew-color-picker-arrow">
<div class="ew-color-picker-arrow-left">
<div class="ew-color-picker-arrow-right"></div>
<! -- No color value -->
<div class="ew-color-picker-no">×</div>
</div>
</div>
</div>
Copy the code
Here we definitely use a color value to determine which structure to use, and we’ll talk about that later. Let’s make sure that the element structure of the color block is as follows. Of course, the class name can also be customized.
Tips: I added the ew- prefix here to make myself unique. If you use your own custom class names, you’ll need to be careful to change them later when writing styles and manipulating DOM elements.
Also note × It’s an HTML character entity, and all we need to know is that it will eventually display as X, and I won’t go into detail here, but if you want to learn more about HTML character entities, you can go to HTML character entities.
Next, let’s finish styling the color blocks. Let’s finish the outermost box element first. As you can see, the outermost layer will have a custom width and height, then a border, and nothing else, so that we know what CSS code to write. So let’s stick with the style that we wrote. Let’s make a note:
- The border color of the color block box is
#dcdee2
- The font color of the color block box is
# 535353
- Color block boxes have
4px
The rounded corners - The color block box has up and down
4px
The inner spacing of,7px
The left and right inner spacing of - Color block boxes have
14px
Font size - Color block boxes have
1.5
The row height of, note that there are no units
1.5x line height is relative to the font size set by the browser. For example, if the browser font size is 16px, then 1.5x line height is 16px * 1.5 = 24px
Given the above requirements, we should know which CSS properties we want to implement, and have a clear idea in mind.
.ew-color-picker-box {
/* Border color is #dcdee2 */
border: 1px solid #dcdee2;
/* The border has 4px rounded corners */
border-radius: 4px;
/* 4px upper and lower inner spacing,7px left and right inner spacing */
padding: 4px 7px;
}
Copy the code
Now that we’ve finished writing the style for the outermost box element, let’s start writing a style with no color values. 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 and line height later. Because it changes dynamically, but here we can fix a value and change it 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;
}
Copy the code
The next step is to implement a style with a color value, which is a little bit more difficult, but the difficulty is how to implement a drop-down arrow that looks like a drop-down arrow. When we look at the structure of the page, we can see that in fact our down arrow is clearly made up of two elements, that is, one element is a horizontal line rotated 45deg, and the other element is rotated in the opposite direction. And we can see that the two horizontal lines are vertically and horizontally centered. Here, we must quickly think of the elastic box layout, where only two attributes are needed to center the element vertically and horizontally. These are the property-content :center and align-items: Center properties. So, after this analysis, our implementation here is not difficult.
The 2 d coordinate system
3 d 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;
}
Copy the code
With this done, the page structure and style of the color block module are complete, and let’s move on.
The color panel
The color palette 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 that has a little shadow effect and a white background color. One thing to know here is the box model, also known as the box-sizing property, which has two properties: Content-box and border-box. In fact, in actual development, we use the border-box the most. Let’s look at the document box-sizing.
From the documentation, we know what this property means. So the box model of the color palette container element here we need to pay attention to. In this case, it is the standard box model, that is, we just include the width and height of the content separately. Therefore, we conclude as follows:
- 1px solid border #ebeeff
- The box model is the standard box model
- Shadow effect document
- 7px inside margin
- 5 px rounded corners
Tips: I’ll leave you wondering why the standard box model is used.
Now that we’re done analyzing the container elements, we can start writing the structure and style.
<div class="ew-color-picker">
<! -- Of course, the structure inside will be analyzed later.
</div>
Copy the code
.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;
}
Copy the code
Now we determine what are the elements in the container element, the first is a color panel and color palette contains a container element again, we can see that the color panel is like three out of the background color overlay effect, no doubt, bold said, yes, yes, is the superposition of three kinds of background color, so we need a container element, The container element then contains two panel elements. The background color of the container element is superimposed with two panel elements. A white background and a black background will add up to what we want. Let’s take a look at an example:
<div class="panel">
<div class="white-panel"></div>
<div class="black-panel"></div>
</div>
Copy the code
.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);
}
Copy the code
Another point that may be involved here is the gradient color. I won’t go into details here, but you can check the documentation if you are interested.
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>
Copy the code
Based on the previous example, we should be able to write this color palette soon, but we are missing one more element, which is the drag element, or cursor element, within the color palette area.
.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);
}
Copy the code
Ok, now I can answer the question left earlier, why use the standard box model and not the IE standard box model. This is because here we will dynamically calculate the drag distance of cursor elements through JS. If it is the IE standard box model, the size of the border and the size of the spacing will be considered, which undoubtedly increases the difficulty for us to calculate the drag distance. Therefore, for simplicity, we use the standard box model.
Now we’ll add the cursor element, because it is in the dynamic change of the color panel, usually we want to make one of the elements in the parent element to move, then obviously we thought about the child elements using absolute positioning, the parent element to add a other than static positioning the static positioning, usually we use relative positioning, there is no exception. So we add a relative position to the.ew-color-picker-panel: relative; The reason why.
<div class="ew-color-picker-content">
<div class="ew-color-picker-panel">
<! -- Omitted some content, cursor element added -->
<div class="ew-color-picker-panel-cursor"></div>
</div>
</div>
Copy the code
It is important to note that the width and height of the cursor element will affect our calculation, so the width and height of the cursor element will be taken into account in the calculation. We will talk about this later, but for now, 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%/* is equivalent torgba(0.0.0.0.4) * /0 0 2px 3px rgb(0 0 0 / 50%); /* is equivalent to rgba(0,0, 0.5)*/
cursor: default;
}
Copy the code
The cursor element, we look like a small circle, so the width and height we give is not very much, only 4px. Since it is a circle, we all know that we can use a border-radius of 50% to turn an element into a circle. Then we have the shaded area, and that completes our little circle. Of course we don’t have to achieve this effect, but in order to restore the color picker itself and facilitate subsequent calculations, we will use the original style.
Levels column
Next, let’s look at the implementation of a chromatic column, or tonal column. Looking at this diagram, it should be clear that the step column consists of two parts. The first part is the bar, called the bar, and the second part is the drag slider, called the thumb. Then we add a container element to contain the level column and transparent column, so we can determine the structure of the level column as follows:
<! -- Container element -->
<div class="ew-color-slider ew-is-vertical">
<div class="ew-color-slider-bar">
<div class="ew-color-slider-thumb"></div>
</div>
</div>
Copy the code
Then we have to determine the style of the implementation, first of all, the whole levels of column is a vertical layout, so we should know that it is a fixed width, and height equivalent to the color of the panel rectangle, the background color of it through a gradient, is actually red, orange, yellow, green, blue, violet seven kinds of color mixing, is like a rainbow. Each of these colors has different proportions. Secondly, we also need to know that the slider part needs to be dynamically dragged. Here we can imagine that the color column can be laid out horizontally or vertically. For now we will implement the vertical layout (to distinguish the container elements by adding a class name eW-IS-vertical). So the dynamic part of the slider should be top. 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;
}
Copy the code
So far, we have implemented the color level column. Now let’s look at the implementation of the transparency column.
The transparency of the column
Principle of transparency in the column with the levels are similar in column, first of all, we can see the transparency column there will be a transparent background, the background is clearly a picture, then it will have a background color, depends on when and levels column in which tonal, then the same as color order column has a slider, also have vertical and horizontal layout layout, Change the top value. So we get the structure as follows:
<div class="ew-alpha-slider-bar">
<! -- Background image -->
<div class="ew-alpha-slider-wrapper"></div>
<! -- Background color -->
<div class="ew-alpha-slider-bg"></div>
<! -- Slider element -->
<div class="ew-alpha-slider-thumb"></div>
</div>
Copy the code
One thing to note here is that the background color of the background bar changes dynamically, which will be discussed later. For the background bar, we also used a linear gradient. Let’s have 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;
}
Copy the code
Ok, so now that we have our transparency bar, let’s look at the implementation of the input field.
Input fields and buttons
The input box is 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" />
Copy the code
It is in line with the clear and OK button elements, so we wrap them with a container element, which should look like this:
<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">empty</button>
<button type="button" class="ew-color-drop-btn ew-color-sure">determine</button>
</div>
</div>
Copy the code
And then there’s nothing to analyze about styles, it’s just basic styles, and we keep coding. 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.2 s cubic-bezier(0.175.0.885.0.32.1.275);
transition: border-color 0.2 s 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.1 s;
transition: 0.1 s;
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;
}
Copy the code
We’re done with the input fields and buttons, so let’s look at the predefined color elements.
Predefined color
And more simple to achieve predefined color element, element is a container, then contain multiple child elements, may be a little bit difficult is the style of the child elements we divided into four kinds of circumstances, the first is the default style, the second is to ban it click style, in addition, we also add a color difference between transparency, and then finally is selected to style. Without further ado, we can write four child elements to represent styles in each of the four cases. 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>
Copy the code
Next, let’s look at the implementation 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;
}
Copy the code
That’s the end of the style and layout, and the focus is on implementing the color picker.
JavaScript
Utility methods
Start by managing tool methods with an empty object. As follows:
const util = Object.create(null);
Copy the code
Then there is the following method:
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) {
// Delay unbinding
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);
// Use recT to determine whether the user clicks within the color picker panel area
if (config.hasBox) {
if (
targetRect.x >= rect.x &&
targetRect.y >= rect.y &&
targetRect.width <= rect.width
)
return;
// If the box element is clicked
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"];
Copy the code
Encapsulation of animation functions
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 =newTimerManager(); }};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);
};
});
Copy the code
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 | | | 0 0 1.0? . [0-9] {1, 2}) [\ \ s] * [\] {1} $/;
// hsl color
const colorRegHSL =
/ ^ (hH) [Ss] [Ll] [\] ([\ \ s] * (2 [0-9] [0-9] | 360 | 3 [0 to 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 to 5] [0-9] | [01]? [0-9] [0-9]?) [\\s]*,)([\\s]*((100|[0-9][0-9]?) % | 0) [\ \ s] *,) {2} ([\ \ s] * (1 | | | 0 0 1.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 {
returnutil.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); }});returnutil.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 * Conversion formula: 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(The ${Math.ceil(r * 255)}.The ${Math.ceil(g * 255)}.The ${Math.ceil(
b * 255
)}.${a}) `
);
}
/** * rgba to hsla * Conversion formula: 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,
};
}
/* * Arbitrary color values (even CSS color keywords) to RGBA color method * this method is supported by IE9+ browser, based on DOM features * @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);
}
/** * Check whether the color value is qualified *@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)
);
}
Copy the code
Now that we’re done with the tools and methods, it’s time to formally complete our main line of functionality logic.
Definition of a constructor
The first thing to do, of course, is to complete our constructor. We treat a color selector as an instance of a constructor, and therefore, we create a constructor.
function ewColorPicker(options){
// Main logic
}
Copy the code
Ok. Next, let’s finish the first step, check the user of the incoming parameters, we divided into two kinds of circumstances, the first is if the user of the incoming is a DOM element string or a DOM element, so we are going to define a default configuration object, if the user of the incoming is a custom object, then we will not take the default object. Before the validation, let’s think about the possible error cases that need to be handled, that is, if the parameter passed by the user does not conform to the rules, we need to return some error messages to the user to know, now let’s define these error rules. 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! '
};
Copy the code
These validation errors are constant and cannot be modified, so we use uppercase letters. Next we need to do a check in the constructor.
Configure the definition and validation of attributes
1. Check whether it is instantiated
Select new.target as follows:
if(util.isUndefined(new.target))return ewError(ERROR_VARIABLE.CONSTRUCTOR_ERROR);
Copy the code
2. Define a function, startInit, that evaluates specific attributes. As follows:
function startInit(context,options){
let initOptions = initConfig(config);
if(! initOptions)return;
// Cache configures object properties
context.config = initOptions.config;
// Define private attributes
context._private = {
boxSize: {
b_width: null.b_height: null
},
pickerFlag: false.colorValue: ""};// The operation done before initialization
context.beforeInit(initOptions.element,initOptions.config,initOptions.error);
}
Copy the code
Next, let’s look at the initConfig function, as follows:
export function initConfig(config){
// Default configuration object properties
constdefaultConfig = { ... colorPickerConfig };let element,error,mergeConfig = null;
// If the second argument is a string, or a DOM object, the default configuration is initialized
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;
} // If it is an object, you can customize the configuration as follows:
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
}
}
Copy the code
Then let’s 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,}Copy the code
Next, let’s look at the beforeInit function, as follows:
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); }}Copy the code
Where, isNotDom method, we define:
const isNotDom = ele= > {
if (NOT_DOM_ELEMENTS.indexOf(ele.tagName.toLowerCase()) > -1) {
util.ewError(ERROR_VARIABLE.DOM_NOT_ERROR);
return true;
}
return false;
}
Copy the code
Finally, let’s look at the init function, as follows:
function init(element, config) {
let b_width, b_height;
// Define the type of the color picker
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;
// Render selector
this.render(element, config);
}
Copy the code
So we’re done with our initialization, so let’s review what we did when we initialized. I summarize as follows:
- Error constants are defined for the prompt.
- Validates user-passed parameters in two cases, the first is a string or DOM element, and the second is a custom object, where the EL attribute must be specified as a DOM element.
- Default configuration objects are defined and some private variables are defined.
- The size of the color block box was normalized once.
Next, we have the render function that actually renders a color picker, called the render function.
Render function
The core idea of the Render function is pretty simple, essentially creating a bunch of elements and adding them to them. There are just a few things we need to pay attention to, such as the predefined color array, default color values, the size of the color block box, and the invisibility 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>`; })};// Open the color picker box
const colorBox = config.defaultColor ? `<div class="ew-color-picker-arrow" style="width:The ${this._private.boxSize.b_width}; height:The ${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:The ${this._private.boxSize.b_width}; height:The ${this._private.boxSize.b_height}; line-height:The ${this._private.boxSize.b_height};" >× </div>`;
/ / transparency
if (config.alpha) {
alphaBar = `
;
}
`// 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:The ${this._private.color}`;
// Box style
const boxStyle = `width:The ${this._private.boxSize.b_width}; height:The ${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 = `
`;
openChangeColorModeLabelHTML = `<label class="ew-color-mode-title">The ${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>`;
}
// Color picker
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);
}
Copy the code
StartMain function
Next, let’s look at what logic we want to implement. First we need to determine an initial value of the color object, which is represented by HSVA. We create an initColor function 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}; }}Copy the code
This is the first logic we will implement, which is to initialize the color value. This color value object will be used throughout the color picker instance, and all the logical changes will be around it. Next, we’ll internally store some DOM elements or some private object attributes and user-passed configuration objects, so that we can manipulate them later.
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, and the left and top offsets of the panel
- Predefined color logic
- Initialize the animation logic for the color palette
- Color block box processing logic
- Input box logic
- Disable the logic
- Click outside the target area to turn off the color palette logic
- Clear buttons and determine button logic
- Color palette click logic and color palette element drag logic
We are going to focus on these logic together. As follows:
// Initialize the logic
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');
// Clear the button logic
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');
// Input box logic
this.$Dom.pickerInput = getELByClass(ele, 'ew-color-input');
// Determine the button logic
this.$Dom.pickerSure = getELByClass(ele, 'ew-color-sure');
initColor(this, config);
// Initialize the left offset and top offset of the panel
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;
Copy the code
Next we start to initialize the predefined color logic:
// Pre-define 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); }}); })}Copy the code
Then we initialize the animation logic:
initAnimation(scope);
function initAnimation(context) {
// The color picker opens the initial Settings of the animation
const expression = getAnimationType(context);
util.setCss(context.$Dom.picker, (expression ? 'display' : 'opacity'), (expression ? 'none' : 0))
let pickerWidth = 0, sliderWidth = 0, sliderHeight = 0;
letisVerticalAlpha = ! context.isAlphaHorizontal;letisVerticalHue = ! 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'); }}Copy the code
Next, is some of our functional logic, let’s implement it one by one, the first thing we need to achieve is to click on the color block to open or close the color picker panel. As follows:
/ / color piece
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); }})); }Copy the code
The logic is not complicated, is to determine whether to disable, then click events for box elements are added, the function of the core is here handlePicker method, we can see the incoming three parameters, the first parameter to the current root container element, the second parameter is the current execution context object, the third argument is a callback function, For a little bit of detail. SetColorValue, initColor, handleClickOutSide, handlePicker, setColorValue, handlePicker, handlePicker, 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);
}
Copy the code
As you can see, the core operation of this method is to change the state of the color picker, and the most important is the openAndClose method, so let’s take a look at it,
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;
}
Copy the code
This method is to get the animation execution time, and then determine whether to turn the color picker on or off based on the pickerFlag. The core is the open and close methods, both of which take three parameters, the first is the type of the animation, the second is the color picker panel element, and the third is the animation execution time. Let’s take a look at each:
1. The open method
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);
}
Copy the code
2. The close method
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);
}
Copy the code
As you can see, we do a time limit inside the open and close methods, and then determine the animation type to call which animation to turn the color picker on and off. HandleClickOutSide = handleClickOutSide = handleClickOutSide = 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); }}); }Copy the code
As you can see, the only action we’re going to do with the color picker panel if it’s on is to close the color picker panel by clicking on anything other than the area that doesn’t contain the box element. How do we determine if our mouse click is outside the area of the element? There are two ways to do this, the first kind of judgment is the DOM elements we click color picker element and its child nodes can, that is to say, we only need to decide if we click on the element is the color picker panel container element or its elements, we can’t close the color picker, and of course the color picker in an open state of the panel. The other is to judge whether the coordinate range of the mouse click is in the coordinate area of the color selector panel through the calculation of the coordinate value. Here we use the second implementation method, let’s take a look at it together.
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);
// Use recT to determine whether the user clicks within the color picker panel area
if (targetRect.x >= rect.x && targetRect.y >= rect.y && targetRect.width <= rect.width) return;
// If the box element is clicked
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);
}
Copy the code
As you can see, we compare the x and y coordinates to determine whether the clicked area belongs to the color selector panel area, thus determining the closed state of the color selector. Of course this is what we call it by default, but we also provide an option to determine if we can close the color picker panel by clicking on space outside the element area. As follows:
if (config.isClickOutside) {
handleClickOutSide(this, config);
}
Copy the code
The code is not complex and is easy to understand. Next, let’s look at the implementation of alpha transparency logic. As follows:
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));
}
Copy the code
As you can see, we first need to determine whether to disable or not, and then we need to add event logic to the transparency column in two ways. The first is the drag event triggered by dragging the slider element of the transparency column, and the second is the click event of the transparency column, which involves a changeAlpha event. Let’s take a look:
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);
}
Copy the code
This method involves two methods, setAlphaHuePosition and changeElementColor. Let’s take a look at each:
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
}
}
Copy the code
As you can see, here is our main logic operation standardization style of handling, which means we drag the slider to change the vertical top offset (future will consider joining level is left offset), so do a public method extracted separately, the top offset comparison there will be a maximum and minimum values. Next, let’s look at the implementation of the changeElementColor method:
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);
}
Copy the code
Obviously, the core purpose of this method is to handle color changes. We have two parameters, the first parameter is the current context, and the second parameter is to determine whether the transparency bar is on. First we convert the current color value to rGBA color using the colorHsvaToRgba method. Then we decide that if the transparency bar is turned on, we don’t need to convert, otherwise we need to convert to HEX color mode. Then we pass the new color value to the input element. If you change the color value, it is possible that the transparency will change. Therefore, you need to call the changeAlphaBar method again to change the transparency bar function. Finally, we expose a changeColor method interface for users to use.
We also mentioned a bindEvent method, so let’s look at the implementation of this bindEvent method. As follows:
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);
}
Copy the code
Is the core of this method in the PC to monitor the onmousedown, onmousemove, onmouseup events, in the mobile terminal to monitor touchstart, touchmove, touchend events and the current context, coordinates x and y coordinates back to bring up.
Next, let’s move on. Let’s implement the logic of the Hue column, which is similar to the logic of the transparency column.
if(! config.disabled) {// Hue click event
util.on(this.$Dom.hueBar, 'click'.event= > changeHue(scope, event.y))
// Hue track drag event
this.bindEvent(this.$Dom.hueBarThumb, (scope, el, x, y) = > changeHue(scope, y));
}
Copy the code
As you can see, we also determine whether to disable it and add a click event to the hue bar and a drag event to the Hue slider. This is the core implementation of a changeHue method. Let’s take a look.
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);
}
Copy the code
In this method, we also get a value from the previous color algorithm, the color Angle is limited to 0~360, and then we get the color (h) from 360 * barThumbPosition/barPosition. Then we need to modify the background style of the color palette. Then call the changeElementColor method (which was covered earlier). We left behind a method called changeAlphaBar, so let’s look at what this method does.
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%)');
}
Copy the code
As you can see, we actually made a change to the background color of the transparency column. Since our transparency column does not necessarily exist (because it is user-defined to display or not), we need to make a judgment here.
Next, let’s go ahead and implement the logic of the color palette component. In fact, its logic and transparency column and color column, are divided into drag and click. As follows:
// Color palette click event
util.on(this.$Dom.pickerPanel, 'click'.event= > onClickPanel(scope, event));
// Color palette drag element drag 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);
});
Copy the code
Let’s look at the click logic first, again listening for the click event of the panel, and then calling the onClickPanel method. Let’s look at the implementation of this method.
export function onClickPanel(scope, eve) {
if(eve.target ! == scope.$Dom.pickerCursor) {// Critical value processing
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)
}
}
Copy the code
As you can see, all we’re doing is taking an X and y coordinate, and then setting the left and top offsets of the drag cursor, with critical handling. A slight width reduction of 1 and height reduction of 2 is a layer of deviation treatment. We then call the changeCursorColor method again, and we continue to look at the implementation of this method.
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);
// Subtract the width and height to determine
scope.hsvaColor.s = s > 100 ? 100 : s < 0 ? 0 : s;
scope.hsvaColor.v = v > 100 ? 100 : v < 0 ? 0 : v;
changeElementColor(scope);
}
Copy the code
As you can see in this method all we do is set the offset of the cursor element, and its offset represents s and V in the HSVA color mode, and then we call the changeElementColor method again to change the color value.
Let’s continue with the empty button’s event logic, as follows:
util.on(this.$Dom.pickerClear, 'click'.() = > onClearColor(scope));
Copy the code
That is, add a listener for the click event, and then call the onClearColor method in the event callback. Next, let’s look at the onClearColor method. As follows:
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);
}
Copy the code
As you can see, what we did was quite simple: reset the color selector state, then call the Close color selector method to turn off the color selector, then reset our color, and call back a clear method interface for the user to use. The same logic applies to our ok button. As follows:
util.on(this.$Dom.pickerSure, 'click'.() = > onSureColor(scope));
Copy the code
So we add a listener for the click event, and then we call the onSureColor method in the event callback, and then we look at the onSureColor method. As follows:
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);
}
Copy the code
This is similar to the clear button logic. We need to set the color value and then call back a sure method to the user. This method calls back two parameters, the first parameter is the currently selected color value and the second parameter is the current context object. In addition, we need to call the changeElementColor method to change the color value.
Next, let’s go ahead and implement the logic of the input box, which is our last logic. The first thing we need to make sure is that when the input box shifts focus, it changes the color value. So we listen for its off-focus event, and then encapsulate an additional method. To do this, we need to listen for the disable logic, as shown below:
// Disable logic
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;
}
Copy the code
As you can see, the above logic checks if the user passed in the disabled attribute, and then checks if the input element still has our custom ew-input-disabled class name. If not, we add the class name. Similarly, we do the same logic for the picker. Finally, we set the Input element’s disabled property to true. Let’s look at the blur event implementation:
util.on(this.$Dom.pickerInput, 'blur'.event= > onInputColor(scope, event.target.value));
Copy the code
This code is very simple, just add listening events, next, let’s look at the implementation of the onInputColor method. As follows:
export function onInputColor(scope, value) {
if(! isValidColor(value))return;
// If they are equal, the user has not changed the color
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);
}
Copy the code
The logic of this code is not complicated, first determine whether the value of the input box is a qualified color value or whether the current value is the same as our cache value, if it is not a qualified color value or the same as the cache value then do nothing. We then use the colorHexToRgba method to convert the color value to RGBA color and the colorRgbaToHsva method to convert the color value to HSVA color based on whether the transparency bar is turned on. And then you assign. Finally, call the setColorValue method to assign the value. Next, let’s look at the implementation of the setColorValue method. As follows:
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);
}
Copy the code
The implementation of this method is a little more complicated, and in fact we have used this method before, but we have not explained it. Let’s take a look at what this method does. First, the changeElementColor method is called to assign a value, second, the color value of the current input box is cached, then the left and top offsets of the color palette cursor elements are calculated, then they are set separately, and then the background color of the color palette is set. And sets the offset of the color column. If the transparency column exists, the offset of the transparency column is also set.
So far, the basic functions of the color picker we want to implement are complete. Next, let’s make a summary of our document. We started from the analysis of each color selector module, the corresponding structure and style are analyzed one by one, and then refined to each function. The modules for each color picker are as follows:
- The shades of color
- The color panel
- Tonal column
- The transparency of the column
- Input box
- Clear and confirm buttons
- List of predefined color elements
Then, we implement each module’s function one by one. What have we learned from these features?
- Closure. That is, we access variables in one scope from other scopes. Example: implementation of the bindEvent method)
- The timer. (e.g. implementation of animation functions)
- Color conversion algorithm.
- Regular expression.
- Object-oriented programming.
- How to achieve the logical function of clicking outside the target area
There’s a lot more, of course, and we should know there’s a lot more, but our documentation does end there, and there should be more to come. I’ll see you later, thank you for watching, and have a great time.
If you feel this article is not detailed enough, you can check out the video course. Thank you for your support. Of course you can also view the source code.