@thednp/color-picker 0.0.1-alpha1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
});
|