@thednp/color-picker 0.0.1-alpha1 → 0.0.1

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