@thednp/color-picker 0.0.1-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ });