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