从零开始实现一个颜色选择器(原生JavaScript实现)

JavaScript

==========

工具方法


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

const util = Object.create(null);

然后有如下方法:

const util = Object.create(null);

const _toString = Object.prototype.toString;

let addMethod = (instance, method, func) => {

instance.prototype[method] = func;

return instance;

};

[“Number”, “String”, “Function”, “Undefined”, “Boolean”].forEach(

(type) => (util[“is” + type] = (value) => typeof value === type.toLowerCase())

);

util.addMethod = addMethod;

[“Object”, “Array”, “RegExp”].forEach(

(type) =>

(util[“isDeep” + type] = (value) =>

_toString.call(value).slice(8, -1).toLowerCase() === type.toLowerCase())

);

util.isShallowObject = (value) =>

typeof value === “object” && !util.isNull(value);

util[“ewObjToArray”] = (value) =>

util.isShallowObject(value) ? Array.prototype.slice.call(value) : value;

util.isNull = (value) => value === null;

util.ewAssign = function (target) {

if (util.isNull(target)) return;

const _ = Object(target);

for (let j = 1, len = arguments.length; j < len; j += 1) {

const source = arguments[j];

if (source) {

for (let key in source) {

if (Object.prototype.hasOwnProperty.call(source, key)) {

_[key] = source[key];

}

}

}

}

return _;

};

util.addClass = (el, className) => el.classList.add(className);

util.removeClass = (el, className) => el.classList.remove(className);

util.hasClass = (el, className) => {

let _hasClass = (value) =>

new RegExp(" " + el.className + " “).test(” " + value + " ");

if (util.isDeepArray(className)) {

return className.some((name) => _hasClass(name));

} else {

return _hasClass(className);

}

};

util[“setCss”] = (el, prop, value) => el.style.setProperty(prop, value);

util.setSomeCss = (el, propValue = []) => {

if (propValue.length) {

propValue.forEach(§ => util.setCss(el, p.prop, p.value));

}

};

util.isDom = (el) =>

util.isShallowObject(HTMLElement)

? el instanceof HTMLElement

: (el &&

util.isShallowObject(el) &&

el.nodeType === 1 &&

util.isString(el.nodeName)) ||

el instanceof HTMLCollection ||

el instanceof NodeList;

util.ewError = (value) =>

console.error(“[ewColorPicker warn]\n” + new Error(value));

util.ewWarn = (value) => console.warn(“[ewColorPicker warn]\n” + value);

util.deepCloneObjByJSON = (obj) => JSON.parse(JSON.stringify(obj));

util.deepCloneObjByRecursion = function f(obj) {

if (!util.isShallowObject(obj)) return;

let cloneObj = util.isDeepArray(obj) ? [] : {};

for (let k in obj) {

cloneObj[k] = util.isShallowObject(obj[k]) ? f(obj[k]) : obj[k];

}

return cloneObj;

};

util.getCss = (el, prop) => window.getComputedStyle(el, null)[prop];

util.$ = (ident) => {

if (!ident) return null;

return document[

ident.indexOf(“#”) > -1 ? “querySelector” : “querySelectorAll”

](ident);

};

util[“on”] = (element, type, handler, useCapture = false) => {

if (element && type && handler) {

element.addEventListener(type, handler, useCapture);

}

};

util[“off”] = (element, type, handler, useCapture = false) => {

if (element && type && handler) {

element.removeEventListener(type, handler, useCapture);

}

};

util[“getRect”] = (el) => el.getBoundingClientRect();

util[“baseClickOutSide”] = (element, isUnbind = true, callback) => {

const mouseHandler = (event) => {

const rect = util.getRect(element);

const target = event.target;

if (!target) return;

const targetRect = util.getRect(target);

if (

targetRect.x >= rect.x &&

targetRect.y >= rect.y &&

targetRect.width <= rect.width &&

targetRect.height <= rect.height

)

return;

if (util.isFunction(callback)) callback();

if (isUnbind) {

// 延迟解除绑定

setTimeout(() => {

util.off(document, util.eventType[0], mouseHandler);

}, 0);

}

};

util.on(document, util.eventType[0], mouseHandler);

};

util[“clickOutSide”] = (context, config, callback) => {

const mouseHandler = (event) => {

const rect = util.getRect(context.$Dom.picker);

let boxRect = null;

if (config.hasBox) {

boxRect = util.getRect(context.$Dom.box);

}

const target = event.target;

if (!target) return;

const targetRect = util.getRect(target);

// 利用rect来判断用户点击的地方是否在颜色选择器面板区域之内

if (config.hasBox) {

if (

targetRect.x >= rect.x &&

targetRect.y >= rect.y &&

targetRect.width <= rect.width

)

return;

// 如果点击的是盒子元素

if (

targetRect.x >= boxRect.x &&

targetRect.y >= boxRect.y &&

targetRect.width <= boxRect.width &&

targetRect.height <= boxRect.height

)

return;

callback();

} else {

if (

targetRect.x >= rect.x &&

targetRect.y >= rect.y &&

targetRect.width <= rect.width &&

targetRect.height <= rect.height

)

return;

callback();

}

setTimeout(() => {

util.off(document, util.eventType[0], mouseHandler);

}, 0);

};

util.on(document, util.eventType[0], mouseHandler);

};

util[“createUUID”] = () =>

(Math.random() * 10000000).toString(16).substr(0, 4) +

“-” +

new Date().getTime() +

“-” +

Math.random().toString().substr(2, 5);

util.removeAllSpace = (value) => value.replace(/\s+/g, “”);

util.isJQDom = (dom) =>

typeof window.jQuery !== “undefined” && dom instanceof jQuery;

//the event

util.eventType = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)

? [“touchstart”, “touchmove”, “touchend”]

: [“mousedown”, “mousemove”, “mouseup”];

动画函数的封装


const animation = {};

function TimerManager() {

this.timers = [];

this.args = [];

this.isTimerRun = false;

}

TimerManager.makeTimerManage = function (element) {

const elementTimerManage = element.TimerManage;

if (!elementTimerManage || elementTimerManage.constructor !== TimerManager) {

element.TimerManage = new TimerManager();

}

};

const methods = [

{

method: “add”,

func: function (timer, args) {

this.timers.push(timer);

this.args.push(args);

this.timerRun();

},

},

{

method: “timerRun”,

func: function () {

if (!this.isTimerRun) {

let timer = this.timers.shift(),

args = this.args.shift();

if (timer && args) {

this.isTimerRun = true;

timer(args[0], args[1]);

}

}

},

},

{

method: “next”,

func: function () {

this.isTimerRun = false;

this.timerRun();

},

},

];

methods.forEach((method) =>

util.addMethod(TimerManager, method.method, method.func)

);

function runNext(element) {

const elementTimerManage = element.TimerManage;

if (elementTimerManage && elementTimerManage.constructor === TimerManager) {

elementTimerManage.next();

}

}

function registerMethods(type, element, time) {

let transition = “”;

if (type.indexOf(“slide”) > -1) {

transition = “height” + time + " ms";

util.setCss(element, “overflow”, “hidden”);

upAndDown();

} else {

transition = “opacity” + time + " ms";

inAndOut();

}

util.setCss(element, “transition”, transition);

function upAndDown() {

const isDown = type.toLowerCase().indexOf(“down”) > -1;

if (isDown) util.setCss(element, “display”, “block”);

const getPropValue = function (item, prop) {

let v = util.getCss(item, prop);

return util.removeAllSpace(v).length ? parseInt(v) : Number(v);

};

const elementChildHeight = [].reduce.call(

element.children,

(res, item) => {

res +=

item.offsetHeight +

getPropValue(item, “margin-top”) +

getPropValue(item, “margin-bottom”);

return res;

},

0

);

let totalHeight = Math.max(element.offsetHeight, elementChildHeight + 10);

let currentHeight = isDown ? 0 : totalHeight;

let unit = totalHeight / (time / 10);

if (isDown) util.setCss(element, “height”, “0px”);

let timer = setInterval(() => {

currentHeight = isDown ? currentHeight + unit : currentHeight - unit;

util.setCss(element, “height”, currentHeight + “px”);

if (currentHeight >= totalHeight || currentHeight <= 0) {

clearInterval(timer);

util.setCss(element, “height”, totalHeight + “px”);

runNext(element);

}

if (!isDown && currentHeight <= 0) {

util.setCss(element, “display”, “none”);

util.setCss(element, “height”, “0”);

}

}, 10);

}

function inAndOut() {

const isIn = type.toLowerCase().indexOf(“in”) > -1;

let timer = null;

let unit = (1 * 100) / (time / 10);

let curAlpha = isIn ? 0 : 100;

util.setSomeCss(element, [

{

prop: “display”,

value: isIn ? “none” : “block”,

},

{

prop: “opacity”,

value: isIn ? 0 : 1,

},

]);

let handleFade = function () {

curAlpha = isIn ? curAlpha + unit : curAlpha - unit;

if (element.style.display === “none” && isIn)

util.setCss(element, “display”, “block”);

util.setCss(element, “opacity”, (curAlpha / 100).toFixed(2));

if (curAlpha >= 100 || curAlpha <= 0) {

if (timer) clearTimeout(timer);

runNext(element);

if (curAlpha <= 0) util.setCss(element, “display”, “none”);

util.setCss(element, “opacity”, curAlpha >= 100 ? 1 : 0);

} else {

timer = setTimeout(handleFade, 10);

}

};

handleFade();

}

}

[“slideUp”, “slideDown”, “fadeIn”, “fadeOut”].forEach((method) => {

animation[method] = function (element) {

TimerManager.makeTimerManage(element);

element.TimerManage.add(function (element, time) {

return registerMethods(method, element, time);

}, arguments);

};

});

一些颜色操作的算法


const colorRegExp = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;

// RGB color

const colorRegRGB =

/[rR][gG][Bb][Aa]?({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 =

/1[gG][Bb][Aa]({3}[\s](1|1.0|0|0?.[0-9]{1,2})[\s][)]{1}$/;

// hsl color

const colorRegHSL =

/2[Ss][Ll](([\s]((100|[0-9][0-9]?)%|0)[\s],)([\s]((100|[0-9][0-9]?)%|0)[\s])[)]$/;

// HSLA color

const colorRegHSLA =

/3[Ss][Ll][Aa](([\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® +

“,” +

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( M a t h . c e i l ( r   ∗   255 ) , {Math.ceil(r * 255)}, Math.ceil(r  255),{Math.ceil(g * 255)},${Math.ceil(

b * 255

)},${a})`

);

}

/**

* rgba to hsla

* 换算公式:https://zh.wikipedia.org/wiki/HSL%E5%92%8CHSV%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4#%E4%BB%8EHSL%E5%88%B0RGB%E7%9A%84%E8%BD%AC%E6%8D%A2

* @param {*} rgba

*/

function colorRgbaToHsla(rgba) {

const rgbaArr = rgba

.slice(rgba.indexOf(“(”) + 1, rgba.lastIndexOf(“)”))

.split(“,”);

let a = rgbaArr.length < 4 ? 1 : Number(rgbaArr[3]);

let r = parseInt(rgbaArr[0]) / 255,

g = parseInt(rgbaArr[1]) / 255,

b = parseInt(rgbaArr[2]) / 255;

let max = Math.max(r, g, b),

min = Math.min(r, g, b);

let h,

s,

l = (max + min) / 2;

if (max === min) {

h = s = 0;

} else {

const d = max - min;

s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

switch (max) {

case r:

h = (g - b) / d + (g >= b ? 0 : 6);

break;

case g:

h = (b - r) / d + 2;

break;

case b:

h = (r - g) / d + 4;

break;

}

}

return {

colorStr: util.removeAllSpace(

“hsla(” +

Math.ceil(h * 60) +

“,” +

Math.ceil(s * 100) +

“%,” +

Math.ceil(l * 100) +

“%,” +

a +

“)”

),

colorObj: {

h,

s,

l,

a,

},

};

}

/**

* rgba to hsva

* @param {*} rgba

*/

function colorRgbaToHsva(rgba) {

const rgbaArr = rgba

.slice(rgba.indexOf(“(”) + 1, rgba.lastIndexOf(“)”))

.split(“,”);

let a = rgbaArr.length < 4 ? 1 : Number(rgbaArr[3]);

let r = parseInt(rgbaArr[0]) / 255,

g = parseInt(rgbaArr[1]) / 255,

b = parseInt(rgbaArr[2]) / 255;

let h, s, v;

let min = Math.min(r, g, b);

let max = (v = Math.max(r, g, b));

let diff = max - min;

if (max === 0) {

s = 0;

} else {

s = 1 - min / max;

}

if (max === min) {

h = 0;

} else {

switch (max) {

case r:

h = (g - b) / diff + (g < b ? 6 : 0);

break;

case g:

h = 2.0 + (b - r) / diff;

break;

case b:

h = 4.0 + (r - g) / diff;

break;

}

h = h * 60;

}

s = s * 100;

v = v * 100;

return {

h,

s,

v,

a,

};

}

/*

* 任意色值(甚至是CSS颜色关键字)转换为RGBA颜色的方法

* 此方法IE9+浏览器支持,基于DOM特性实现

* @param {*} color

*/

function colorToRgba(color) {

const div = document.createElement(“div”);

util.setCss(div, “background-color”, color);

document.body.appendChild(div);

const c = util.getCss(div, “background-color”);

document.body.removeChild(div);

let isAlpha = c.match(/,/g) && c.match(/,/g).length > 2;

let result = isAlpha

? c

: c.slice(0, 2) + “ba” + c.slice(3, c.length - 1) + “, 1)”;

return util.removeAllSpace(result);

}

/**

* 判断是否是合格的颜色值

* @param {*} color

*/

function isValidColor(color) {

// https://developer.mozilla.org/zh-CN/docs/Web/CSS/color_value#%E8%89%B2%E5%BD%A9%E5%85%B3%E9%94%AE%E5%AD%97

let isTransparent = color === “transparent”;

return (

colorRegExp.test(color) ||

colorRegRGB.test(color) ||

colorRegRGBA.test(color) ||

colorRegHSL.test(color) ||

colorRegHSLA.test(color) ||

(colorToRgba(color) !== “rgba(0,0,0,0)” && !isTransparent) ||

isTransparent

);

}

/**

* @param {*} color

* @returns

*/

function isAlphaColor(color) {

return (

colorRegRGB.test(color) ||

colorRegRGBA.test(color) ||

colorRegHSL.test(color) ||

colorRegHSLA.test(color)

);

}

工具方法这些我们已经完成了,接下来就是正式完成我们的主线功能逻辑了。

构造函数的定义


首先当然是完成我们的构造函数呢,我们把一个颜色选择器看做是一个构造实例,也因此,我们创建一个构造函数。

function ewColorPicker(options){

//主要逻辑

}

好的,接下来,让我们完成第一步,校验用户传入的参数,我们分为2种情况,第一种是如果用户传入的是一个DOM元素字符串或者是一个DOM元素,那么我们就要定义一个默认的配置对象,如果用户传入的是一个自定义的对象,那么我们将不采取默认对象。在校验之前,我们先思考一下可能需要处理的错误情况,也就是说假如用户传入的参数不符合规则,我们是不是需要返回一些错误提示给用户知道,现在让我们来定义一下这些错误规则吧。如下所示:

const NOT_DOM_ELEMENTS = [‘html’,‘head’,‘meta’,‘title’,‘link’,‘style’,‘script’,‘body’];

const ERROR_VARIABLE = {

DOM_OBJECT_ERROR:‘can not find the element by el property,make sure to pass a correct value!’,

DOM_ERROR:‘can not find the element,make sure to pass a correct param!’,

CONFIG_SIZE_ERROR:‘the value must be a string which is one of the normal,medium,small,mini,or must be an object and need to contain width or height property!’,

DOM_NOT_ERROR:‘Do not pass these elements: ’ + NOT_DOM_ELEMENTS.join(’,‘) + ’ as a param,pass the correct element such as div!’,

PREDEFINE_COLOR_ERROR:‘“predefineColor” is a array that is need to contain color value!’,

CONSTRUCTOR_ERROR:‘ewColorPicker is a constructor and should be called with the new keyword!’,

DEFAULT_COLOR_ERROR:‘the “defaultColor” is not an invalid color,make sure to use the correct color!’

};

这些校验错误都是常量,不允许被修改的,所以我们用大写字母来表示。接下来我们就需要在构造函数里做一个校验了。

配置属性的定义与校验


1.校验是否是实例化

判断new.target就可以了,如下所示:

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

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

function startInit(context,options){

let initOptions = initConfig(config);

if(!initOptions)return;

// 缓存配置对象属性

context.config = initOptions.config;

//定义私有属性

context._private = {

boxSize: {

b_width: null,

b_height: null

},

pickerFlag: false,

colorValue: “”,

};

// 在初始化之前所作的操作

context.beforeInit(initOptions.element,initOptions.config,initOptions.error);

}

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

export function initConfig(config){

// 默认的配置对象属性

const defaultConfig = { …colorPickerConfig };

let element,error,mergeConfig = null;

//如果第二个参数传的是字符串,或DOM对象,则初始化默认的配置

if (util.isString(config) || util.isDom(config) || util.isJQDom(config)) {

mergeConfig = defaultConfig;

element = util.isJQDom(config) ? config.get(0) : config;

error = ERROR_VARIABLE.DOM_ERROR;

} //如果是对象,则自定义配置,自定义配置选项如下:

else if (util.isDeepObject(config) && (util.isString(config.el) || util.isDom(config.el) || util.isJQDom(config.el))) {

mergeConfig = util.ewAssign(defaultConfig, config);

element = util.isJQDom(config.el) ? config.el.get(0) : config.el;

error = ERROR_VARIABLE.DOM_OBJECT_ERROR;

} else {

if(util.isDeepObject(config)){

error = ERROR_VARIABLE.DOM_OBJECT_ERROR;

}else{

error = ERROR_VARIABLE.DOM_ERROR;

}

}

return {

element,

config:mergeConfig,

error

}

}

然后我们来看看默认的配置对象属性:

export const emptyFun = function () { };

const baseDefaultConfig = {

alpha: false,

size: “normal”,

predefineColor: [],

disabled: false,

defaultColor: “”,

pickerAnimation: “height”,

pickerAnimationTime:200,

sure: emptyFun,

clear: emptyFun,

togglePicker: emptyFun,

changeColor: emptyFun,

isClickOutside: true,

}

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

function beforeInit(element, config, errorText) {

let ele = util.isDom(element) ? element : util.isString(element) ? util.$(element) : util.isJQDom(element) ? element.get(0) : null;

if (!ele) return util.ewError(errorText);

ele = ele.length ? ele[0] : ele;

if (!ele.tagName) return util.ewError(errorText);

if (!isNotDom(ele)) {

if(!this._color_picker_uid){

this._color_picker_uid = util.createUUID();

}

this.init(ele, config);

}

}

其中,isNotDom方法,我们先定义好:

const isNotDom = ele => {

if (NOT_DOM_ELEMENTS.indexOf(ele.tagName.toLowerCase()) > -1) {

util.ewError(ERROR_VARIABLE.DOM_NOT_ERROR);

return true;

}

return false;

}

最后,我们来看init函数,如下所示:

function init(element, config) {

let b_width, b_height;

//自定义颜色选择器的类型

if (util.isString(config.size)) {

switch (config.size) {

case ‘normal’:

b_width = b_height = ‘40px’;

break;

case ‘medium’:

b_width = b_height = ‘36px’;

break;

case ‘small’:

b_width = b_height = ‘32px’;

break;

case ‘mini’:

b_width = b_height = ‘28px’;

break;

default:

b_width = b_height = ‘40px’;

break;

}

} else if (util.isDeepObject(config.size)) {

b_width = config.size.width && (util.isNumber(config.size.width) || util.isString(config.size.width)) ? (parseInt(config.size.width) <= 25 ? 25 :  parseInt(config.size.width))+ ‘px’ : ‘40px’;

b_height = config.size.height && (util.isNumber(config.size.height) || util.isString(config.size.height)) ? (parseInt(config.size.height) <= 25 ? 25 : parseInt(config.size.height)) + ‘px’ : ‘40px’;

} else {

return util.ewError(ERROR_VARIABLE.CONFIG_SIZE_ERROR);

}

this._private.boxSize.b_width = b_width;

this._private.boxSize.b_height = b_height;

//渲染选择器

this.render(element, config);

}

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

  • 定义了一些错误的常量,用于提示。

  • 验证用户传入的参数,分为2种情况,第一种是字符串或者DOM元素,第二种是自定义对象,其中必须指定el属性为一个DOM元素。

  • 定义了默认配置对象,定义了一些私有变量。

  • 对色块盒子的大小做了一次规范化。

接下来,就是我们实际渲染一个颜色选择器的渲染函数,即render函数。

render函数

render函数的核心思路非常的简单,实际上就是创建一堆元素,然后添加到元素当中去。只不过我们需要注意几点,例如预定义颜色数组,默认颜色值,以及色块盒子的大小,还有就是alpha柱的显隐。如下所示:

ewColorPicker.prototype.render = function(element,config){

let predefineColorHTML = ‘’,

alphaBar = ‘’,

hueBar = ‘’,

predefineHTML = ‘’,

boxDisabledClassName = ‘’,

boxBackground = ‘’,

boxHTML = ‘’,

clearHTML = ‘’,

sureHTML = ‘’,

inputHTML = ‘’,

btnGroupHTML = ‘’,

dropHTML = ‘’,

openChangeColorModeHTML = ‘’,

openChangeColorModeLabelHTML = ‘’,

horizontalSliderHTML = ‘’,

verticalSliderHTML = ‘’;

const p_c = config.predefineColor;

if (!util.isDeepArray(p_c)) return util.ewError(ERROR_VARIABLE.PREDEFINE_COLOR_ERROR);

if (p_c.length) {

p_c.map((color,index) => {

let isValidColorString = util.isString(color) && isValidColor(color);

let isValidColorObj = util.isDeepObject(color) && color.hasOwnProperty(‘color’) && isValidColor(color.color);

let renderColor = isValidColorString ? color : isValidColorObj ? color.color : ‘’;

let renderDisabled = isValidColorObj ? setPredefineDisabled(color.disabled) : ‘’;

predefineColorHTML += `

`;

})

};

//打开颜色选择器的方框

const colorBox = config.defaultColor ? `

` : `
×
`;

//透明度

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:${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 = ‘’;

}

if (config.openChangeColorMode) {

if (!config.alpha || !config.hue) return util.ewError(ERROR_VARIABLE.COLOR_MODE_ERROR);

openChangeColorModeHTML = `

`;

openChangeColorModeLabelHTML = <label class="ew-color-mode-title">${this.colorMode[1]}</label>;

}

if (config.hasColorInput || config.hasClear || config.hasSure) {

dropHTML = config.openChangeColorMode ? `

o p e n C h a n g e C o l o r M o d e L a b e l H T M L {openChangeColorModeLabelHTML} openChangeColorModeLabelHTML{inputHTML}${openChangeColorModeHTML}

${btnGroupHTML}

` : `

i n p u t H T M L {inputHTML} inputHTML{btnGroupHTML}

`;

}

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}

${ verticalSliderHTML }

${ horizontalSliderHTML }

${dropHTML}

${predefineHTML}

`;

element.setAttribute(“color-picker-id”,this._color_picker_uid);

element.innerHTML = <div class="ew-color-picker-container">${ html }</div>;

this.startMain(element, config);

}

startMain函数

接下来,我们来看看我们要实现哪些逻辑。首先我们需要确定一个初始值的颜色对象,用hsva来表示,我们创建一个initColor函数,代码如下所示:

function initColor(context, config) {

if (config.defaultColor) {

context.hsvaColor = colorRegRGBA.test(config.defaultColor) ? colorRgbaToHsva(config.defaultColor) : colorRgbaToHsva(colorToRgba(config.defaultColor));

} else {

context.hsvaColor = {

h: 0,

s: 100,

v: 100,

a: 1

};

}

}

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

现在我们再来分析一下,我们可以大致得到主要的逻辑有:

  • 初始化一些后续需要操作的DOM元素与颜色值以及面板的left与top偏移

  • 预定义颜色逻辑

  • 初始化颜色面板的动画逻辑

  • 色块盒子的处理逻辑

  • 输入框逻辑

  • 禁用逻辑

  • 点击目标区域之外关闭颜色面板的逻辑

  • 清空按钮与确定按钮的逻辑

  • 颜色面板的点击逻辑与颜色面板的元素拖拽逻辑

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

// 初始化逻辑

let scope = this;

this.$Dom = Object.create(null);

this.$Dom.rootElement = ele;

this.$Dom.picker = getELByClass(ele, ‘ew-color-picker’);

this.$Dom.pickerPanel = getELByClass(ele, ‘ew-color-panel’);

this.$Dom.pickerCursor = getELByClass(ele, ‘ew-color-cursor’);

this.$Dom.verticalSlider = getELByClass(ele, ‘ew-is-vertical’);

// 清空按钮逻辑

this.$Dom.pickerClear = getELByClass(ele, ‘ew-color-clear’);

this.$Dom.hueBar = getELByClass(ele, ‘ew-color-slider-bar’);

this.$Dom.hueThumb = getELByClass(ele, ‘ew-color-slider-thumb’);

this.$Dom.preDefineItem = getELByClass(ele, ‘ew-pre-define-color’, true);

this.$Dom.box = getELByClass(ele, ‘ew-color-picker-box’);

// 输入框逻辑

this.$Dom.pickerInput = getELByClass(ele, ‘ew-color-input’);

// 确定按钮逻辑

this.$Dom.pickerSure = getELByClass(ele, ‘ew-color-sure’);

initColor(this, config);

//初始化面板的left偏移和top偏移

const panelWidth = this.panelWidth = parseInt(util.getCss(this.$Dom.pickerPanel, ‘width’));

const panelHeight = this.panelHeight = parseInt(util.getCss(this.$Dom.pickerPanel, ‘height’));

const rect = util.getRect(ele);

this.panelLeft = rect.left;

this.panelTop = rect.top + rect.height;

接着我们开始初始化预定义颜色逻辑:

// 预定义颜色逻辑

if (this.$Dom.preDefineItem.length) {

initPreDefineHandler(util.ewObjToArray(this.$Dom.preDefineItem), scope);

}

function initPreDefineHandler(items, context) {

// get the siblings

const siblings = el => Array.prototype.filter.call(el.parentElement.children, child => child !== el);

items.map(item => {

const clickHandler = event => {

util.addClass(item, ‘ew-pre-define-color-active’);

siblings(item).forEach(sibling => util.removeClass(sibling, ‘ew-pre-define-color-active’))

const bgColor = util.getCss(event.target, ‘background-color’);

context.hsvaColor = colorRgbaToHsva(bgColor);

setColorValue(context, context.panelWidth, context.panelHeight, true);

changeElementColor(context);

};

const blurHandler = event => util.removeClass(event.target, ‘ew-pre-define-color-active’);

[{ type: “click”, handler: clickHandler }, { type: “blur”, handler: blurHandler }].forEach(t => {

if (!context.config.disabled && util.ewObjToArray(item.classList).indexOf(‘ew-pre-define-color-disabled’) === -1) {

util.on(item, t.type, t.handler);

}

});

})

}

然后我们开始初始化动画逻辑:

initAnimation(scope);

function initAnimation(context) {

//颜色选择器打开的动画初始设置

const expression = getAnimationType(context);

util.setCss(context.$Dom.picker, (expression ? ‘display’ : ‘opacity’), (expression ? ‘none’ : 0))

let pickerWidth = 0, sliderWidth = 0, sliderHeight = 0;

let isVerticalAlpha = !context.isAlphaHorizontal;

let isVerticalHue = !context.isHueHorizontal;

let isHue = context.config.hue;

let isAlpha = context.config.alpha;

if (isAlpha && isHue && isVerticalAlpha && isVerticalHue) {

pickerWidth = 320;

sliderWidth = 28;

} else if (isVerticalAlpha && isAlpha && (!isVerticalHue || !isHue) || (isVerticalHue && isHue && (!isVerticalAlpha || !isAlpha))) {

pickerWidth = 300;

sliderWidth = sliderHeight = 14;

} else {

pickerWidth = 280;

sliderHeight = isAlpha && isHue && !isVerticalHue && !isVerticalAlpha ? 30 : 14;

}

util.setCss(context.$Dom.picker, ‘min-width’, pickerWidth + ‘px’);

if (context.$Dom.horizontalSlider) {

util.setCss(context.$Dom.horizontalSlider, ‘height’, sliderHeight + ‘px’);

}

if (context.$Dom.verticalSlider) {

util.setCss(context.$Dom.verticalSlider, ‘width’, sliderWidth + ‘px’);

}

}

接下来,就是我们的一些功能逻辑了,让我们一一来实现吧,首先我们需要的实现的是点击色块打开或者关闭颜色选择器面板。如下所示:

// 色块

if (!config.disabled){

util.on(this.$Dom.box, ‘click’, () => handlePicker(ele, scope, (flag) => {

if (flag && scope.config.isClickOutside) {

initColor(this, config);

setColorValue(scope, scope.panelWidth, scope.panelHeight, false);

handleClickOutSide(scope, scope.config);

}

}));

}

这里的逻辑也不复杂,就是判断是否禁用,然后为盒子元素添加点击事件,在这里核心的功能就是handlePicker方法,我们可以看到传入3个参数,第一个参数为当前根容器元素,第二个参数则是当前执行上下文对象,第三个参数则是一个回调函数,用来做一些细节处理。setColorValue方法暂时先不作说明,而initColor方法我们前面已经讲过,handleClickOutSide方法我们将在讲完handlePicker方法之后再做介绍,现在让我们先来看一下handlePicker这个方法吧。

export function handlePicker(el, scope,callback) {

scope._private.pickerFlag = !scope._private.pickerFlag;

openAndClose(scope);

initColor(scope, scope.config);

setColorValue(scope, scope.panelWidth, scope.panelHeight, false);

if (util.isFunction(scope.config.togglePicker)){

scope.config.togglePicker(el, scope._private.pickerFlag,scope);

}

if(util.isFunction(callback))callback(scope._private.pickerFlag);

}

可以看到,这个方法的核心操作是改变颜色选择器的状态,最重要的就是openAndClose方法呢,让我们一起来看一下吧,

export function openAndClose(scope) {

const time = scope.config.pickerAnimationTime;

scope._private.pickerFlag ? open(getAnimationType(scope), scope. D o m . p i c k e r , t i m e )   :   c l o s e ( g e t A n i m a t i o n T y p e ( s c o p e ) ,   s c o p e . Dom.picker,time) : close(getAnimationType(scope), scope. Dom.picker,time) : close(getAnimationType(scope), scope.Dom.picker,time);

}

export function getAnimationType(scope) {

return scope.config.pickerAnimation;

}

这个方法就是获取动画执行时间,然后根据pickerFlag来判断是开启还是关闭颜色选择器,核心的就是openclose方法,两者都接收3个参数,第一个则是动画的类型,第二个则是颜色选择器面板元素,第三个则是动画执行时间。我们分别来看一下:

1.open方法

export function open(expression, picker,time = 200) {

time = time > 10000 ? 10000 : time;

let animation = ‘’;

switch(expression){

case ‘opacity’:

animation = ‘fadeIn’;

break;

default:

animation = ‘slideDown’;

}

return anianimation;

}

2.close方法

export function close(expression, picker,time = 200) {

time = time > 10000 ? 10000 : time;

let animation = ‘’;

switch(expression){

case ‘opacity’:

animation = ‘fadeOut’;

break;

default:

animation = ‘slideUp’;

}

return anianimation;

}

可以看到,我们再openclose方法内部对时间做了一次限制处理,然后判断动画类型来决定调用哪种动画来实现颜色选择器的开启和关闭。到这里,我们还少实现了一个方法,那就是handleClickOutSide,让我们来一起看一下这个方法的实现:

export function handleClickOutSide(context, config) {

util.clickOutSide(context, config, () => {

if (context._private.pickerFlag) {

context._private.pickerFlag = false;

closePicker(getAnimationType(config.pickerAnimation), context.$Dom.picker,config.pickerAnimationTime);

}

});

}

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

util[“clickOutSide”] = (context, config, callback) => {

const mouseHandler = (event) => {

const rect = util.getRect(context.$Dom.picker);

const boxRect = util.getRect(context.$Dom.box);

const target = event.target;

if (!target) return;

const targetRect = util.getRect(target);

// 利用rect来判断用户点击的地方是否在颜色选择器面板区域之内

if (targetRect.x >= rect.x && targetRect.y >= rect.y && targetRect.width <= rect.width) return;

// 如果点击的是盒子元素

if (targetRect.x >= boxRect.x && targetRect.y >= boxRect.y && targetRect.width <= boxRect.width && targetRect.height <= boxRect.height) return;

callback();

setTimeout(() => {

util.off(document, util.eventType[0], mouseHandler);

}, 0);

}

util.on(document, util.eventType[0], mouseHandler);

}

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

if (config.isClickOutside) {

handleClickOutSide(this, config);

}

代码不复杂,很容易就理解了。接下来,我们来看alpha透明度的逻辑的实现。如下:

if (!config.disabled) {

this.bindEvent(this.$Dom.alphaBarThumb, (scope, el, x, y) => changeAlpha(scope, y));

util.on(this.$Dom.alphaBar, ‘click’, event => changeAlpha(scope, event.y));

}

可以看到,我们这里首先需要判断是否禁用,然后我们需要2种方式给透明度柱子添加事件逻辑,第一种就是拖拽透明度柱子的滑块元素所触发的拖拽事件,第二种则是点击透明度柱子的事件,这其中涉及到了一个changeAlpha事件。我们来看一下:

export function changeAlpha(context, position) {

let value = setAlphaHuePosition(context. D o m . a l p h a B a r , c o n t e x t . Dom.alphaBar,context. Dom.alphaBar,context.Dom.alphaBarThumb,position);

let currentValue = value.barPosition - value.barThumbPosition <= 0 ? 0 : value.barPosition - value.barThumbPosition;

let alpha = context.isAlphaHorizontal ? 1 - currentValue / value.barPosition : currentValue / value.barPosition;

context.hsvaColor.a = alpha >= 1 ? 1 : alpha.toFixed(2);

changeElementColor(context, true);

}

这个方法又涉及到了2个方法setAlphaHuePositionchangeElementColor。我们分别来看一下:

function setAlphaHuePosition(bar,thumb,position){

const positionProp = ‘y’;

const barProp = ‘top’;

const barPosition = bar.offsetHeight,

barRect = util.getRect(bar);

const barThumbPosition = Math.max(0,Math.min(position - barRect[positionProp],barPosition));

util.setCss(thumb,barProp,barThumbPosition +‘px’);

return {

barPosition,

barThumbPosition

}

}

可以看到,这里我们主要的逻辑操作就是规范化样式处理,也就是说我们拖动滑块改变的是垂直方向上的top偏移(未来会考虑加入水平方向也就是left偏移),所以单独抽取出来做一个公共的方法,这个top偏移会有一个最大值与最小值的比较。接下来,我们来看changeElementColor方法的实现:

export function changeElementColor(scope, isAlpha) {

const color = colorHsvaToRgba(scope.hsvaColor);

let newColor = isAlpha || scope.config.alpha ? color : colorRgbaToHex(color);

scope.$Dom.pickerInput.value = newColor;

scope.prevInputValue = newColor;

changeAlphaBar(scope);

if (util.isFunction(scope.config.changeColor))scope.config.changeColor(newColor);

}

显然这个方法的核心目的就是处理颜色值的改变,我们有2个参数,第一个参数则是当前上下文,第二个参数用于判断透明度柱是否开启。先利用colorHsvaToRgba方法将当前的颜色值转换成rgba颜色,然后判断如果开启了透明度柱,则不需要进行转换,否则就需要转换成hex颜色模式,然后我们把新的颜色值传给input元素。并且缓存了一下这个颜色值,然后这里需要注意一下,如果改变了颜色值,则有可能透明度会改变,因此,需要再次调用changeAlphaBar方法来改变透明度柱的功能。最后我们暴露了一个changeColor方法接口给用户使用。

前面还提到了一个bindEvent方法,我们接下来来看一下这个bindEvent方法的实现。如下:

export function bindEvent(el, callback, bool) {

const context = this;

const callResult = event => {

context.moveX = util.eventType[0].indexOf(‘touch’) > -1 ? event.changedTouches[0].clientX : event.clientX;

context.moveY = util.eventType[0].indexOf(‘touch’) > -1 ? event.changedTouches[0].clientY : event.clientY;

这里分享一份由字节前端面试官整理的「2021大厂前端面试手册」,内容囊括Html、CSS、Javascript、Vue、HTTP、浏览器面试题、数据结构与算法。全部整理在下方文档中,共计111道

HTML

  • HTML5有哪些新特性?

  • Doctype作⽤? 严格模式与混杂模式如何区分?它们有何意义?

  • 如何实现浏览器内多个标签页之间的通信?

  • ⾏内元素有哪些?块级元素有哪些? 空(void)元素有那些?⾏内元 素和块级元素有什么区别?

  • 简述⼀下src与href的区别?

  • cookies,sessionStorage,localStorage 的区别?

  • HTML5 的离线储存的使用和原理?

  • 怎样处理 移动端 1px 被 渲染成 2px 问题?

  • iframe 的优缺点?

  • Canvas 和 SVG 图形的区别是什么?

JavaScript

  • 问:0.1 + 0.2 === 0.3 嘛?为什么?

  • JS 数据类型

  • 写代码:实现函数能够深度克隆基本类型

  • 事件流

  • 事件是如何实现的?

  • new 一个函数发生了什么

  • 什么是作用域?

  • JS 隐式转换,显示转换

  • 了解 this 嘛,bind,call,apply 具体指什么

  • 手写 bind、apply、call

  • setTimeout(fn, 0)多久才执行,Event Loop

  • 手写题:Promise 原理

  • 说一下原型链和原型链的继承吧

  • 数组能够调用的函数有那些?

  • PWA使用过吗?serviceWorker的使用原理是啥?

  • ES6 之前使用 prototype 实现继承

  • 箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?

  • 事件循环机制 (Event Loop)


  1. rR ↩︎

  2. hH ↩︎

  3. hH ↩︎

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值