@thednp/color-picker 0.0.1-alpha1 → 0.0.1

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