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

Sign up to get free protection for your applications and to get access to all the features.
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,