@thednp/color-picker 0.0.1-alpha1 → 0.0.1-alpha2

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