@thednp/color-picker 0.0.1-alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1333 @@
1
+ import { addListener, removeListener } from 'event-listener.js';
2
+
3
+ import ariaSelected from 'shorter-js/src/strings/ariaSelected';
4
+ import ariaExpanded from 'shorter-js/src/strings/ariaExpanded';
5
+ import ariaHidden from 'shorter-js/src/strings/ariaHidden';
6
+ import ariaLabelledBy from 'shorter-js/src/strings/ariaLabelledBy';
7
+ import keyArrowDown from 'shorter-js/src/strings/keyArrowDown';
8
+ import keyArrowUp from 'shorter-js/src/strings/keyArrowUp';
9
+ import keyArrowLeft from 'shorter-js/src/strings/keyArrowLeft';
10
+ import keyArrowRight from 'shorter-js/src/strings/keyArrowRight';
11
+ import keyEnter from 'shorter-js/src/strings/keyEnter';
12
+ import keySpace from 'shorter-js/src/strings/keySpace';
13
+ import keyEscape from 'shorter-js/src/strings/keyEscape';
14
+
15
+ import isMobile from 'shorter-js/src/boolean/isMobile';
16
+ import getUID from 'shorter-js/src/get/getUID';
17
+ import getBoundingClientRect from 'shorter-js/src/get/getBoundingClientRect';
18
+ import querySelector from 'shorter-js/src/selectors/querySelector';
19
+ import querySelectorAll from 'shorter-js/src/selectors/querySelectorAll';
20
+ import closest from 'shorter-js/src/selectors/closest';
21
+ import createElement from 'shorter-js/src/misc/createElement';
22
+ import createElementNS from 'shorter-js/src/misc/createElementNS';
23
+ import dispatchEvent from 'shorter-js/src/misc/dispatchEvent';
24
+ import ObjectAssign from 'shorter-js/src/misc/ObjectAssign';
25
+ import Data, { getInstance } from 'shorter-js/src/misc/data';
26
+ import hasClass from 'shorter-js/src/class/hasClass';
27
+ import addClass from 'shorter-js/src/class/addClass';
28
+ import removeClass from 'shorter-js/src/class/removeClass';
29
+ import hasAttribute from 'shorter-js/src/attr/hasAttribute';
30
+ import setAttribute from 'shorter-js/src/attr/setAttribute';
31
+ import getAttribute from 'shorter-js/src/attr/getAttribute';
32
+ import removeAttribute from 'shorter-js/src/attr/removeAttribute';
33
+
34
+ import getColorForm from './util/getColorForm';
35
+ import getColorControl from './util/getColorControl';
36
+ import vHidden from './util/vHidden';
37
+ import Color from './color';
38
+
39
+ // ColorPicker GC
40
+ // ==============
41
+ const colorPickerString = 'color-picker';
42
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
43
+ const nonColors = ['transparent', 'currentColor', 'inherit', 'initial'];
44
+ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
45
+ const colorPickerLabels = {
46
+ pickerLabel: 'Colour Picker',
47
+ toggleLabel: 'Select colour',
48
+ menuLabel: 'Select colour preset',
49
+ requiredLabel: 'Required',
50
+ formatLabel: 'Colour Format',
51
+ formatHEX: 'Hexadecimal Format',
52
+ formatRGB: 'RGB Format',
53
+ formatHSL: 'HSL Format',
54
+ alphaLabel: 'Alpha',
55
+ appearanceLabel: 'Colour Appearance',
56
+ hexLabel: 'Hexadecimal',
57
+ hueLabel: 'Hue',
58
+ saturationLabel: 'Saturation',
59
+ lightnessLabel: 'Lightness',
60
+ redLabel: 'Red',
61
+ greenLabel: 'Green',
62
+ blueLabel: 'Blue',
63
+ };
64
+
65
+ // ColorPicker Static Methods
66
+ // ==========================
67
+
68
+ /** @type {CP.GetInstance<ColorPicker>} */
69
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
70
+
71
+ /** @type {CP.InitCallback<ColorPicker>} */
72
+ const initColorPicker = (element) => new ColorPicker(element);
73
+
74
+ // ColorPicker Private Methods
75
+ // ===========================
76
+
77
+ /**
78
+ * Add / remove `ColorPicker` main event listeners.
79
+ * @param {ColorPicker} self
80
+ * @param {boolean=} action
81
+ */
82
+ function toggleEvents(self, action) {
83
+ const fn = action ? addListener : removeListener;
84
+ const { input, pickerToggle, menuToggle } = self;
85
+
86
+ fn(input, 'focusin', self.showPicker);
87
+ fn(pickerToggle, 'click', self.togglePicker);
88
+
89
+ fn(input, 'keydown', self.keyHandler);
90
+
91
+ if (menuToggle) {
92
+ fn(menuToggle, 'click', self.toggleMenu);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Generate HTML markup and update instance properties.
98
+ * @param {ColorPicker} self
99
+ */
100
+ function initCallback(self) {
101
+ const {
102
+ input, parent, format, id, componentLabels, keywords,
103
+ } = self;
104
+ const colorValue = getAttribute(input, 'value') || '#fff';
105
+
106
+ const {
107
+ toggleLabel, menuLabel, formatLabel, pickerLabel, appearanceLabel,
108
+ } = componentLabels;
109
+
110
+ // update color
111
+ const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
112
+ self.color = new Color(color, { format });
113
+
114
+ // set initial controls dimensions
115
+ // make the controls smaller on mobile
116
+ const cv1w = isMobile ? 150 : 230;
117
+ const cvh = isMobile ? 150 : 230;
118
+ const cv2w = 21;
119
+ const dropClass = isMobile ? ' mobile' : '';
120
+ const ctrl1Labelledby = format === 'hsl' ? `appearance_${id} appearance1_${id}` : `appearance1_${id}`;
121
+ const ctrl2Labelledby = format === 'hsl' ? `appearance2_${id}` : `appearance_${id} appearance2_${id}`;
122
+
123
+ const pickerBtn = createElement({
124
+ tagName: 'button',
125
+ className: 'picker-toggle button-appearance',
126
+ ariaExpanded: 'false',
127
+ ariaHasPopup: 'true',
128
+ ariaLive: 'polite',
129
+ });
130
+ setAttribute(pickerBtn, 'tabindex', '-1');
131
+ pickerBtn.append(createElement({
132
+ tagName: 'span',
133
+ className: vHidden,
134
+ innerText: 'Open Color Picker',
135
+ }));
136
+
137
+ const colorPickerDropdown = createElement({
138
+ tagName: 'div',
139
+ className: `color-dropdown picker${dropClass}`,
140
+ });
141
+ setAttribute(colorPickerDropdown, ariaLabelledBy, `picker-label-${id} format-label-${id}`);
142
+ setAttribute(colorPickerDropdown, 'role', 'group');
143
+ colorPickerDropdown.append(
144
+ createElement({
145
+ tagName: 'label',
146
+ className: vHidden,
147
+ ariaHidden: 'true',
148
+ id: `picker-label-${id}`,
149
+ innerText: `${pickerLabel}`,
150
+ }),
151
+ createElement({
152
+ tagName: 'label',
153
+ className: vHidden,
154
+ ariaHidden: 'true',
155
+ id: `format-label-${id}`,
156
+ innerText: `${formatLabel}`,
157
+ }),
158
+ createElement({
159
+ tagName: 'label',
160
+ className: `color-appearance ${vHidden}`,
161
+ ariaHidden: 'true',
162
+ ariaLive: 'polite',
163
+ id: `appearance_${id}`,
164
+ innerText: `${appearanceLabel}`,
165
+ }),
166
+ );
167
+
168
+ const colorControls = createElement({
169
+ tagName: 'div',
170
+ className: `color-controls ${format}`,
171
+ });
172
+
173
+ colorControls.append(
174
+ getColorControl(1, id, cv1w, cvh, ctrl1Labelledby),
175
+ getColorControl(2, id, cv2w, cvh, ctrl2Labelledby),
176
+ );
177
+
178
+ if (format !== 'hex') {
179
+ colorControls.append(
180
+ getColorControl(3, id, cv2w, cvh),
181
+ );
182
+ }
183
+
184
+ // @ts-ignore
185
+ const colorForm = getColorForm(self);
186
+ colorPickerDropdown.append(colorControls, colorForm);
187
+ parent.append(pickerBtn, colorPickerDropdown);
188
+
189
+ // set color key menu template
190
+ if (keywords) {
191
+ const colorKeys = keywords;
192
+ const presetsDropdown = createElement({
193
+ tagName: 'div',
194
+ className: `color-dropdown menu${dropClass}`,
195
+ });
196
+ const presetsMenu = createElement({
197
+ tagName: 'ul',
198
+ ariaLabel: `${menuLabel}`,
199
+ className: 'color-menu',
200
+ });
201
+ setAttribute(presetsMenu, 'role', 'listbox');
202
+ presetsDropdown.append(presetsMenu);
203
+
204
+ colorKeys.forEach((x) => {
205
+ const xKey = x.trim();
206
+ const xRealColor = new Color(xKey, { format }).toString();
207
+ const isActive = xRealColor === getAttribute(input, 'value');
208
+ const active = isActive ? ' active' : '';
209
+
210
+ const keyOption = createElement({
211
+ tagName: 'li',
212
+ className: `color-option${active}`,
213
+ ariaSelected: isActive ? 'true' : 'false',
214
+ innerText: `${x}`,
215
+ });
216
+ setAttribute(keyOption, 'role', 'option');
217
+ setAttribute(keyOption, 'tabindex', '0');
218
+ setAttribute(keyOption, 'data-value', `${xKey}`);
219
+ presetsMenu.append(keyOption);
220
+ });
221
+ const presetsBtn = createElement({
222
+ tagName: 'button',
223
+ className: 'menu-toggle button-appearance',
224
+ ariaExpanded: 'false',
225
+ ariaHasPopup: 'true',
226
+ });
227
+ const xmlns = encodeURI('http://www.w3.org/2000/svg');
228
+ const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
229
+ setAttribute(presetsIcon, 'xmlns', xmlns);
230
+ setAttribute(presetsIcon, ariaHidden, 'true');
231
+ setAttribute(presetsIcon, 'viewBox', '0 0 512 512');
232
+ const piPath = createElementNS(xmlns, { tagName: 'path' });
233
+ setAttribute(piPath, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
234
+ setAttribute(piPath, 'fill', '#fff');
235
+ presetsIcon.append(piPath);
236
+ presetsBtn.append(createElement({
237
+ tagName: 'span',
238
+ className: vHidden,
239
+ innerText: `${toggleLabel}`,
240
+ }), presetsIcon);
241
+
242
+ parent.append(presetsBtn, presetsDropdown);
243
+ }
244
+
245
+ // solve non-colors after settings save
246
+ if (keywords && nonColors.includes(colorValue)) {
247
+ self.value = colorValue;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Add / remove `ColorPicker` event listeners active only when open.
253
+ * @param {ColorPicker} self
254
+ * @param {boolean=} action
255
+ */
256
+ function toggleEventsOnShown(self, action) {
257
+ const fn = action ? addListener : removeListener;
258
+ const pointerEvents = 'ontouchstart' in document
259
+ ? { down: 'touchstart', move: 'touchmove', up: 'touchend' }
260
+ : { down: 'mousedown', move: 'mousemove', up: 'mouseup' };
261
+
262
+ fn(self.controls, pointerEvents.down, self.pointerDown);
263
+ self.controlKnobs.forEach((x) => fn(x, 'keydown', self.handleKnobs));
264
+
265
+ fn(window, 'scroll', self.handleScroll);
266
+
267
+ [self.input, ...self.inputs].forEach((x) => fn(x, 'change', self.changeHandler));
268
+
269
+ if (self.colorMenu) {
270
+ fn(self.colorMenu, 'click', self.menuClickHandler);
271
+ fn(self.colorMenu, 'keydown', self.menuKeyHandler);
272
+ }
273
+
274
+ fn(document, pointerEvents.move, self.pointerMove);
275
+ fn(document, pointerEvents.up, self.pointerUp);
276
+ fn(window, 'keyup', self.handleDismiss);
277
+ fn(self.parent, 'focusout', self.handleFocusOut);
278
+ }
279
+
280
+ /**
281
+ * Triggers the `ColorPicker` original event.
282
+ * @param {ColorPicker} self
283
+ */
284
+ function firePickerChange(self) {
285
+ dispatchEvent(self.input, new CustomEvent('colorpicker.change'));
286
+ }
287
+
288
+ /**
289
+ * Toggles the visibility of a dropdown or returns false if none is visible.
290
+ * @param {HTMLElement} element
291
+ * @param {boolean=} check
292
+ * @returns {void | boolean}
293
+ */
294
+ function classToggle(element, check) {
295
+ const fn1 = !check ? 'forEach' : 'some';
296
+ const fn2 = !check ? removeClass : hasClass;
297
+
298
+ if (element) {
299
+ return ['show', 'show-top'][fn1]((x) => fn2(element, x));
300
+ }
301
+
302
+ return false;
303
+ }
304
+
305
+ /**
306
+ * Shows the `ColorPicker` presets menu.
307
+ * @param {ColorPicker} self
308
+ */
309
+ function showMenu(self) {
310
+ classToggle(self.colorPicker);
311
+ addClass(self.colorMenu, 'show');
312
+ self.show();
313
+ setAttribute(self.menuToggle, ariaExpanded, 'true');
314
+ }
315
+
316
+ /**
317
+ * Color Picker
318
+ * @see http://thednp.github.io/color-picker
319
+ */
320
+ export default class ColorPicker {
321
+ /**
322
+ * Returns a new ColorPicker instance.
323
+ * @param {HTMLInputElement | string} target the target `<input>` element
324
+ */
325
+ constructor(target) {
326
+ const self = this;
327
+ /** @type {HTMLInputElement} */
328
+ // @ts-ignore
329
+ self.input = querySelector(target);
330
+ // invalidate
331
+ if (!self.input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
332
+ const { input } = self;
333
+
334
+ /** @type {HTMLElement} */
335
+ // @ts-ignore
336
+ self.parent = closest(input, `.${colorPickerString},${colorPickerString}`);
337
+ if (!self.parent) throw new TypeError('ColorPicker requires a specific markup to work.');
338
+
339
+ /** @type {number} */
340
+ self.id = getUID(input, colorPickerString);
341
+
342
+ // set initial state
343
+ /** @type {HTMLCanvasElement?} */
344
+ self.dragElement = null;
345
+ /** @type {boolean} */
346
+ self.isOpen = false;
347
+ /** @type {Record<string, number>} */
348
+ self.controlPositions = {
349
+ c1x: 0, c1y: 0, c2y: 0, c3y: 0,
350
+ };
351
+ /** @type {Record<string, string>} */
352
+ self.colorLabels = {};
353
+ /** @type {Array<string> | false} */
354
+ self.keywords = false;
355
+ /** @type {Color} */
356
+ self.color = new Color('white', { format: self.format });
357
+ /** @type {Record<string, string>} */
358
+ self.componentLabels = ObjectAssign({}, colorPickerLabels);
359
+
360
+ const { componentLabels, colorLabels, keywords } = input.dataset;
361
+ const temp = componentLabels ? JSON.parse(componentLabels) : {};
362
+ self.componentLabels = ObjectAssign(self.componentLabels, temp);
363
+
364
+ const translatedColorLabels = colorLabels && colorLabels.split(',').length === 17
365
+ ? colorLabels.split(',') : colorNames;
366
+
367
+ // expose color labels to all methods
368
+ colorNames.forEach((c, i) => { self.colorLabels[c] = translatedColorLabels[i]; });
369
+
370
+ // set colour presets
371
+ if (keywords !== 'false') {
372
+ self.keywords = keywords ? keywords.split(',') : nonColors;
373
+ }
374
+
375
+ // bind events
376
+ self.showPicker = self.showPicker.bind(self);
377
+ self.togglePicker = self.togglePicker.bind(self);
378
+ self.toggleMenu = self.toggleMenu.bind(self);
379
+ self.menuClickHandler = self.menuClickHandler.bind(self);
380
+ self.menuKeyHandler = self.menuKeyHandler.bind(self);
381
+ self.pointerDown = self.pointerDown.bind(self);
382
+ self.pointerMove = self.pointerMove.bind(self);
383
+ self.pointerUp = self.pointerUp.bind(self);
384
+ self.handleScroll = self.handleScroll.bind(self);
385
+ self.handleFocusOut = self.handleFocusOut.bind(self);
386
+ self.changeHandler = self.changeHandler.bind(self);
387
+ self.handleDismiss = self.handleDismiss.bind(self);
388
+ self.keyHandler = self.keyHandler.bind(self);
389
+ self.handleKnobs = self.handleKnobs.bind(self);
390
+
391
+ // generate markup
392
+ initCallback(self);
393
+
394
+ const { parent } = self;
395
+ // set main elements
396
+ /** @type {HTMLElement} */
397
+ // @ts-ignore
398
+ self.pickerToggle = querySelector('.picker-toggle', parent);
399
+ /** @type {HTMLElement} */
400
+ // @ts-ignore
401
+ self.menuToggle = querySelector('.menu-toggle', parent);
402
+ /** @type {HTMLElement} */
403
+ // @ts-ignore
404
+ self.colorMenu = querySelector('.color-dropdown.menu', parent);
405
+ /** @type {HTMLElement} */
406
+ // @ts-ignore
407
+ self.colorPicker = querySelector('.color-dropdown.picker', parent);
408
+ /** @type {HTMLElement} */
409
+ // @ts-ignore
410
+ self.controls = querySelector('.color-controls', parent);
411
+ /** @type {HTMLInputElement[]} */
412
+ // @ts-ignore
413
+ self.inputs = [...querySelectorAll('.color-input', parent)];
414
+ /** @type {(HTMLElement)[]} */
415
+ // @ts-ignore
416
+ self.controlKnobs = [...querySelectorAll('.knob', parent)];
417
+ /** @type {HTMLCanvasElement[]} */
418
+ // @ts-ignore
419
+ self.visuals = [...querySelectorAll('canvas', self.controls)];
420
+ /** @type {HTMLLabelElement[]} */
421
+ // @ts-ignore
422
+ self.knobLabels = [...querySelectorAll('.color-label', parent)];
423
+ /** @type {HTMLLabelElement} */
424
+ // @ts-ignore
425
+ self.appearance = querySelector('.color-appearance', parent);
426
+
427
+ const [v1, v2, v3] = self.visuals;
428
+ // set dimensions
429
+ /** @type {number} */
430
+ self.width1 = v1.width;
431
+ /** @type {number} */
432
+ self.height1 = v1.height;
433
+ /** @type {number} */
434
+ self.width2 = v2.width;
435
+ /** @type {number} */
436
+ self.height2 = v2.height;
437
+ // set main controls
438
+ /** @type {*} */
439
+ self.ctx1 = v1.getContext('2d');
440
+ /** @type {*} */
441
+ self.ctx2 = v2.getContext('2d');
442
+ self.ctx1.rect(0, 0, self.width1, self.height1);
443
+ self.ctx2.rect(0, 0, self.width2, self.height2);
444
+
445
+ /** @type {number} */
446
+ self.width3 = 0;
447
+ /** @type {number} */
448
+ self.height3 = 0;
449
+
450
+ // set alpha control except hex
451
+ if (self.format !== 'hex') {
452
+ self.width3 = v3.width;
453
+ self.height3 = v3.height;
454
+ /** @type {*} */
455
+ this.ctx3 = v3.getContext('2d');
456
+ self.ctx3.rect(0, 0, self.width3, self.height3);
457
+ }
458
+
459
+ // update color picker controls, inputs and visuals
460
+ this.setControlPositions();
461
+ this.setColorAppearence();
462
+ // don't trigger change at initialization
463
+ this.updateInputs(true);
464
+ this.updateControls();
465
+ this.updateVisuals();
466
+ // add main events listeners
467
+ toggleEvents(self, true);
468
+
469
+ // set component data
470
+ Data.set(input, colorPickerString, self);
471
+ }
472
+
473
+ /** Returns the current color value */
474
+ get value() { return this.input.value; }
475
+
476
+ /**
477
+ * Sets a new color value.
478
+ * @param {string} v new color value
479
+ */
480
+ set value(v) { this.input.value = v; }
481
+
482
+ /** Check if the input is required to have a valid value. */
483
+ get required() { return hasAttribute(this.input, 'required'); }
484
+
485
+ /**
486
+ * Returns the colour format.
487
+ * @returns {CP.ColorFormats | string}
488
+ */
489
+ get format() { return getAttribute(this.input, 'format') || 'hex'; }
490
+
491
+ /** Returns the input name. */
492
+ get name() { return getAttribute(this.input, 'name'); }
493
+
494
+ /**
495
+ * Returns the label associated to the input.
496
+ * @returns {HTMLLabelElement?}
497
+ */
498
+ // @ts-ignore
499
+ get label() { return querySelector(`[for="${this.input.id}"]`); }
500
+
501
+ /** Check if the color presets include any non-color. */
502
+ get includeNonColor() {
503
+ return this.keywords instanceof Array
504
+ && this.keywords.some((x) => nonColors.includes(x));
505
+ }
506
+
507
+ /** Returns hexadecimal value of the current color. */
508
+ get hex() { return this.color.toHex(); }
509
+
510
+ /** Returns the current color value in {h,s,v,a} object format. */
511
+ get hsv() { return this.color.toHsv(); }
512
+
513
+ /** Returns the current color value in {h,s,l,a} object format. */
514
+ get hsl() { return this.color.toHsl(); }
515
+
516
+ /** Returns the current color value in {r,g,b,a} object format. */
517
+ get rgb() { return this.color.toRgb(); }
518
+
519
+ /** Returns the current color brightness. */
520
+ get brightness() { return this.color.brightness; }
521
+
522
+ /** Returns the current color luminance. */
523
+ get luminance() { return this.color.luminance; }
524
+
525
+ /** Checks if the current colour requires a light text color. */
526
+ get isDark() {
527
+ const { rgb, brightness } = this;
528
+ return brightness < 120 && rgb.a > 0.33;
529
+ }
530
+
531
+ /** Checks if the current input value is a valid color. */
532
+ get isValid() {
533
+ const inputValue = this.input.value;
534
+ return inputValue !== '' && new Color(inputValue).isValid;
535
+ }
536
+
537
+ /** Updates `ColorPicker` visuals. */
538
+ updateVisuals() {
539
+ const self = this;
540
+ const {
541
+ color, format, controlPositions,
542
+ width1, width2, width3,
543
+ height1, height2, height3,
544
+ ctx1, ctx2, ctx3,
545
+ } = self;
546
+ const { r, g, b } = color;
547
+
548
+ if (format !== 'hsl') {
549
+ const hue = Math.round((controlPositions.c2y / height2) * 360);
550
+ ctx1.fillStyle = new Color(`hsl(${hue},100%,50%})`).toRgbString();
551
+ ctx1.fillRect(0, 0, width1, height1);
552
+
553
+ const whiteGrad = ctx2.createLinearGradient(0, 0, width1, 0);
554
+ whiteGrad.addColorStop(0, 'rgba(255,255,255,1)');
555
+ whiteGrad.addColorStop(1, 'rgba(255,255,255,0)');
556
+ ctx1.fillStyle = whiteGrad;
557
+ ctx1.fillRect(0, 0, width1, height1);
558
+
559
+ const blackGrad = ctx2.createLinearGradient(0, 0, 0, height1);
560
+ blackGrad.addColorStop(0, 'rgba(0,0,0,0)');
561
+ blackGrad.addColorStop(1, 'rgba(0,0,0,1)');
562
+ ctx1.fillStyle = blackGrad;
563
+ ctx1.fillRect(0, 0, width1, height1);
564
+
565
+ const hueGrad = ctx2.createLinearGradient(0, 0, 0, height1);
566
+ hueGrad.addColorStop(0, 'rgba(255,0,0,1)');
567
+ hueGrad.addColorStop(0.17, 'rgba(255,255,0,1)');
568
+ hueGrad.addColorStop(0.34, 'rgba(0,255,0,1)');
569
+ hueGrad.addColorStop(0.51, 'rgba(0,255,255,1)');
570
+ hueGrad.addColorStop(0.68, 'rgba(0,0,255,1)');
571
+ hueGrad.addColorStop(0.85, 'rgba(255,0,255,1)');
572
+ hueGrad.addColorStop(1, 'rgba(255,0,0,1)');
573
+ ctx2.fillStyle = hueGrad;
574
+ ctx2.fillRect(0, 0, width2, height2);
575
+ } else {
576
+ const hueGrad = ctx1.createLinearGradient(0, 0, width1, 0);
577
+ const saturation = Math.round((1 - controlPositions.c2y / height2) * 100);
578
+
579
+ hueGrad.addColorStop(0, new Color('rgba(255,0,0,1)').desaturate(100 - saturation).toRgbString());
580
+ hueGrad.addColorStop(0.17, new Color('rgba(255,255,0,1)').desaturate(100 - saturation).toRgbString());
581
+ hueGrad.addColorStop(0.34, new Color('rgba(0,255,0,1)').desaturate(100 - saturation).toRgbString());
582
+ hueGrad.addColorStop(0.51, new Color('rgba(0,255,255,1)').desaturate(100 - saturation).toRgbString());
583
+ hueGrad.addColorStop(0.68, new Color('rgba(0,0,255,1)').desaturate(100 - saturation).toRgbString());
584
+ hueGrad.addColorStop(0.85, new Color('rgba(255,0,255,1)').desaturate(100 - saturation).toRgbString());
585
+ hueGrad.addColorStop(1, new Color('rgba(255,0,0,1)').desaturate(100 - saturation).toRgbString());
586
+
587
+ ctx1.fillStyle = hueGrad;
588
+ ctx1.fillRect(0, 0, width1, height1);
589
+
590
+ const whiteGrad = ctx1.createLinearGradient(0, 0, 0, height1);
591
+ whiteGrad.addColorStop(0, 'rgba(255,255,255,1)');
592
+ whiteGrad.addColorStop(0.5, 'rgba(255,255,255,0)');
593
+ ctx1.fillStyle = whiteGrad;
594
+ ctx1.fillRect(0, 0, width1, height1);
595
+
596
+ const blackGrad = ctx1.createLinearGradient(0, 0, 0, height1);
597
+ blackGrad.addColorStop(0.5, 'rgba(0,0,0,0)');
598
+ blackGrad.addColorStop(1, 'rgba(0,0,0,1)');
599
+ ctx1.fillStyle = blackGrad;
600
+ ctx1.fillRect(0, 0, width1, height1);
601
+
602
+ const saturationGrad = ctx2.createLinearGradient(0, 0, 0, height2);
603
+ const incolor = color.clone().greyscale().toRgb();
604
+
605
+ saturationGrad.addColorStop(0, `rgba(${r},${g},${b},1)`);
606
+ saturationGrad.addColorStop(1, `rgba(${incolor.r},${incolor.g},${incolor.b},1)`);
607
+
608
+ ctx2.fillStyle = saturationGrad;
609
+ ctx2.fillRect(0, 0, width3, height3);
610
+ }
611
+
612
+ if (format !== 'hex') {
613
+ ctx3.clearRect(0, 0, width3, height3);
614
+ const alphaGrad = ctx3.createLinearGradient(0, 0, 0, height3);
615
+ alphaGrad.addColorStop(0, `rgba(${r},${g},${b},1)`);
616
+ alphaGrad.addColorStop(1, `rgba(${r},${g},${b},0)`);
617
+ ctx3.fillStyle = alphaGrad;
618
+ ctx3.fillRect(0, 0, width3, height3);
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Handles the `focusout` listener of the `ColorPicker`.
624
+ * @param {FocusEvent} e
625
+ * @this {ColorPicker}
626
+ */
627
+ handleFocusOut({ relatedTarget }) {
628
+ // @ts-ignore
629
+ if (relatedTarget && !this.parent.contains(relatedTarget)) {
630
+ this.hide(true);
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Handles the `focusout` listener of the `ColorPicker`.
636
+ * @param {KeyboardEvent} e
637
+ * @this {ColorPicker}
638
+ */
639
+ handleDismiss({ code }) {
640
+ const self = this;
641
+ if (self.isOpen && code === keyEscape) {
642
+ self.hide();
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Handles the `ColorPicker` scroll listener when open.
648
+ * @param {Event} e
649
+ * @this {ColorPicker}
650
+ */
651
+ handleScroll(e) {
652
+ const self = this;
653
+ /** @type {*} */
654
+ const { activeElement } = document;
655
+
656
+ if ((isMobile && self.dragElement)
657
+ || (activeElement && self.controlKnobs.includes(activeElement))) {
658
+ e.stopPropagation();
659
+ e.preventDefault();
660
+ }
661
+
662
+ self.updateDropdownPosition();
663
+ }
664
+
665
+ /**
666
+ * Handles all `ColorPicker` click listeners.
667
+ * @param {KeyboardEvent} e
668
+ * @this {ColorPicker}
669
+ */
670
+ menuKeyHandler(e) {
671
+ const { target, code } = e;
672
+
673
+ if ([keyArrowDown, keyArrowUp].includes(code)) {
674
+ e.preventDefault();
675
+ } else if ([keyEnter, keySpace].includes(code)) {
676
+ this.menuClickHandler({ target });
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Handles all `ColorPicker` click listeners.
682
+ * @param {Partial<Event>} e
683
+ * @this {ColorPicker}
684
+ */
685
+ menuClickHandler(e) {
686
+ const self = this;
687
+ /** @type {*} */
688
+ const { target } = e;
689
+ const { format } = self;
690
+ const newOption = (getAttribute(target, 'data-value') || '').trim();
691
+ const currentActive = self.colorMenu.querySelector('li.active');
692
+ const newColor = nonColors.includes(newOption) ? 'white' : newOption;
693
+ self.color = new Color(newColor, { format });
694
+ self.setControlPositions();
695
+ self.setColorAppearence();
696
+ self.updateInputs(true);
697
+ self.updateControls();
698
+ self.updateVisuals();
699
+
700
+ if (currentActive) {
701
+ removeClass(currentActive, 'active');
702
+ removeAttribute(currentActive, ariaSelected);
703
+ }
704
+
705
+ if (currentActive !== target) {
706
+ addClass(target, 'active');
707
+ setAttribute(target, ariaSelected, 'true');
708
+
709
+ if (nonColors.includes(newOption)) {
710
+ self.value = newOption;
711
+ firePickerChange(self);
712
+ }
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Handles the `ColorPicker` touchstart / mousedown events listeners.
718
+ * @param {TouchEvent} e
719
+ * @this {ColorPicker}
720
+ */
721
+ pointerDown(e) {
722
+ const self = this;
723
+ const {
724
+ // @ts-ignore
725
+ type, target, touches, pageX, pageY,
726
+ } = e;
727
+ const { visuals, controlKnobs, format } = self;
728
+ const [v1, v2, v3] = visuals;
729
+ const [c1, c2, c3] = controlKnobs;
730
+ /** @type {HTMLCanvasElement} */
731
+ // @ts-ignore
732
+ const visual = target.tagName === 'canvas' // @ts-ignore
733
+ ? target : querySelector('canvas', target.parentElement);
734
+ const visualRect = getBoundingClientRect(visual);
735
+ const X = type === 'touchstart' ? touches[0].pageX : pageX;
736
+ const Y = type === 'touchstart' ? touches[0].pageY : pageY;
737
+ const offsetX = X - window.pageXOffset - visualRect.left;
738
+ const offsetY = Y - window.pageYOffset - visualRect.top;
739
+
740
+ if (target === v1 || target === c1) {
741
+ self.dragElement = visual;
742
+ self.changeControl1({ offsetX, offsetY });
743
+ } else if (target === v2 || target === c2) {
744
+ self.dragElement = visual;
745
+ self.changeControl2({ offsetY });
746
+ } else if (format !== 'hex' && (target === v3 || target === c3)) {
747
+ self.dragElement = visual;
748
+ self.changeAlpha({ offsetY });
749
+ }
750
+ e.preventDefault();
751
+ }
752
+
753
+ /**
754
+ * Handles the `ColorPicker` touchend / mouseup events listeners.
755
+ * @param {TouchEvent} e
756
+ * @this {ColorPicker}
757
+ */
758
+ pointerUp({ target }) {
759
+ const self = this;
760
+ const selection = document.getSelection();
761
+ // @ts-ignore
762
+ if (!self.dragElement && !selection.toString().length
763
+ // @ts-ignore
764
+ && !self.parent.contains(target)) {
765
+ self.hide();
766
+ }
767
+
768
+ self.dragElement = null;
769
+ }
770
+
771
+ /**
772
+ * Handles the `ColorPicker` touchmove / mousemove events listeners.
773
+ * @param {TouchEvent} e
774
+ */
775
+ pointerMove(e) {
776
+ const self = this;
777
+ const { dragElement, visuals, format } = self;
778
+ const [v1, v2, v3] = visuals;
779
+ const {
780
+ // @ts-ignore
781
+ type, touches, pageX, pageY,
782
+ } = e;
783
+
784
+ if (!dragElement) return;
785
+
786
+ const controlRect = getBoundingClientRect(dragElement);
787
+ const X = type === 'touchmove' ? touches[0].pageX : pageX;
788
+ const Y = type === 'touchmove' ? touches[0].pageY : pageY;
789
+ const offsetX = X - window.pageXOffset - controlRect.left;
790
+ const offsetY = Y - window.pageYOffset - controlRect.top;
791
+
792
+ if (dragElement === v1) {
793
+ self.changeControl1({ offsetX, offsetY });
794
+ }
795
+
796
+ if (dragElement === v2) {
797
+ self.changeControl2({ offsetY });
798
+ }
799
+
800
+ if (dragElement === v3 && format !== 'hex') {
801
+ self.changeAlpha({ offsetY });
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Handles the `ColorPicker` events listeners associated with the color knobs.
807
+ * @param {KeyboardEvent} e
808
+ */
809
+ handleKnobs(e) {
810
+ const { target, code } = e;
811
+ const self = this;
812
+
813
+ // only react to arrow buttons
814
+ if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
815
+ e.preventDefault();
816
+
817
+ const { activeElement } = document;
818
+ const { controlKnobs } = self;
819
+ const currentKnob = controlKnobs.find((x) => x === activeElement);
820
+ const [c1, c2, c3] = controlKnobs;
821
+
822
+ if (currentKnob) {
823
+ let offsetX = 0;
824
+ let offsetY = 0;
825
+ if (target === c1) {
826
+ if ([keyArrowLeft, keyArrowRight].includes(code)) {
827
+ self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
828
+ } else if ([keyArrowUp, keyArrowDown].includes(code)) {
829
+ self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
830
+ }
831
+
832
+ offsetX = self.controlPositions.c1x;
833
+ offsetY = self.controlPositions.c1y;
834
+ self.changeControl1({ offsetX, offsetY });
835
+ } else if (target === c2) {
836
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
837
+ offsetY = self.controlPositions.c2y;
838
+ self.changeControl2({ offsetY });
839
+ } else if (target === c3) {
840
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
841
+ offsetY = self.controlPositions.c3y;
842
+ self.changeAlpha({ offsetY });
843
+ }
844
+
845
+ self.setColorAppearence();
846
+ self.updateInputs();
847
+ self.updateControls();
848
+ self.updateVisuals();
849
+ self.handleScroll(e);
850
+ }
851
+ }
852
+
853
+ /** Handles the event listeners of the color form. */
854
+ changeHandler() {
855
+ const self = this;
856
+ let colorSource;
857
+ /** @type {HTMLInputElement} */
858
+ // @ts-ignore
859
+ const { activeElement } = document;
860
+ const {
861
+ inputs, format, value: currentValue, input,
862
+ } = self;
863
+ const [i1, i2, i3, i4] = inputs;
864
+ const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
865
+
866
+ if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
867
+ if (activeElement === input) {
868
+ if (isNonColorValue) {
869
+ colorSource = 'white';
870
+ } else {
871
+ colorSource = currentValue;
872
+ }
873
+ } else if (format === 'hex') {
874
+ colorSource = i1.value;
875
+ } else if (format === 'hsl') {
876
+ colorSource = `hsla(${i1.value},${i2.value}%,${i3.value}%,${i4.value})`;
877
+ } else {
878
+ colorSource = `rgba(${inputs.map((x) => x.value).join(',')})`;
879
+ }
880
+
881
+ self.color = new Color(colorSource, { format });
882
+ self.setControlPositions();
883
+ self.setColorAppearence();
884
+ self.updateInputs();
885
+ self.updateControls();
886
+ self.updateVisuals();
887
+
888
+ // set non-color keyword
889
+ if (activeElement === input && isNonColorValue) {
890
+ self.value = currentValue;
891
+ }
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Updates `ColorPicker` first control:
897
+ * * `lightness` and `saturation` for HEX/RGB;
898
+ * * `lightness` and `hue` for HSL.
899
+ *
900
+ * @param {Record<string, number>} offsets
901
+ */
902
+ changeControl1(offsets) {
903
+ const self = this;
904
+ let [offsetX, offsetY] = [0, 0];
905
+ const { offsetX: X, offsetY: Y } = offsets;
906
+ const {
907
+ format, controlPositions,
908
+ height1, height2, height3, width1,
909
+ } = self;
910
+
911
+ if (X > width1) {
912
+ offsetX = width1;
913
+ } else if (X >= 0) {
914
+ offsetX = X;
915
+ }
916
+
917
+ if (Y > height1) {
918
+ offsetY = height1;
919
+ } else if (Y >= 0) {
920
+ offsetY = Y;
921
+ }
922
+
923
+ const hue = format !== 'hsl'
924
+ ? Math.round((controlPositions.c2y / height2) * 360)
925
+ : Math.round((offsetX / width1) * 360);
926
+
927
+ const saturation = format !== 'hsl'
928
+ ? Math.round((offsetX / width1) * 100)
929
+ : Math.round((1 - controlPositions.c2y / height2) * 100);
930
+
931
+ const lightness = Math.round((1 - offsetY / height1) * 100);
932
+ const alpha = format !== 'hex' ? Math.round((1 - controlPositions.c3y / height3) * 100) / 100 : 1;
933
+ const tempFormat = format !== 'hsl' ? 'hsva' : 'hsla';
934
+
935
+ // new color
936
+ self.color = new Color(`${tempFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
937
+ // new positions
938
+ self.controlPositions.c1x = offsetX;
939
+ self.controlPositions.c1y = offsetY;
940
+
941
+ // update color picker
942
+ self.setColorAppearence();
943
+ self.updateInputs();
944
+ self.updateControls();
945
+ self.updateVisuals();
946
+ }
947
+
948
+ /**
949
+ * Updates `ColorPicker` second control:
950
+ * * `hue` for HEX/RGB;
951
+ * * `saturation` for HSL.
952
+ *
953
+ * @param {Record<string, number>} offset
954
+ */
955
+ changeControl2(offset) {
956
+ const self = this;
957
+ const { offsetY: Y } = offset;
958
+ const {
959
+ format, width1, height1, height2, height3, controlPositions,
960
+ } = self;
961
+ let offsetY = 0;
962
+
963
+ if (Y > height2) {
964
+ offsetY = height2;
965
+ } else if (Y >= 0) {
966
+ offsetY = Y;
967
+ }
968
+
969
+ const hue = format !== 'hsl' ? Math.round((offsetY / height2) * 360) : Math.round((controlPositions.c1x / width1) * 360);
970
+ const saturation = format !== 'hsl' ? Math.round((controlPositions.c1x / width1) * 100) : Math.round((1 - offsetY / height2) * 100);
971
+ const lightness = Math.round((1 - controlPositions.c1y / height1) * 100);
972
+ const alpha = format !== 'hex' ? Math.round((1 - controlPositions.c3y / height3) * 100) / 100 : 1;
973
+ const colorFormat = format !== 'hsl' ? 'hsva' : 'hsla';
974
+
975
+ // new color
976
+ self.color = new Color(`${colorFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
977
+ // new position
978
+ self.controlPositions.c2y = offsetY;
979
+ // update color picker
980
+ self.setColorAppearence();
981
+ self.updateInputs();
982
+ self.updateControls();
983
+ self.updateVisuals();
984
+ }
985
+
986
+ /**
987
+ * Updates `ColorPicker` last control,
988
+ * the `alpha` channel for RGB/HSL.
989
+ *
990
+ * @param {Record<string, number>} offset
991
+ */
992
+ changeAlpha(offset) {
993
+ const self = this;
994
+ const { height3 } = self;
995
+ const { offsetY: Y } = offset;
996
+ let offsetY = 0;
997
+
998
+ if (Y > height3) {
999
+ offsetY = height3;
1000
+ } else if (Y >= 0) {
1001
+ offsetY = Y;
1002
+ }
1003
+
1004
+ // update color alpha
1005
+ const alpha = Math.round((1 - offsetY / height3) * 100);
1006
+ self.color.setAlpha(alpha / 100);
1007
+ // update position
1008
+ self.controlPositions.c3y = offsetY;
1009
+ // update color picker
1010
+ self.updateInputs();
1011
+ self.updateControls();
1012
+ // alpha?
1013
+ self.updateVisuals();
1014
+ }
1015
+
1016
+ /** Update opened dropdown position on scroll. */
1017
+ updateDropdownPosition() {
1018
+ const self = this;
1019
+ const { input, colorPicker, colorMenu } = self;
1020
+ const elRect = getBoundingClientRect(input);
1021
+ const { offsetHeight: elHeight } = input;
1022
+ const windowHeight = document.documentElement.clientHeight;
1023
+ const isPicker = classToggle(colorPicker, true);
1024
+ const dropdown = isPicker ? colorPicker : colorMenu;
1025
+ const { offsetHeight: dropHeight } = dropdown;
1026
+ const distanceBottom = windowHeight - elRect.bottom;
1027
+ const distanceTop = elRect.top;
1028
+ const bottomExceed = elRect.top + dropHeight + elHeight > windowHeight; // show
1029
+ const topExceed = elRect.top - dropHeight < 0; // show-top
1030
+
1031
+ if (hasClass(dropdown, 'show') && distanceBottom < distanceTop && bottomExceed) {
1032
+ removeClass(dropdown, 'show');
1033
+ addClass(dropdown, 'show-top');
1034
+ }
1035
+ if (hasClass(dropdown, 'show-top') && distanceBottom > distanceTop && topExceed) {
1036
+ removeClass(dropdown, 'show-top');
1037
+ addClass(dropdown, 'show');
1038
+ }
1039
+ }
1040
+
1041
+ /** Update control knobs' positions. */
1042
+ setControlPositions() {
1043
+ const self = this;
1044
+ const {
1045
+ hsv, hsl, format, height1, height2, height3, width1,
1046
+ } = self;
1047
+ const hue = hsl.h;
1048
+ const saturation = format !== 'hsl' ? hsv.s : hsl.s;
1049
+ const lightness = format !== 'hsl' ? hsv.v : hsl.l;
1050
+ const alpha = hsv.a;
1051
+
1052
+ self.controlPositions.c1x = format !== 'hsl' ? saturation * width1 : (hue / 360) * width1;
1053
+ self.controlPositions.c1y = (1 - lightness) * height1;
1054
+ self.controlPositions.c2y = format !== 'hsl' ? (hue / 360) * height2 : (1 - saturation) * height2;
1055
+
1056
+ if (format !== 'hex') {
1057
+ self.controlPositions.c3y = (1 - alpha) * height3;
1058
+ }
1059
+ }
1060
+
1061
+ /** Update the visual appearance label. */
1062
+ setColorAppearence() {
1063
+ const self = this;
1064
+ const {
1065
+ componentLabels, colorLabels, hsl, hsv, hex, format, knobLabels,
1066
+ } = self;
1067
+ const {
1068
+ lightnessLabel, saturationLabel, hueLabel, alphaLabel, appearanceLabel, hexLabel,
1069
+ } = componentLabels;
1070
+ let { requiredLabel } = componentLabels;
1071
+ const [knob1Lbl, knob2Lbl, knob3Lbl] = knobLabels;
1072
+ const hue = Math.round(hsl.h);
1073
+ const alpha = hsv.a;
1074
+ const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
1075
+ const saturation = Math.round(saturationSource * 100);
1076
+ const lightness = Math.round(hsl.l * 100);
1077
+ const hsvl = hsv.v * 100;
1078
+ let colorName;
1079
+
1080
+ // determine color appearance
1081
+ if (lightness === 100 && saturation === 0) {
1082
+ colorName = colorLabels.white;
1083
+ } else if (lightness === 0) {
1084
+ colorName = colorLabels.black;
1085
+ } else if (saturation === 0) {
1086
+ colorName = colorLabels.grey;
1087
+ } else if (hue < 15 || hue >= 345) {
1088
+ colorName = colorLabels.red;
1089
+ } else if (hue >= 15 && hue < 45) {
1090
+ colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
1091
+ } else if (hue >= 45 && hue < 75) {
1092
+ const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
1093
+ const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
1094
+ colorName = isGold ? colorLabels.gold : colorLabels.yellow;
1095
+ colorName = isOlive ? colorLabels.olive : colorName;
1096
+ } else if (hue >= 75 && hue < 155) {
1097
+ colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
1098
+ } else if (hue >= 155 && hue < 175) {
1099
+ colorName = colorLabels.teal;
1100
+ } else if (hue >= 175 && hue < 195) {
1101
+ colorName = colorLabels.cyan;
1102
+ } else if (hue >= 195 && hue < 255) {
1103
+ colorName = colorLabels.blue;
1104
+ } else if (hue >= 255 && hue < 270) {
1105
+ colorName = colorLabels.violet;
1106
+ } else if (hue >= 270 && hue < 295) {
1107
+ colorName = colorLabels.magenta;
1108
+ } else if (hue >= 295 && hue < 345) {
1109
+ colorName = colorLabels.pink;
1110
+ }
1111
+
1112
+ if (format === 'hsl') {
1113
+ knob1Lbl.innerText = `${hueLabel}: ${hue}°. ${lightnessLabel}: ${lightness}%`;
1114
+ knob2Lbl.innerText = `${saturationLabel}: ${saturation}%`;
1115
+ } else {
1116
+ knob1Lbl.innerText = `${lightnessLabel}: ${lightness}%. ${saturationLabel}: ${saturation}%`;
1117
+ knob2Lbl.innerText = `${hueLabel}: ${hue}°`;
1118
+ }
1119
+
1120
+ if (format !== 'hex') {
1121
+ const alphaValue = Math.round(alpha * 100);
1122
+ knob3Lbl.innerText = `${alphaLabel}: ${alphaValue}%`;
1123
+ }
1124
+
1125
+ // update color labels
1126
+ self.appearance.innerText = `${appearanceLabel}: ${colorName}.`;
1127
+ const colorLabel = format === 'hex'
1128
+ ? `${hexLabel} ${hex.split('').join(' ')}.`
1129
+ : self.value.toUpperCase();
1130
+
1131
+ if (self.label) {
1132
+ const fieldLabel = self.label.innerText.replace('*', '').trim();
1133
+ /** @type {HTMLSpanElement} */
1134
+ // @ts-ignore
1135
+ const [pickerBtnSpan] = self.pickerToggle.children;
1136
+ requiredLabel = self.required ? ` ${requiredLabel}` : '';
1137
+ pickerBtnSpan.innerText = `${fieldLabel}: ${colorLabel}${requiredLabel}`;
1138
+ }
1139
+ }
1140
+
1141
+ /** Updates the control knobs positions. */
1142
+ updateControls() {
1143
+ const { format, controlKnobs, controlPositions } = this;
1144
+ const [control1, control2, control3] = controlKnobs;
1145
+ control1.style.transform = `translate3d(${controlPositions.c1x - 3}px,${controlPositions.c1y - 3}px,0)`;
1146
+ control2.style.transform = `translate3d(0,${controlPositions.c2y - 3}px,0)`;
1147
+
1148
+ if (format !== 'hex') {
1149
+ control3.style.transform = `translate3d(0,${controlPositions.c3y - 3}px,0)`;
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Update all color form inputs.
1155
+ * @param {boolean=} isPrevented when `true`, the component original event is prevented
1156
+ */
1157
+ updateInputs(isPrevented) {
1158
+ const self = this;
1159
+ const {
1160
+ value: oldColor, rgb, hsl, hsv, format, parent, input, inputs,
1161
+ } = self;
1162
+ const [i1, i2, i3, i4] = inputs;
1163
+
1164
+ const alpha = hsl.a;
1165
+ const hue = Math.round(hsl.h);
1166
+ const saturation = Math.round(hsl.s * 100);
1167
+ const lightSource = format === 'hsl' ? hsl.l : hsv.v;
1168
+ const lightness = Math.round(lightSource * 100);
1169
+ let newColor;
1170
+
1171
+ if (format === 'hex') {
1172
+ newColor = self.color.toHexString();
1173
+ i1.value = self.hex;
1174
+ } else if (format === 'hsl') {
1175
+ newColor = self.color.toHslString();
1176
+ i1.value = `${hue}`;
1177
+ i2.value = `${saturation}`;
1178
+ i3.value = `${lightness}`;
1179
+ i4.value = `${alpha}`;
1180
+ } else if (format === 'rgb') {
1181
+ newColor = self.color.toRgbString();
1182
+ i1.value = `${rgb.r}`;
1183
+ i2.value = `${rgb.g}`;
1184
+ i3.value = `${rgb.b}`;
1185
+ i4.value = `${alpha}`;
1186
+ }
1187
+
1188
+ // update the color value
1189
+ self.value = `${newColor}`;
1190
+
1191
+ // update the input backgroundColor
1192
+ ObjectAssign(input.style, { backgroundColor: newColor });
1193
+
1194
+ // toggle dark/light classes will also style the placeholder
1195
+ // dark sets color white, light sets color black
1196
+ // isDark ? '#000' : '#fff'
1197
+ if (!self.isDark) {
1198
+ if (hasClass(parent, 'dark')) removeClass(parent, 'dark');
1199
+ if (!hasClass(parent, 'light')) addClass(parent, 'light');
1200
+ } else {
1201
+ if (hasClass(parent, 'light')) removeClass(parent, 'light');
1202
+ if (!hasClass(parent, 'dark')) addClass(parent, 'dark');
1203
+ }
1204
+
1205
+ // don't trigger the custom event unless it's really changed
1206
+ if (!isPrevented && newColor !== oldColor) {
1207
+ firePickerChange(self);
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Handles the `Space` and `Enter` keys inputs.
1213
+ * @param {KeyboardEvent} e
1214
+ * @this {ColorPicker}
1215
+ */
1216
+ keyHandler(e) {
1217
+ const self = this;
1218
+ const { menuToggle } = self;
1219
+ const { activeElement } = document;
1220
+ const { code } = e;
1221
+
1222
+ if ([keyEnter, keySpace].includes(code)) {
1223
+ if ((menuToggle && activeElement === menuToggle) || !activeElement) {
1224
+ e.preventDefault();
1225
+ if (!activeElement) {
1226
+ self.togglePicker(e);
1227
+ } else {
1228
+ self.toggleMenu();
1229
+ }
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ /**
1235
+ * Toggle the `ColorPicker` dropdown visibility.
1236
+ * @param {Event} e
1237
+ * @this {ColorPicker}
1238
+ */
1239
+ togglePicker(e) {
1240
+ e.preventDefault();
1241
+ const self = this;
1242
+ const pickerIsOpen = classToggle(self.colorPicker, true);
1243
+
1244
+ if (self.isOpen && pickerIsOpen) {
1245
+ self.hide(true);
1246
+ } else {
1247
+ self.showPicker();
1248
+ }
1249
+ }
1250
+
1251
+ /** Shows the `ColorPicker` dropdown. */
1252
+ showPicker() {
1253
+ const self = this;
1254
+ classToggle(self.colorMenu);
1255
+ addClass(self.colorPicker, 'show');
1256
+ self.input.focus();
1257
+ self.show();
1258
+ setAttribute(self.pickerToggle, ariaExpanded, 'true');
1259
+ }
1260
+
1261
+ /** Toggles the visibility of the `ColorPicker` presets menu. */
1262
+ toggleMenu() {
1263
+ const self = this;
1264
+ const menuIsOpen = classToggle(self.colorMenu, true);
1265
+
1266
+ if (self.isOpen && menuIsOpen) {
1267
+ self.hide(true);
1268
+ } else {
1269
+ showMenu(self);
1270
+ }
1271
+ }
1272
+
1273
+ /** Show the dropdown. */
1274
+ show() {
1275
+ const self = this;
1276
+ if (!self.isOpen) {
1277
+ addClass(self.parent, 'open');
1278
+ toggleEventsOnShown(self, true);
1279
+ self.updateDropdownPosition();
1280
+ self.isOpen = true;
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Hides the currently opened dropdown.
1286
+ * @param {boolean=} focusPrevented
1287
+ */
1288
+ hide(focusPrevented) {
1289
+ const self = this;
1290
+ if (self.isOpen) {
1291
+ const { pickerToggle, colorMenu } = self;
1292
+ toggleEventsOnShown(self);
1293
+
1294
+ removeClass(self.parent, 'open');
1295
+
1296
+ classToggle(self.colorPicker);
1297
+ setAttribute(pickerToggle, ariaExpanded, 'false');
1298
+
1299
+ if (colorMenu) {
1300
+ classToggle(colorMenu);
1301
+ setAttribute(self.menuToggle, ariaExpanded, 'false');
1302
+ }
1303
+
1304
+ if (!self.isValid) {
1305
+ self.value = self.color.toString();
1306
+ }
1307
+
1308
+ self.isOpen = false;
1309
+
1310
+ if (!focusPrevented) {
1311
+ pickerToggle.focus();
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ dispose() {
1317
+ const self = this;
1318
+ const { input, parent } = self;
1319
+ self.hide(true);
1320
+ toggleEvents(self);
1321
+ [...parent.children].forEach((el) => {
1322
+ if (el !== input) el.remove();
1323
+ });
1324
+ Data.remove(input, colorPickerString);
1325
+ }
1326
+ }
1327
+
1328
+ ObjectAssign(ColorPicker, {
1329
+ Color,
1330
+ getInstance: getColorPickerInstance,
1331
+ init: initColorPicker,
1332
+ selector: colorPickerSelector,
1333
+ });