@thednp/color-picker 1.0.1 → 2.0.0-alpha2
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/.eslintrc.cjs +199 -0
- package/.lgtm.yml +9 -0
- package/.prettierrc.json +15 -0
- package/.stylelintrc.json +236 -0
- package/LICENSE +0 -0
- package/README.md +55 -72
- package/compile.js +48 -0
- package/cypress/e2e/color-palette.cy.ts +128 -0
- package/cypress/e2e/color-picker.cy.ts +920 -0
- package/cypress/fixtures/colorNamesFrench.js +3 -0
- package/cypress/fixtures/componentLabelsFrench.js +21 -0
- package/cypress/fixtures/format.js +3 -0
- package/cypress/fixtures/getCEMarkup.js +29 -0
- package/cypress/fixtures/getMarkup.js +28 -0
- package/cypress/fixtures/getRandomInt.js +6 -0
- package/cypress/fixtures/sampleWebcolors.js +18 -0
- package/cypress/fixtures/testSample.js +8 -0
- package/cypress/plugins/esbuild-istanbul.ts +50 -0
- package/cypress/plugins/tsCompile.ts +34 -0
- package/cypress/support/commands.ts +0 -0
- package/cypress/support/e2e.ts +21 -0
- package/cypress/test.html +23 -0
- package/cypress.config.ts +29 -0
- package/dist/css/color-picker.css +16 -40
- package/dist/css/color-picker.min.css +2 -2
- package/dist/css/color-picker.rtl.css +16 -40
- package/dist/css/color-picker.rtl.min.css +2 -2
- package/dist/js/color-picker.cjs +8 -0
- package/dist/js/color-picker.cjs.map +1 -0
- package/dist/js/color-picker.d.ts +278 -0
- package/dist/js/color-picker.js +5 -3570
- package/dist/js/color-picker.js.map +1 -0
- package/dist/js/color-picker.mjs +2631 -0
- package/dist/js/color-picker.mjs.map +1 -0
- package/dts.config.ts +15 -0
- package/package.json +64 -74
- package/src/scss/_variables.scss +0 -1
- package/src/scss/color-picker.rtl.scss +4 -0
- package/src/scss/color-picker.scss +80 -40
- package/src/ts/colorPalette.ts +89 -0
- package/src/{js/color-picker.js → ts/index.ts} +489 -486
- package/src/ts/interface/colorPickerLabels.ts +20 -0
- package/src/ts/interface/colorPickerOptions.ts +11 -0
- package/src/ts/interface/paletteOptions.ts +6 -0
- package/src/ts/util/colorNames.ts +21 -0
- package/src/{js/util/colorPickerLabels.js → ts/util/colorPickerLabels.ts} +4 -2
- package/src/ts/util/getColorControls.ts +90 -0
- package/src/{js/util/getColorForm.js → ts/util/getColorForm.ts} +28 -18
- package/src/{js/util/getColorMenu.js → ts/util/getColorMenu.ts} +21 -30
- package/src/ts/util/isValidJSON.ts +19 -0
- package/src/{js/util/setMarkup.js → ts/util/setMarkup.ts} +57 -48
- package/src/{js/util/vHidden.js → ts/util/vHidden.ts} +0 -0
- package/tsconfig.json +29 -0
- package/vite.config.ts +34 -0
- package/dist/js/color-esm.js +0 -1164
- package/dist/js/color-esm.min.js +0 -2
- package/dist/js/color-palette-esm.js +0 -1235
- package/dist/js/color-palette-esm.min.js +0 -2
- package/dist/js/color-palette.js +0 -1243
- package/dist/js/color-palette.min.js +0 -2
- package/dist/js/color-picker-element-esm.js +0 -3718
- package/dist/js/color-picker-element-esm.min.js +0 -2
- package/dist/js/color-picker-element.js +0 -3726
- package/dist/js/color-picker-element.min.js +0 -2
- package/dist/js/color-picker-esm.js +0 -3565
- package/dist/js/color-picker-esm.min.js +0 -2
- package/dist/js/color-picker.min.js +0 -2
- package/dist/js/color.js +0 -1172
- package/dist/js/color.min.js +0 -2
- package/src/js/color-palette.js +0 -75
- package/src/js/color-picker-element.js +0 -107
- package/src/js/color.js +0 -1104
- package/src/js/index.js +0 -8
- package/src/js/util/colorNames.js +0 -6
- package/src/js/util/getColorControls.js +0 -103
- package/src/js/util/isValidJSON.js +0 -13
- package/src/js/util/nonColors.js +0 -5
- package/src/js/util/roundPart.js +0 -9
- package/src/js/util/setCSSProperties.js +0 -12
- package/src/js/util/tabindex.js +0 -3
- package/src/js/util/toggleCEAttr.js +0 -70
- package/src/js/util/version.js +0 -5
- package/src/js/version.js +0 -5
- package/types/cp.d.ts +0 -558
- package/types/index.d.ts +0 -44
- package/types/source/source.ts +0 -4
- package/types/source/types.d.ts +0 -92
@@ -1,3565 +0,0 @@
|
|
1
|
-
/*!
|
2
|
-
* ColorPicker v1.0.1 (http://thednp.github.io/color-picker)
|
3
|
-
* Copyright 2022 © thednp
|
4
|
-
* Licensed under MIT (https://github.com/thednp/color-picker/blob/master/LICENSE)
|
5
|
-
*/
|
6
|
-
/** @type {Record<string, any>} */
|
7
|
-
const EventRegistry = {};
|
8
|
-
|
9
|
-
/**
|
10
|
-
* The global event listener.
|
11
|
-
*
|
12
|
-
* @type {EventListener}
|
13
|
-
* @this {EventTarget}
|
14
|
-
*/
|
15
|
-
function globalListener(e) {
|
16
|
-
const that = this;
|
17
|
-
const { type } = e;
|
18
|
-
|
19
|
-
[...EventRegistry[type]].forEach((elementsMap) => {
|
20
|
-
const [element, listenersMap] = elementsMap;
|
21
|
-
/* istanbul ignore else */
|
22
|
-
if (element === that) {
|
23
|
-
[...listenersMap].forEach((listenerMap) => {
|
24
|
-
const [listener, options] = listenerMap;
|
25
|
-
listener.apply(element, [e]);
|
26
|
-
|
27
|
-
if (options && options.once) {
|
28
|
-
removeListener(element, type, listener, options);
|
29
|
-
}
|
30
|
-
});
|
31
|
-
}
|
32
|
-
});
|
33
|
-
}
|
34
|
-
|
35
|
-
/**
|
36
|
-
* Register a new listener with its options and attach the `globalListener`
|
37
|
-
* to the target if this is the first listener.
|
38
|
-
*
|
39
|
-
* @type {Listener.ListenerAction<EventTarget>}
|
40
|
-
*/
|
41
|
-
const addListener = (element, eventType, listener, options) => {
|
42
|
-
// get element listeners first
|
43
|
-
if (!EventRegistry[eventType]) {
|
44
|
-
EventRegistry[eventType] = new Map();
|
45
|
-
}
|
46
|
-
const oneEventMap = EventRegistry[eventType];
|
47
|
-
|
48
|
-
if (!oneEventMap.has(element)) {
|
49
|
-
oneEventMap.set(element, new Map());
|
50
|
-
}
|
51
|
-
const oneElementMap = oneEventMap.get(element);
|
52
|
-
|
53
|
-
// get listeners size
|
54
|
-
const { size } = oneElementMap;
|
55
|
-
|
56
|
-
// register listener with its options
|
57
|
-
oneElementMap.set(listener, options);
|
58
|
-
|
59
|
-
// add listener last
|
60
|
-
if (!size) {
|
61
|
-
element.addEventListener(eventType, globalListener, options);
|
62
|
-
}
|
63
|
-
};
|
64
|
-
|
65
|
-
/**
|
66
|
-
* Remove a listener from registry and detach the `globalListener`
|
67
|
-
* if no listeners are found in the registry.
|
68
|
-
*
|
69
|
-
* @type {Listener.ListenerAction<EventTarget>}
|
70
|
-
*/
|
71
|
-
const removeListener = (element, eventType, listener, options) => {
|
72
|
-
// get listener first
|
73
|
-
const oneEventMap = EventRegistry[eventType];
|
74
|
-
const oneElementMap = oneEventMap && oneEventMap.get(element);
|
75
|
-
const savedOptions = oneElementMap && oneElementMap.get(listener);
|
76
|
-
|
77
|
-
// also recover initial options
|
78
|
-
const { options: eventOptions } = savedOptions !== undefined
|
79
|
-
? savedOptions
|
80
|
-
: { options };
|
81
|
-
|
82
|
-
// unsubscribe second, remove from registry
|
83
|
-
if (oneElementMap && oneElementMap.has(listener)) oneElementMap.delete(listener);
|
84
|
-
if (oneEventMap && (!oneElementMap || !oneElementMap.size)) oneEventMap.delete(element);
|
85
|
-
if (!oneEventMap || !oneEventMap.size) delete EventRegistry[eventType];
|
86
|
-
|
87
|
-
// remove listener last
|
88
|
-
/* istanbul ignore else */
|
89
|
-
if (!oneElementMap || !oneElementMap.size) {
|
90
|
-
element.removeEventListener(eventType, globalListener, eventOptions);
|
91
|
-
}
|
92
|
-
};
|
93
|
-
|
94
|
-
/**
|
95
|
-
* A global namespace for aria-description.
|
96
|
-
* @type {string}
|
97
|
-
*/
|
98
|
-
const ariaDescription = 'aria-description';
|
99
|
-
|
100
|
-
/**
|
101
|
-
* A global namespace for aria-selected.
|
102
|
-
* @type {string}
|
103
|
-
*/
|
104
|
-
const ariaSelected = 'aria-selected';
|
105
|
-
|
106
|
-
/**
|
107
|
-
* A global namespace for aria-expanded.
|
108
|
-
* @type {string}
|
109
|
-
*/
|
110
|
-
const ariaExpanded = 'aria-expanded';
|
111
|
-
|
112
|
-
/**
|
113
|
-
* A global namespace for aria-valuetext.
|
114
|
-
* @type {string}
|
115
|
-
*/
|
116
|
-
const ariaValueText = 'aria-valuetext';
|
117
|
-
|
118
|
-
/**
|
119
|
-
* A global namespace for aria-valuenow.
|
120
|
-
* @type {string}
|
121
|
-
*/
|
122
|
-
const ariaValueNow = 'aria-valuenow';
|
123
|
-
|
124
|
-
/**
|
125
|
-
* A global namespace for `ArrowDown` key.
|
126
|
-
* @type {string} e.which = 40 equivalent
|
127
|
-
*/
|
128
|
-
const keyArrowDown = 'ArrowDown';
|
129
|
-
|
130
|
-
/**
|
131
|
-
* A global namespace for `ArrowUp` key.
|
132
|
-
* @type {string} e.which = 38 equivalent
|
133
|
-
*/
|
134
|
-
const keyArrowUp = 'ArrowUp';
|
135
|
-
|
136
|
-
/**
|
137
|
-
* A global namespace for `ArrowLeft` key.
|
138
|
-
* @type {string} e.which = 37 equivalent
|
139
|
-
*/
|
140
|
-
const keyArrowLeft = 'ArrowLeft';
|
141
|
-
|
142
|
-
/**
|
143
|
-
* A global namespace for `ArrowRight` key.
|
144
|
-
* @type {string} e.which = 39 equivalent
|
145
|
-
*/
|
146
|
-
const keyArrowRight = 'ArrowRight';
|
147
|
-
|
148
|
-
/**
|
149
|
-
* A global namespace for `Enter` key.
|
150
|
-
* @type {string} e.which = 13 equivalent
|
151
|
-
*/
|
152
|
-
const keyEnter = 'Enter';
|
153
|
-
|
154
|
-
/**
|
155
|
-
* A global namespace for `Space` key.
|
156
|
-
* @type {string} e.which = 32 equivalent
|
157
|
-
*/
|
158
|
-
const keySpace = 'Space';
|
159
|
-
|
160
|
-
/**
|
161
|
-
* A global namespace for `Escape` key.
|
162
|
-
* @type {string} e.which = 27 equivalent
|
163
|
-
*/
|
164
|
-
const keyEscape = 'Escape';
|
165
|
-
|
166
|
-
/**
|
167
|
-
* A global namespace for `focusin` event.
|
168
|
-
* @type {string}
|
169
|
-
*/
|
170
|
-
const focusinEvent = 'focusin';
|
171
|
-
|
172
|
-
/**
|
173
|
-
* A global namespace for `click` event.
|
174
|
-
* @type {string}
|
175
|
-
*/
|
176
|
-
const mouseclickEvent = 'click';
|
177
|
-
|
178
|
-
/**
|
179
|
-
* A global namespace for `keydown` event.
|
180
|
-
* @type {string}
|
181
|
-
*/
|
182
|
-
const keydownEvent = 'keydown';
|
183
|
-
|
184
|
-
/**
|
185
|
-
* A global namespace for `change` event.
|
186
|
-
* @type {string}
|
187
|
-
*/
|
188
|
-
const changeEvent = 'change';
|
189
|
-
|
190
|
-
/**
|
191
|
-
* A global namespace for `touchmove` event.
|
192
|
-
* @type {string}
|
193
|
-
*/
|
194
|
-
const touchmoveEvent = 'touchmove';
|
195
|
-
|
196
|
-
/**
|
197
|
-
* A global namespace for `pointerdown` event.
|
198
|
-
* @type {string}
|
199
|
-
*/
|
200
|
-
const pointerdownEvent = 'pointerdown';
|
201
|
-
|
202
|
-
/**
|
203
|
-
* A global namespace for `pointermove` event.
|
204
|
-
* @type {string}
|
205
|
-
*/
|
206
|
-
const pointermoveEvent = 'pointermove';
|
207
|
-
|
208
|
-
/**
|
209
|
-
* A global namespace for `pointerup` event.
|
210
|
-
* @type {string}
|
211
|
-
*/
|
212
|
-
const pointerupEvent = 'pointerup';
|
213
|
-
|
214
|
-
/**
|
215
|
-
* A global namespace for `scroll` event.
|
216
|
-
* @type {string}
|
217
|
-
*/
|
218
|
-
const scrollEvent = 'scroll';
|
219
|
-
|
220
|
-
/**
|
221
|
-
* A global namespace for `keyup` event.
|
222
|
-
* @type {string}
|
223
|
-
*/
|
224
|
-
const keyupEvent = 'keyup';
|
225
|
-
|
226
|
-
/**
|
227
|
-
* A global namespace for `resize` event.
|
228
|
-
* @type {string}
|
229
|
-
*/
|
230
|
-
const resizeEvent = 'resize';
|
231
|
-
|
232
|
-
/**
|
233
|
-
* A global namespace for `focusout` event.
|
234
|
-
* @type {string}
|
235
|
-
*/
|
236
|
-
const focusoutEvent = 'focusout';
|
237
|
-
|
238
|
-
/**
|
239
|
-
* Returns the `document` or the `#document` element.
|
240
|
-
* @see https://github.com/floating-ui/floating-ui
|
241
|
-
* @param {(Node | HTMLElement | Element | globalThis)=} node
|
242
|
-
* @returns {Document}
|
243
|
-
*/
|
244
|
-
function getDocument(node) {
|
245
|
-
if (node instanceof HTMLElement) return node.ownerDocument;
|
246
|
-
if (node instanceof Window) return node.document;
|
247
|
-
return window.document;
|
248
|
-
}
|
249
|
-
|
250
|
-
/**
|
251
|
-
* Returns the `document.documentElement` or the `<html>` element.
|
252
|
-
*
|
253
|
-
* @param {(Node | HTMLElement | Element | globalThis)=} node
|
254
|
-
* @returns {HTMLElement | HTMLHtmlElement}
|
255
|
-
*/
|
256
|
-
function getDocumentElement(node) {
|
257
|
-
return getDocument(node).documentElement;
|
258
|
-
}
|
259
|
-
|
260
|
-
/**
|
261
|
-
* Shortcut for `window.getComputedStyle(element).propertyName`
|
262
|
-
* static method.
|
263
|
-
*
|
264
|
-
* * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
|
265
|
-
* throws a `ReferenceError`.
|
266
|
-
*
|
267
|
-
* @param {HTMLElement | Element} element target
|
268
|
-
* @param {string} property the css property
|
269
|
-
* @return {string} the css property value
|
270
|
-
*/
|
271
|
-
function getElementStyle(element, property) {
|
272
|
-
const computedStyle = getComputedStyle(element);
|
273
|
-
|
274
|
-
// @ts-ignore -- must use camelcase strings,
|
275
|
-
// or non-camelcase strings with `getPropertyValue`
|
276
|
-
return property in computedStyle ? computedStyle[property] : '';
|
277
|
-
}
|
278
|
-
|
279
|
-
let elementUID = 0;
|
280
|
-
let elementMapUID = 0;
|
281
|
-
const elementIDMap = new Map();
|
282
|
-
|
283
|
-
/**
|
284
|
-
* Returns a unique identifier for popover, tooltip, scrollspy.
|
285
|
-
*
|
286
|
-
* @param {HTMLElement | Element} element target element
|
287
|
-
* @param {string=} key predefined key
|
288
|
-
* @returns {number} an existing or new unique ID
|
289
|
-
*/
|
290
|
-
function getUID(element, key) {
|
291
|
-
let result = key ? elementUID : elementMapUID;
|
292
|
-
|
293
|
-
if (key) {
|
294
|
-
const elID = getUID(element);
|
295
|
-
const elMap = elementIDMap.get(elID) || new Map();
|
296
|
-
if (!elementIDMap.has(elID)) {
|
297
|
-
elementIDMap.set(elID, elMap);
|
298
|
-
}
|
299
|
-
if (!elMap.has(key)) {
|
300
|
-
elMap.set(key, result);
|
301
|
-
elementUID += 1;
|
302
|
-
} else result = elMap.get(key);
|
303
|
-
} else {
|
304
|
-
const elkey = element.id || element;
|
305
|
-
|
306
|
-
if (!elementIDMap.has(elkey)) {
|
307
|
-
elementIDMap.set(elkey, result);
|
308
|
-
elementMapUID += 1;
|
309
|
-
} else result = elementIDMap.get(elkey);
|
310
|
-
}
|
311
|
-
return result;
|
312
|
-
}
|
313
|
-
|
314
|
-
/**
|
315
|
-
* Returns the bounding client rect of a target `HTMLElement`.
|
316
|
-
*
|
317
|
-
* @see https://github.com/floating-ui/floating-ui
|
318
|
-
*
|
319
|
-
* @param {HTMLElement | Element} element event.target
|
320
|
-
* @param {boolean=} includeScale when *true*, the target scale is also computed
|
321
|
-
* @returns {SHORTER.BoundingClientRect} the bounding client rect object
|
322
|
-
*/
|
323
|
-
function getBoundingClientRect(element, includeScale) {
|
324
|
-
const {
|
325
|
-
width, height, top, right, bottom, left,
|
326
|
-
} = element.getBoundingClientRect();
|
327
|
-
let scaleX = 1;
|
328
|
-
let scaleY = 1;
|
329
|
-
|
330
|
-
if (includeScale && element instanceof HTMLElement) {
|
331
|
-
const { offsetWidth, offsetHeight } = element;
|
332
|
-
scaleX = offsetWidth > 0 ? Math.round(width) / offsetWidth || 1 : 1;
|
333
|
-
scaleY = offsetHeight > 0 ? Math.round(height) / offsetHeight || 1 : 1;
|
334
|
-
}
|
335
|
-
|
336
|
-
return {
|
337
|
-
width: width / scaleX,
|
338
|
-
height: height / scaleY,
|
339
|
-
top: top / scaleY,
|
340
|
-
right: right / scaleX,
|
341
|
-
bottom: bottom / scaleY,
|
342
|
-
left: left / scaleX,
|
343
|
-
x: left / scaleX,
|
344
|
-
y: top / scaleY,
|
345
|
-
};
|
346
|
-
}
|
347
|
-
|
348
|
-
/**
|
349
|
-
* A global namespace for 'transitionDuration' string.
|
350
|
-
* @type {string}
|
351
|
-
*/
|
352
|
-
const transitionDuration = 'transitionDuration';
|
353
|
-
|
354
|
-
/**
|
355
|
-
* A global namespace for `transitionProperty` string for modern browsers.
|
356
|
-
*
|
357
|
-
* @type {string}
|
358
|
-
*/
|
359
|
-
const transitionProperty = 'transitionProperty';
|
360
|
-
|
361
|
-
/**
|
362
|
-
* Utility to get the computed `transitionDuration`
|
363
|
-
* from Element in miliseconds.
|
364
|
-
*
|
365
|
-
* @param {HTMLElement | Element} element target
|
366
|
-
* @return {number} the value in miliseconds
|
367
|
-
*/
|
368
|
-
function getElementTransitionDuration(element) {
|
369
|
-
const propertyValue = getElementStyle(element, transitionProperty);
|
370
|
-
const durationValue = getElementStyle(element, transitionDuration);
|
371
|
-
const durationScale = durationValue.includes('ms') ? 1 : 1000;
|
372
|
-
const duration = propertyValue && propertyValue !== 'none'
|
373
|
-
? parseFloat(durationValue) * durationScale : 0;
|
374
|
-
|
375
|
-
return !Number.isNaN(duration) ? duration : 0;
|
376
|
-
}
|
377
|
-
|
378
|
-
/**
|
379
|
-
* A global array of possible `ParentNode`.
|
380
|
-
*/
|
381
|
-
const parentNodes = [Document, Element, HTMLElement];
|
382
|
-
|
383
|
-
/**
|
384
|
-
* A global array with `Element` | `HTMLElement`.
|
385
|
-
*/
|
386
|
-
const elementNodes = [Element, HTMLElement];
|
387
|
-
|
388
|
-
/**
|
389
|
-
* Utility to check if target is typeof `HTMLElement`, `Element`, `Node`
|
390
|
-
* or find one that matches a selector.
|
391
|
-
*
|
392
|
-
* @param {HTMLElement | Element | string} selector the input selector or target element
|
393
|
-
* @param {(HTMLElement | Element | Document)=} parent optional node to look into
|
394
|
-
* @return {(HTMLElement | Element)?} the `HTMLElement` or `querySelector` result
|
395
|
-
*/
|
396
|
-
function querySelector(selector, parent) {
|
397
|
-
const lookUp = parentNodes.some((x) => parent instanceof x)
|
398
|
-
? parent : getDocument();
|
399
|
-
|
400
|
-
// @ts-ignore
|
401
|
-
return elementNodes.some((x) => selector instanceof x)
|
402
|
-
// @ts-ignore
|
403
|
-
? selector : lookUp.querySelector(selector);
|
404
|
-
}
|
405
|
-
|
406
|
-
/**
|
407
|
-
* Shortcut for `HTMLElement.closest` method which also works
|
408
|
-
* with children of `ShadowRoot`. The order of the parameters
|
409
|
-
* is intentional since they're both required.
|
410
|
-
*
|
411
|
-
* @see https://stackoverflow.com/q/54520554/803358
|
412
|
-
*
|
413
|
-
* @param {HTMLElement | Element} element Element to look into
|
414
|
-
* @param {string} selector the selector name
|
415
|
-
* @return {(HTMLElement | Element)?} the query result
|
416
|
-
*/
|
417
|
-
function closest(element, selector) {
|
418
|
-
return element ? (element.closest(selector)
|
419
|
-
// @ts-ignore -- break out of `ShadowRoot`
|
420
|
-
|| closest(element.getRootNode().host, selector)) : null;
|
421
|
-
}
|
422
|
-
|
423
|
-
/**
|
424
|
-
* Shortcut for `HTMLElement.getElementsByClassName` method. Some `Node` elements
|
425
|
-
* like `ShadowRoot` do not support `getElementsByClassName`.
|
426
|
-
*
|
427
|
-
* @param {string} selector the class name
|
428
|
-
* @param {(HTMLElement | Element | Document)=} parent optional Element to look into
|
429
|
-
* @return {HTMLCollectionOf<HTMLElement | Element>} the 'HTMLCollection'
|
430
|
-
*/
|
431
|
-
function getElementsByClassName(selector, parent) {
|
432
|
-
const lookUp = parent && parentNodes.some((x) => parent instanceof x)
|
433
|
-
? parent : getDocument();
|
434
|
-
return lookUp.getElementsByClassName(selector);
|
435
|
-
}
|
436
|
-
|
437
|
-
/**
|
438
|
-
* Shortcut for the `Element.dispatchEvent(Event)` method.
|
439
|
-
*
|
440
|
-
* @param {HTMLElement | Element} element is the target
|
441
|
-
* @param {Event} event is the `Event` object
|
442
|
-
*/
|
443
|
-
const dispatchEvent = (element, event) => element.dispatchEvent(event);
|
444
|
-
|
445
|
-
/**
|
446
|
-
* Shortcut for `Object.assign()` static method.
|
447
|
-
* @param {Record<string, any>} obj a target object
|
448
|
-
* @param {Record<string, any>} source a source object
|
449
|
-
*/
|
450
|
-
const ObjectAssign = (obj, source) => Object.assign(obj, source);
|
451
|
-
|
452
|
-
/** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
|
453
|
-
const componentData = new Map();
|
454
|
-
/**
|
455
|
-
* An interface for web components background data.
|
456
|
-
* @see https://github.com/thednp/bootstrap.native/blob/master/src/components/base-component.js
|
457
|
-
*/
|
458
|
-
const Data = {
|
459
|
-
/**
|
460
|
-
* Sets web components data.
|
461
|
-
* @param {HTMLElement | Element | string} target target element
|
462
|
-
* @param {string} component the component's name or a unique key
|
463
|
-
* @param {Record<string, any>} instance the component instance
|
464
|
-
*/
|
465
|
-
set: (target, component, instance) => {
|
466
|
-
const element = querySelector(target);
|
467
|
-
if (!element) return;
|
468
|
-
|
469
|
-
if (!componentData.has(component)) {
|
470
|
-
componentData.set(component, new Map());
|
471
|
-
}
|
472
|
-
|
473
|
-
const instanceMap = componentData.get(component);
|
474
|
-
// @ts-ignore - not undefined, but defined right above
|
475
|
-
instanceMap.set(element, instance);
|
476
|
-
},
|
477
|
-
|
478
|
-
/**
|
479
|
-
* Returns all instances for specified component.
|
480
|
-
* @param {string} component the component's name or a unique key
|
481
|
-
* @returns {Map<HTMLElement | Element, Record<string, any>>?} all the component instances
|
482
|
-
*/
|
483
|
-
getAllFor: (component) => {
|
484
|
-
const instanceMap = componentData.get(component);
|
485
|
-
|
486
|
-
return instanceMap || null;
|
487
|
-
},
|
488
|
-
|
489
|
-
/**
|
490
|
-
* Returns the instance associated with the target.
|
491
|
-
* @param {HTMLElement | Element | string} target target element
|
492
|
-
* @param {string} component the component's name or a unique key
|
493
|
-
* @returns {Record<string, any>?} the instance
|
494
|
-
*/
|
495
|
-
get: (target, component) => {
|
496
|
-
const element = querySelector(target);
|
497
|
-
const allForC = Data.getAllFor(component);
|
498
|
-
const instance = element && allForC && allForC.get(element);
|
499
|
-
|
500
|
-
return instance || null;
|
501
|
-
},
|
502
|
-
|
503
|
-
/**
|
504
|
-
* Removes web components data.
|
505
|
-
* @param {HTMLElement | Element | string} target target element
|
506
|
-
* @param {string} component the component's name or a unique key
|
507
|
-
*/
|
508
|
-
remove: (target, component) => {
|
509
|
-
const element = querySelector(target);
|
510
|
-
const instanceMap = componentData.get(component);
|
511
|
-
if (!instanceMap || !element) return;
|
512
|
-
|
513
|
-
instanceMap.delete(element);
|
514
|
-
|
515
|
-
if (instanceMap.size === 0) {
|
516
|
-
componentData.delete(component);
|
517
|
-
}
|
518
|
-
},
|
519
|
-
};
|
520
|
-
|
521
|
-
/**
|
522
|
-
* An alias for `Data.get()`.
|
523
|
-
* @type {SHORTER.getInstance<any>}
|
524
|
-
*/
|
525
|
-
const getInstance = (target, component) => Data.get(target, component);
|
526
|
-
|
527
|
-
/**
|
528
|
-
* Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
|
529
|
-
* @param {HTMLElement | Element} element target element
|
530
|
-
* @param {Partial<CSSStyleDeclaration>} styles attribute value
|
531
|
-
*/
|
532
|
-
// @ts-ignore
|
533
|
-
const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
|
534
|
-
|
535
|
-
/**
|
536
|
-
* Shortcut for `HTMLElement.getAttribute()` method.
|
537
|
-
* @param {HTMLElement | Element} element target element
|
538
|
-
* @param {string} attribute attribute name
|
539
|
-
* @returns {string?} attribute value
|
540
|
-
*/
|
541
|
-
const getAttribute = (element, attribute) => element.getAttribute(attribute);
|
542
|
-
|
543
|
-
/**
|
544
|
-
* The raw value or a given component option.
|
545
|
-
*
|
546
|
-
* @typedef {string | HTMLElement | Function | number | boolean | null} niceValue
|
547
|
-
*/
|
548
|
-
|
549
|
-
/**
|
550
|
-
* Utility to normalize component options
|
551
|
-
*
|
552
|
-
* @param {any} value the input value
|
553
|
-
* @return {niceValue} the normalized value
|
554
|
-
*/
|
555
|
-
function normalizeValue(value) {
|
556
|
-
if (value === 'true') { // boolean
|
557
|
-
return true;
|
558
|
-
}
|
559
|
-
|
560
|
-
if (value === 'false') { // boolean
|
561
|
-
return false;
|
562
|
-
}
|
563
|
-
|
564
|
-
if (!Number.isNaN(+value)) { // number
|
565
|
-
return +value;
|
566
|
-
}
|
567
|
-
|
568
|
-
if (value === '' || value === 'null') { // null
|
569
|
-
return null;
|
570
|
-
}
|
571
|
-
|
572
|
-
// string / function / HTMLElement / object
|
573
|
-
return value;
|
574
|
-
}
|
575
|
-
|
576
|
-
/**
|
577
|
-
* Shortcut for `Object.keys()` static method.
|
578
|
-
* @param {Record<string, any>} obj a target object
|
579
|
-
* @returns {string[]}
|
580
|
-
*/
|
581
|
-
const ObjectKeys = (obj) => Object.keys(obj);
|
582
|
-
|
583
|
-
/**
|
584
|
-
* Shortcut for `String.toLowerCase()`.
|
585
|
-
*
|
586
|
-
* @param {string} source input string
|
587
|
-
* @returns {string} lowercase output string
|
588
|
-
*/
|
589
|
-
const toLowerCase = (source) => source.toLowerCase();
|
590
|
-
|
591
|
-
/**
|
592
|
-
* Utility to normalize component options.
|
593
|
-
*
|
594
|
-
* @param {HTMLElement | Element} element target
|
595
|
-
* @param {Record<string, any>} defaultOps component default options
|
596
|
-
* @param {Record<string, any>} inputOps component instance options
|
597
|
-
* @param {string=} ns component namespace
|
598
|
-
* @return {Record<string, any>} normalized component options object
|
599
|
-
*/
|
600
|
-
function normalizeOptions(element, defaultOps, inputOps, ns) {
|
601
|
-
// @ts-ignore -- our targets are always `HTMLElement`
|
602
|
-
const data = { ...element.dataset };
|
603
|
-
/** @type {Record<string, any>} */
|
604
|
-
const normalOps = {};
|
605
|
-
/** @type {Record<string, any>} */
|
606
|
-
const dataOps = {};
|
607
|
-
const title = 'title';
|
608
|
-
|
609
|
-
ObjectKeys(data).forEach((k) => {
|
610
|
-
const key = ns && k.includes(ns)
|
611
|
-
? k.replace(ns, '').replace(/[A-Z]/, (match) => toLowerCase(match))
|
612
|
-
: k;
|
613
|
-
|
614
|
-
dataOps[key] = normalizeValue(data[k]);
|
615
|
-
});
|
616
|
-
|
617
|
-
ObjectKeys(inputOps).forEach((k) => {
|
618
|
-
inputOps[k] = normalizeValue(inputOps[k]);
|
619
|
-
});
|
620
|
-
|
621
|
-
ObjectKeys(defaultOps).forEach((k) => {
|
622
|
-
if (k in inputOps) {
|
623
|
-
normalOps[k] = inputOps[k];
|
624
|
-
} else if (k in dataOps) {
|
625
|
-
normalOps[k] = dataOps[k];
|
626
|
-
} else {
|
627
|
-
normalOps[k] = k === title
|
628
|
-
? getAttribute(element, title)
|
629
|
-
: defaultOps[k];
|
630
|
-
}
|
631
|
-
});
|
632
|
-
|
633
|
-
return normalOps;
|
634
|
-
}
|
635
|
-
|
636
|
-
/**
|
637
|
-
* Utility to force re-paint of an `HTMLElement` target.
|
638
|
-
*
|
639
|
-
* @param {HTMLElement | Element} element is the target
|
640
|
-
* @return {number} the `Element.offsetHeight` value
|
641
|
-
*/
|
642
|
-
// @ts-ignore
|
643
|
-
const reflow = (element) => element.offsetHeight;
|
644
|
-
|
645
|
-
/**
|
646
|
-
* Utility to focus an `HTMLElement` target.
|
647
|
-
*
|
648
|
-
* @param {HTMLElement | Element} element is the target
|
649
|
-
*/
|
650
|
-
// @ts-ignore -- `Element`s resulted from querySelector can focus too
|
651
|
-
const focus = (element) => element.focus();
|
652
|
-
|
653
|
-
/**
|
654
|
-
* Check class in `HTMLElement.classList`.
|
655
|
-
*
|
656
|
-
* @param {HTMLElement | Element} element target
|
657
|
-
* @param {string} classNAME to check
|
658
|
-
* @returns {boolean}
|
659
|
-
*/
|
660
|
-
function hasClass(element, classNAME) {
|
661
|
-
return element.classList.contains(classNAME);
|
662
|
-
}
|
663
|
-
|
664
|
-
/**
|
665
|
-
* Add class to `HTMLElement.classList`.
|
666
|
-
*
|
667
|
-
* @param {HTMLElement | Element} element target
|
668
|
-
* @param {string} classNAME to add
|
669
|
-
* @returns {void}
|
670
|
-
*/
|
671
|
-
function addClass(element, classNAME) {
|
672
|
-
element.classList.add(classNAME);
|
673
|
-
}
|
674
|
-
|
675
|
-
/**
|
676
|
-
* Remove class from `HTMLElement.classList`.
|
677
|
-
*
|
678
|
-
* @param {HTMLElement | Element} element target
|
679
|
-
* @param {string} classNAME to remove
|
680
|
-
* @returns {void}
|
681
|
-
*/
|
682
|
-
function removeClass(element, classNAME) {
|
683
|
-
element.classList.remove(classNAME);
|
684
|
-
}
|
685
|
-
|
686
|
-
/**
|
687
|
-
* Shortcut for `HTMLElement.setAttribute()` method.
|
688
|
-
* @param {HTMLElement | Element} element target element
|
689
|
-
* @param {string} attribute attribute name
|
690
|
-
* @param {string} value attribute value
|
691
|
-
* @returns {void}
|
692
|
-
*/
|
693
|
-
const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
|
694
|
-
|
695
|
-
/**
|
696
|
-
* Shortcut for `HTMLElement.removeAttribute()` method.
|
697
|
-
* @param {HTMLElement | Element} element target element
|
698
|
-
* @param {string} attribute attribute name
|
699
|
-
* @returns {void}
|
700
|
-
*/
|
701
|
-
const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
|
702
|
-
|
703
|
-
/**
|
704
|
-
* A global namespace for `document.head`.
|
705
|
-
*/
|
706
|
-
const { head: documentHead } = document;
|
707
|
-
|
708
|
-
/**
|
709
|
-
* A list of explicit default non-color values.
|
710
|
-
*/
|
711
|
-
const nonColors = ['transparent', 'currentColor', 'inherit', 'revert', 'initial'];
|
712
|
-
|
713
|
-
/**
|
714
|
-
* Round colour components, for all formats except HEX.
|
715
|
-
* @param {number} v one of the colour components
|
716
|
-
* @returns {number} the rounded number
|
717
|
-
*/
|
718
|
-
function roundPart(v) {
|
719
|
-
const floor = Math.floor(v);
|
720
|
-
return v - floor < 0.5 ? floor : Math.round(v);
|
721
|
-
}
|
722
|
-
|
723
|
-
// Color supported formats
|
724
|
-
const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsv', 'hwb'];
|
725
|
-
|
726
|
-
// Hue angles
|
727
|
-
const ANGLES = 'deg|rad|grad|turn';
|
728
|
-
|
729
|
-
// <http://www.w3.org/TR/css3-values/#integers>
|
730
|
-
const CSS_INTEGER = '[-\\+]?\\d+%?';
|
731
|
-
|
732
|
-
// Include CSS3 Module
|
733
|
-
// <http://www.w3.org/TR/css3-values/#number-value>
|
734
|
-
const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
|
735
|
-
|
736
|
-
// Include CSS4 Module Hue degrees unit
|
737
|
-
// <https://www.w3.org/TR/css3-values/#angle-value>
|
738
|
-
const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
|
739
|
-
|
740
|
-
// Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
|
741
|
-
const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
|
742
|
-
|
743
|
-
// Add angles to the mix
|
744
|
-
const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
|
745
|
-
|
746
|
-
// Start & end
|
747
|
-
const START_MATCH = '(?:[\\s|\\(\\s|\\s\\(\\s]+)?';
|
748
|
-
const END_MATCH = '(?:[\\s|\\)\\s]+)?';
|
749
|
-
// Components separation
|
750
|
-
const SEP = '(?:[,|\\s]+)';
|
751
|
-
const SEP2 = '(?:[,|\\/\\s]*)?';
|
752
|
-
|
753
|
-
// Actual matching.
|
754
|
-
// Parentheses and commas are optional, but not required.
|
755
|
-
// Whitespace can take the place of commas or opening paren
|
756
|
-
const PERMISSIVE_MATCH = `${START_MATCH}(${CSS_UNIT2})${SEP}(${CSS_UNIT})${SEP}(${CSS_UNIT})${SEP2}(${CSS_UNIT})?${END_MATCH}`;
|
757
|
-
|
758
|
-
const matchers = {
|
759
|
-
CSS_UNIT: new RegExp(CSS_UNIT2),
|
760
|
-
hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
|
761
|
-
rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
|
762
|
-
hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
|
763
|
-
hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
|
764
|
-
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
765
|
-
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
766
|
-
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
767
|
-
hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
768
|
-
};
|
769
|
-
|
770
|
-
/**
|
771
|
-
* Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
|
772
|
-
* <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
|
773
|
-
* @param {string} n testing number
|
774
|
-
* @returns {boolean} the query result
|
775
|
-
*/
|
776
|
-
function isOnePointZero(n) {
|
777
|
-
return `${n}`.includes('.') && parseFloat(n) === 1;
|
778
|
-
}
|
779
|
-
|
780
|
-
/**
|
781
|
-
* Check to see if string passed in is a percentage
|
782
|
-
* @param {string} n testing number
|
783
|
-
* @returns {boolean} the query result
|
784
|
-
*/
|
785
|
-
function isPercentage(n) {
|
786
|
-
return `${n}`.includes('%');
|
787
|
-
}
|
788
|
-
|
789
|
-
/**
|
790
|
-
* Check to see if string passed is a web safe colour.
|
791
|
-
* @see https://stackoverflow.com/a/16994164
|
792
|
-
* @param {string} color a colour name, EG: *red*
|
793
|
-
* @returns {boolean} the query result
|
794
|
-
*/
|
795
|
-
function isColorName(color) {
|
796
|
-
if (nonColors.includes(color)
|
797
|
-
|| ['#', ...COLOR_FORMAT].some((f) => color.includes(f))) return false;
|
798
|
-
|
799
|
-
if (['black', 'white'].includes(color)) return true;
|
800
|
-
|
801
|
-
return ['rgb(255, 255, 255)', 'rgb(0, 0, 0)'].every((c) => {
|
802
|
-
setElementStyle(documentHead, { color });
|
803
|
-
const computedColor = getElementStyle(documentHead, 'color');
|
804
|
-
setElementStyle(documentHead, { color: '' });
|
805
|
-
return computedColor !== c;
|
806
|
-
});
|
807
|
-
}
|
808
|
-
|
809
|
-
/**
|
810
|
-
* Check to see if it looks like a CSS unit
|
811
|
-
* (see `matchers` above for definition).
|
812
|
-
* @param {string | number} color testing value
|
813
|
-
* @returns {boolean} the query result
|
814
|
-
*/
|
815
|
-
function isValidCSSUnit(color) {
|
816
|
-
return Boolean(matchers.CSS_UNIT.exec(String(color)));
|
817
|
-
}
|
818
|
-
|
819
|
-
/**
|
820
|
-
* Take input from [0, n] and return it as [0, 1]
|
821
|
-
* @param {*} N the input number
|
822
|
-
* @param {number} max the number maximum value
|
823
|
-
* @returns {number} the number in [0, 1] value range
|
824
|
-
*/
|
825
|
-
function bound01(N, max) {
|
826
|
-
let n = N;
|
827
|
-
|
828
|
-
if (typeof N === 'number'
|
829
|
-
&& Math.min(N, 0) === 0 // round values to 6 decimals Math.round(N * (10 ** 6)) / 10 ** 6
|
830
|
-
&& Math.max(N, 1) === 1) return N;
|
831
|
-
|
832
|
-
if (isOnePointZero(N)) n = '100%';
|
833
|
-
|
834
|
-
const processPercent = isPercentage(n);
|
835
|
-
n = max === 360
|
836
|
-
? parseFloat(n)
|
837
|
-
: Math.min(max, Math.max(0, parseFloat(n)));
|
838
|
-
|
839
|
-
// Automatically convert percentage into number
|
840
|
-
if (processPercent) n = (n * max) / 100;
|
841
|
-
|
842
|
-
// Handle floating point rounding errors
|
843
|
-
if (Math.abs(n - max) < 0.000001) {
|
844
|
-
return 1;
|
845
|
-
}
|
846
|
-
// Convert into [0, 1] range if it isn't already
|
847
|
-
if (max === 360) {
|
848
|
-
// If n is a hue given in degrees,
|
849
|
-
// wrap around out-of-range values into [0, 360] range
|
850
|
-
// then convert into [0, 1].
|
851
|
-
n = (n < 0 ? (n % max) + max : n % max) / max;
|
852
|
-
} else {
|
853
|
-
// If n not a hue given in degrees
|
854
|
-
// Convert into [0, 1] range if it isn't already.
|
855
|
-
n = (n % max) / max;
|
856
|
-
}
|
857
|
-
return n;
|
858
|
-
}
|
859
|
-
|
860
|
-
/**
|
861
|
-
* Return a valid alpha value [0,1] with all invalid values being set to 1.
|
862
|
-
* @param {string | number} a transparency value
|
863
|
-
* @returns {number} a transparency value in the [0, 1] range
|
864
|
-
*/
|
865
|
-
function boundAlpha(a) {
|
866
|
-
let na = parseFloat(`${a}`);
|
867
|
-
|
868
|
-
if (Number.isNaN(na) || na < 0 || na > 1) {
|
869
|
-
na = 1;
|
870
|
-
}
|
871
|
-
|
872
|
-
return na;
|
873
|
-
}
|
874
|
-
|
875
|
-
/**
|
876
|
-
* Force a number between 0 and 1.
|
877
|
-
* @param {number} v the float number
|
878
|
-
* @returns {number} - the resulting number
|
879
|
-
*/
|
880
|
-
function clamp01(v) {
|
881
|
-
return Math.min(1, Math.max(0, v));
|
882
|
-
}
|
883
|
-
|
884
|
-
/**
|
885
|
-
* Returns the hexadecimal value of a web safe colour.
|
886
|
-
* @param {string} name
|
887
|
-
* @returns {string}
|
888
|
-
*/
|
889
|
-
function getRGBFromName(name) {
|
890
|
-
setElementStyle(documentHead, { color: name });
|
891
|
-
const colorName = getElementStyle(documentHead, 'color');
|
892
|
-
setElementStyle(documentHead, { color: '' });
|
893
|
-
return colorName;
|
894
|
-
}
|
895
|
-
|
896
|
-
/**
|
897
|
-
* Converts a decimal value to hexadecimal.
|
898
|
-
* @param {number} d the input number
|
899
|
-
* @returns {string} - the hexadecimal value
|
900
|
-
*/
|
901
|
-
function convertDecimalToHex(d) {
|
902
|
-
return roundPart(d * 255).toString(16);
|
903
|
-
}
|
904
|
-
|
905
|
-
/**
|
906
|
-
* Converts a hexadecimal value to decimal.
|
907
|
-
* @param {string} h hexadecimal value
|
908
|
-
* @returns {number} number in decimal format
|
909
|
-
*/
|
910
|
-
function convertHexToDecimal(h) {
|
911
|
-
return parseIntFromHex(h) / 255;
|
912
|
-
}
|
913
|
-
|
914
|
-
/**
|
915
|
-
* Converts a base-16 hexadecimal value into a base-10 integer.
|
916
|
-
* @param {string} val
|
917
|
-
* @returns {number}
|
918
|
-
*/
|
919
|
-
function parseIntFromHex(val) {
|
920
|
-
return parseInt(val, 16);
|
921
|
-
}
|
922
|
-
|
923
|
-
/**
|
924
|
-
* Force a hexadecimal value to have 2 characters.
|
925
|
-
* @param {string} c string with [0-9A-F] ranged values
|
926
|
-
* @returns {string} 0 => 00, a => 0a
|
927
|
-
*/
|
928
|
-
function pad2(c) {
|
929
|
-
return c.length === 1 ? `0${c}` : String(c);
|
930
|
-
}
|
931
|
-
|
932
|
-
/**
|
933
|
-
* Converts an RGB colour value to HSL.
|
934
|
-
*
|
935
|
-
* @param {number} r Red component [0, 1]
|
936
|
-
* @param {number} g Green component [0, 1]
|
937
|
-
* @param {number} b Blue component [0, 1]
|
938
|
-
* @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
|
939
|
-
*/
|
940
|
-
function rgbToHsl(r, g, b) {
|
941
|
-
const max = Math.max(r, g, b);
|
942
|
-
const min = Math.min(r, g, b);
|
943
|
-
let h = 0;
|
944
|
-
let s = 0;
|
945
|
-
const l = (max + min) / 2;
|
946
|
-
if (max === min) {
|
947
|
-
s = 0;
|
948
|
-
h = 0; // achromatic
|
949
|
-
} else {
|
950
|
-
const d = max - min;
|
951
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
952
|
-
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
953
|
-
if (max === g) h = (b - r) / d + 2;
|
954
|
-
if (max === b) h = (r - g) / d + 4;
|
955
|
-
|
956
|
-
h /= 6;
|
957
|
-
}
|
958
|
-
return { h, s, l };
|
959
|
-
}
|
960
|
-
|
961
|
-
/**
|
962
|
-
* Returns a normalized RGB component value.
|
963
|
-
* @param {number} p
|
964
|
-
* @param {number} q
|
965
|
-
* @param {number} t
|
966
|
-
* @returns {number}
|
967
|
-
*/
|
968
|
-
function hueToRgb(p, q, t) {
|
969
|
-
let T = t;
|
970
|
-
if (T < 0) T += 1;
|
971
|
-
if (T > 1) T -= 1;
|
972
|
-
if (T < 1 / 6) return p + (q - p) * (6 * T);
|
973
|
-
if (T < 1 / 2) return q;
|
974
|
-
if (T < 2 / 3) return p + (q - p) * (2 / 3 - T) * 6;
|
975
|
-
return p;
|
976
|
-
}
|
977
|
-
|
978
|
-
/**
|
979
|
-
* Converts an HSL colour value to RGB.
|
980
|
-
*
|
981
|
-
* @param {number} h Hue Angle [0, 1]
|
982
|
-
* @param {number} s Saturation [0, 1]
|
983
|
-
* @param {number} l Lightness Angle [0, 1]
|
984
|
-
* @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
|
985
|
-
*/
|
986
|
-
function hslToRgb(h, s, l) {
|
987
|
-
let r = 0;
|
988
|
-
let g = 0;
|
989
|
-
let b = 0;
|
990
|
-
|
991
|
-
if (s === 0) {
|
992
|
-
// achromatic
|
993
|
-
g = l;
|
994
|
-
b = l;
|
995
|
-
r = l;
|
996
|
-
} else {
|
997
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
998
|
-
const p = 2 * l - q;
|
999
|
-
r = hueToRgb(p, q, h + 1 / 3);
|
1000
|
-
g = hueToRgb(p, q, h);
|
1001
|
-
b = hueToRgb(p, q, h - 1 / 3);
|
1002
|
-
}
|
1003
|
-
|
1004
|
-
return { r, g, b };
|
1005
|
-
}
|
1006
|
-
|
1007
|
-
/**
|
1008
|
-
* Returns an HWB colour object from an RGB colour object.
|
1009
|
-
* @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
|
1010
|
-
* @link http://alvyray.com/Papers/CG/hwb2rgb.htm
|
1011
|
-
*
|
1012
|
-
* @param {number} r Red component [0, 1]
|
1013
|
-
* @param {number} g Green [0, 1]
|
1014
|
-
* @param {number} b Blue [0, 1]
|
1015
|
-
* @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
|
1016
|
-
*/
|
1017
|
-
function rgbToHwb(r, g, b) {
|
1018
|
-
let f = 0;
|
1019
|
-
let i = 0;
|
1020
|
-
const whiteness = Math.min(r, g, b);
|
1021
|
-
const max = Math.max(r, g, b);
|
1022
|
-
const black = 1 - max;
|
1023
|
-
|
1024
|
-
if (max === whiteness) return { h: 0, w: whiteness, b: black };
|
1025
|
-
if (r === whiteness) {
|
1026
|
-
f = g - b;
|
1027
|
-
i = 3;
|
1028
|
-
} else {
|
1029
|
-
f = g === whiteness ? b - r : r - g;
|
1030
|
-
i = g === whiteness ? 5 : 1;
|
1031
|
-
}
|
1032
|
-
|
1033
|
-
const h = (i - f / (max - whiteness)) / 6;
|
1034
|
-
return {
|
1035
|
-
h: h === 1 ? 0 : h,
|
1036
|
-
w: whiteness,
|
1037
|
-
b: black,
|
1038
|
-
};
|
1039
|
-
}
|
1040
|
-
|
1041
|
-
/**
|
1042
|
-
* Returns an RGB colour object from an HWB colour.
|
1043
|
-
*
|
1044
|
-
* @param {number} H Hue Angle [0, 1]
|
1045
|
-
* @param {number} W Whiteness [0, 1]
|
1046
|
-
* @param {number} B Blackness [0, 1]
|
1047
|
-
* @return {CP.RGB} {r,g,b} object with [0, 1] ranged values
|
1048
|
-
*
|
1049
|
-
* @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
|
1050
|
-
* @link http://alvyray.com/Papers/CG/hwb2rgb.htm
|
1051
|
-
*/
|
1052
|
-
function hwbToRgb(H, W, B) {
|
1053
|
-
if (W + B >= 1) {
|
1054
|
-
const gray = W / (W + B);
|
1055
|
-
return { r: gray, g: gray, b: gray };
|
1056
|
-
}
|
1057
|
-
let { r, g, b } = hslToRgb(H, 1, 0.5);
|
1058
|
-
[r, g, b] = [r, g, b].map((v) => v * (1 - W - B) + W);
|
1059
|
-
|
1060
|
-
return { r, g, b };
|
1061
|
-
}
|
1062
|
-
|
1063
|
-
/**
|
1064
|
-
* Converts an RGB colour value to HSV.
|
1065
|
-
*
|
1066
|
-
* @param {number} r Red component [0, 1]
|
1067
|
-
* @param {number} g Green [0, 1]
|
1068
|
-
* @param {number} b Blue [0, 1]
|
1069
|
-
* @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
|
1070
|
-
*/
|
1071
|
-
function rgbToHsv(r, g, b) {
|
1072
|
-
const max = Math.max(r, g, b);
|
1073
|
-
const min = Math.min(r, g, b);
|
1074
|
-
let h = 0;
|
1075
|
-
const v = max;
|
1076
|
-
const d = max - min;
|
1077
|
-
const s = max === 0 ? 0 : d / max;
|
1078
|
-
if (max === min) {
|
1079
|
-
h = 0; // achromatic
|
1080
|
-
} else {
|
1081
|
-
if (r === max) h = (g - b) / d + (g < b ? 6 : 0);
|
1082
|
-
if (g === max) h = (b - r) / d + 2;
|
1083
|
-
if (b === max) h = (r - g) / d + 4;
|
1084
|
-
|
1085
|
-
h /= 6;
|
1086
|
-
}
|
1087
|
-
return { h, s, v };
|
1088
|
-
}
|
1089
|
-
|
1090
|
-
/**
|
1091
|
-
* Converts an HSV colour value to RGB.
|
1092
|
-
*
|
1093
|
-
* @param {number} H Hue Angle [0, 1]
|
1094
|
-
* @param {number} S Saturation [0, 1]
|
1095
|
-
* @param {number} V Brightness Angle [0, 1]
|
1096
|
-
* @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
|
1097
|
-
*/
|
1098
|
-
function hsvToRgb(H, S, V) {
|
1099
|
-
const h = H * 6;
|
1100
|
-
const s = S;
|
1101
|
-
const v = V;
|
1102
|
-
const i = Math.floor(h);
|
1103
|
-
const f = h - i;
|
1104
|
-
const p = v * (1 - s);
|
1105
|
-
const q = v * (1 - f * s);
|
1106
|
-
const t = v * (1 - (1 - f) * s);
|
1107
|
-
const mod = i % 6;
|
1108
|
-
const r = [v, q, p, p, t, v][mod];
|
1109
|
-
const g = [t, v, v, q, p, p][mod];
|
1110
|
-
const b = [p, p, t, v, v, q][mod];
|
1111
|
-
return { r, g, b };
|
1112
|
-
}
|
1113
|
-
|
1114
|
-
/**
|
1115
|
-
* Converts an RGB colour to hex
|
1116
|
-
*
|
1117
|
-
* Assumes r, g, and b are contained in the set [0, 255]
|
1118
|
-
* Returns a 3 or 6 character hex
|
1119
|
-
* @param {number} r Red component [0, 255]
|
1120
|
-
* @param {number} g Green [0, 255]
|
1121
|
-
* @param {number} b Blue [0, 255]
|
1122
|
-
* @param {boolean=} allow3Char
|
1123
|
-
* @returns {string}
|
1124
|
-
*/
|
1125
|
-
function rgbToHex(r, g, b, allow3Char) {
|
1126
|
-
const hex = [
|
1127
|
-
pad2(roundPart(r).toString(16)),
|
1128
|
-
pad2(roundPart(g).toString(16)),
|
1129
|
-
pad2(roundPart(b).toString(16)),
|
1130
|
-
];
|
1131
|
-
|
1132
|
-
// Return a 3 character hex if possible
|
1133
|
-
if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
|
1134
|
-
&& hex[1].charAt(0) === hex[1].charAt(1)
|
1135
|
-
&& hex[2].charAt(0) === hex[2].charAt(1)) {
|
1136
|
-
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
|
1137
|
-
}
|
1138
|
-
|
1139
|
-
return hex.join('');
|
1140
|
-
}
|
1141
|
-
|
1142
|
-
/**
|
1143
|
-
* Converts an RGBA color plus alpha transparency to hex8.
|
1144
|
-
*
|
1145
|
-
* @param {number} r Red component [0, 255]
|
1146
|
-
* @param {number} g Green [0, 255]
|
1147
|
-
* @param {number} b Blue [0, 255]
|
1148
|
-
* @param {number} a Alpha transparency [0, 1]
|
1149
|
-
* @param {boolean=} allow4Char when *true* it will also find hex shorthand
|
1150
|
-
* @returns {string} a hexadecimal value with alpha transparency
|
1151
|
-
*/
|
1152
|
-
function rgbaToHex(r, g, b, a, allow4Char) {
|
1153
|
-
const hex = [
|
1154
|
-
pad2(roundPart(r).toString(16)),
|
1155
|
-
pad2(roundPart(g).toString(16)),
|
1156
|
-
pad2(roundPart(b).toString(16)),
|
1157
|
-
pad2(convertDecimalToHex(a)),
|
1158
|
-
];
|
1159
|
-
|
1160
|
-
// Return a 4 character hex if possible
|
1161
|
-
if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
|
1162
|
-
&& hex[1].charAt(0) === hex[1].charAt(1)
|
1163
|
-
&& hex[2].charAt(0) === hex[2].charAt(1)
|
1164
|
-
&& hex[3].charAt(0) === hex[3].charAt(1)) {
|
1165
|
-
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
|
1166
|
-
}
|
1167
|
-
return hex.join('');
|
1168
|
-
}
|
1169
|
-
|
1170
|
-
/**
|
1171
|
-
* Permissive string parsing. Take in a number of formats, and output an object
|
1172
|
-
* based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
|
1173
|
-
* @param {string} input colour value in any format
|
1174
|
-
* @returns {Record<string, (number | string | boolean)> | false} an object matching the RegExp
|
1175
|
-
*/
|
1176
|
-
function stringInputToObject(input) {
|
1177
|
-
let color = toLowerCase(input.trim());
|
1178
|
-
|
1179
|
-
if (color.length === 0) {
|
1180
|
-
return {
|
1181
|
-
r: 0, g: 0, b: 0, a: 1,
|
1182
|
-
};
|
1183
|
-
}
|
1184
|
-
|
1185
|
-
if (isColorName(color)) {
|
1186
|
-
color = getRGBFromName(color);
|
1187
|
-
} else if (nonColors.includes(color)) {
|
1188
|
-
const a = color === 'transparent' ? 0 : 1;
|
1189
|
-
return {
|
1190
|
-
r: 0, g: 0, b: 0, a, format: 'rgb', ok: true,
|
1191
|
-
};
|
1192
|
-
}
|
1193
|
-
|
1194
|
-
// Try to match string input using regular expressions.
|
1195
|
-
// Keep most of the number bounding out of this function,
|
1196
|
-
// don't worry about [0,1] or [0,100] or [0,360]
|
1197
|
-
// Just return an object and let the conversion functions handle that.
|
1198
|
-
// This way the result will be the same whether Color is initialized with string or object.
|
1199
|
-
let [, m1, m2, m3, m4] = matchers.rgb.exec(color) || [];
|
1200
|
-
if (m1 && m2 && m3/* && m4 */) {
|
1201
|
-
return {
|
1202
|
-
r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
|
1203
|
-
};
|
1204
|
-
}
|
1205
|
-
|
1206
|
-
[, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
|
1207
|
-
if (m1 && m2 && m3/* && m4 */) {
|
1208
|
-
return {
|
1209
|
-
h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
|
1210
|
-
};
|
1211
|
-
}
|
1212
|
-
|
1213
|
-
[, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
|
1214
|
-
if (m1 && m2 && m3/* && m4 */) {
|
1215
|
-
return {
|
1216
|
-
h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
|
1217
|
-
};
|
1218
|
-
}
|
1219
|
-
|
1220
|
-
[, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
|
1221
|
-
if (m1 && m2 && m3) {
|
1222
|
-
return {
|
1223
|
-
h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
|
1224
|
-
};
|
1225
|
-
}
|
1226
|
-
|
1227
|
-
[, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
|
1228
|
-
if (m1 && m2 && m3 && m4) {
|
1229
|
-
return {
|
1230
|
-
r: parseIntFromHex(m1),
|
1231
|
-
g: parseIntFromHex(m2),
|
1232
|
-
b: parseIntFromHex(m3),
|
1233
|
-
a: convertHexToDecimal(m4),
|
1234
|
-
format: 'hex',
|
1235
|
-
};
|
1236
|
-
}
|
1237
|
-
|
1238
|
-
[, m1, m2, m3] = matchers.hex6.exec(color) || [];
|
1239
|
-
if (m1 && m2 && m3) {
|
1240
|
-
return {
|
1241
|
-
r: parseIntFromHex(m1),
|
1242
|
-
g: parseIntFromHex(m2),
|
1243
|
-
b: parseIntFromHex(m3),
|
1244
|
-
format: 'hex',
|
1245
|
-
};
|
1246
|
-
}
|
1247
|
-
|
1248
|
-
[, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
|
1249
|
-
if (m1 && m2 && m3 && m4) {
|
1250
|
-
return {
|
1251
|
-
r: parseIntFromHex(m1 + m1),
|
1252
|
-
g: parseIntFromHex(m2 + m2),
|
1253
|
-
b: parseIntFromHex(m3 + m3),
|
1254
|
-
a: convertHexToDecimal(m4 + m4),
|
1255
|
-
format: 'hex',
|
1256
|
-
};
|
1257
|
-
}
|
1258
|
-
|
1259
|
-
[, m1, m2, m3] = matchers.hex3.exec(color) || [];
|
1260
|
-
if (m1 && m2 && m3) {
|
1261
|
-
return {
|
1262
|
-
r: parseIntFromHex(m1 + m1),
|
1263
|
-
g: parseIntFromHex(m2 + m2),
|
1264
|
-
b: parseIntFromHex(m3 + m3),
|
1265
|
-
format: 'hex',
|
1266
|
-
};
|
1267
|
-
}
|
1268
|
-
|
1269
|
-
return false;
|
1270
|
-
}
|
1271
|
-
|
1272
|
-
/**
|
1273
|
-
* Given a string or object, convert that input to RGB
|
1274
|
-
*
|
1275
|
-
* Possible string inputs:
|
1276
|
-
* ```
|
1277
|
-
* "red"
|
1278
|
-
* "#f00" or "f00"
|
1279
|
-
* "#ff0000" or "ff0000"
|
1280
|
-
* "#ff000000" or "ff000000" // CSS4 Module
|
1281
|
-
* "rgb 255 0 0" or "rgb (255, 0, 0)"
|
1282
|
-
* "rgb 1.0 0 0" or "rgb (1, 0, 0)"
|
1283
|
-
* "rgba(255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
|
1284
|
-
* "rgba(1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
|
1285
|
-
* "rgb(255 0 0 / 10%)" or "rgb 255 0 0 0.1" // CSS4 Module
|
1286
|
-
* "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
|
1287
|
-
* "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
|
1288
|
-
* "hsl(0deg 100% 50% / 50%)" or "hsl 0 100 50 50" // CSS4 Module
|
1289
|
-
* "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
|
1290
|
-
* "hsva(0, 100%, 100%, 0.1)" or "hsva 0 100% 100% 0.1"
|
1291
|
-
* "hsv(0deg 100% 100% / 10%)" or "hsv 0 100 100 0.1" // CSS4 Module
|
1292
|
-
* "hwb(0deg, 100%, 100%, 100%)" or "hwb 0 100% 100% 0.1" // CSS4 Module
|
1293
|
-
* ```
|
1294
|
-
* @param {string | Record<string, any>} input
|
1295
|
-
* @returns {CP.ColorObject}
|
1296
|
-
*/
|
1297
|
-
function inputToRGB(input) {
|
1298
|
-
let rgb = { r: 0, g: 0, b: 0 };
|
1299
|
-
/** @type {*} */
|
1300
|
-
let color = input;
|
1301
|
-
/** @type {string | number} */
|
1302
|
-
let a = 1;
|
1303
|
-
let s = null;
|
1304
|
-
let v = null;
|
1305
|
-
let l = null;
|
1306
|
-
let w = null;
|
1307
|
-
let b = null;
|
1308
|
-
let h = null;
|
1309
|
-
let r = null;
|
1310
|
-
let g = null;
|
1311
|
-
let ok = false;
|
1312
|
-
const inputFormat = typeof color === 'object' && color.format;
|
1313
|
-
let format = inputFormat && COLOR_FORMAT.includes(inputFormat) ? inputFormat : 'rgb';
|
1314
|
-
|
1315
|
-
if (typeof input === 'string') {
|
1316
|
-
color = stringInputToObject(input);
|
1317
|
-
if (color) ok = true;
|
1318
|
-
}
|
1319
|
-
if (typeof color === 'object') {
|
1320
|
-
if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
|
1321
|
-
({ r, g, b } = color);
|
1322
|
-
// RGB values now are all in [0, 1] range
|
1323
|
-
[r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255));
|
1324
|
-
rgb = { r, g, b };
|
1325
|
-
ok = true;
|
1326
|
-
format = color.format || 'rgb';
|
1327
|
-
}
|
1328
|
-
if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
|
1329
|
-
({ h, s, v } = color);
|
1330
|
-
h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
|
1331
|
-
s = bound01(s, 100); // saturation can be `5%` or a [0, 1] value
|
1332
|
-
v = bound01(v, 100); // brightness can be `5%` or a [0, 1] value
|
1333
|
-
rgb = hsvToRgb(h, s, v);
|
1334
|
-
ok = true;
|
1335
|
-
format = 'hsv';
|
1336
|
-
}
|
1337
|
-
if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
|
1338
|
-
({ h, s, l } = color);
|
1339
|
-
h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
|
1340
|
-
s = bound01(s, 100); // saturation can be `5%` or a [0, 1] value
|
1341
|
-
l = bound01(l, 100); // lightness can be `5%` or a [0, 1] value
|
1342
|
-
rgb = hslToRgb(h, s, l);
|
1343
|
-
ok = true;
|
1344
|
-
format = 'hsl';
|
1345
|
-
}
|
1346
|
-
if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
|
1347
|
-
({ h, w, b } = color);
|
1348
|
-
h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
|
1349
|
-
w = bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
|
1350
|
-
b = bound01(b, 100); // blackness can be `5%` or a [0, 1] value
|
1351
|
-
rgb = hwbToRgb(h, w, b);
|
1352
|
-
ok = true;
|
1353
|
-
format = 'hwb';
|
1354
|
-
}
|
1355
|
-
if (isValidCSSUnit(color.a)) {
|
1356
|
-
a = color.a;
|
1357
|
-
a = isPercentage(`${a}`) || parseFloat(a) > 1 ? bound01(a, 100) : a;
|
1358
|
-
}
|
1359
|
-
}
|
1360
|
-
if (typeof color === 'undefined') {
|
1361
|
-
ok = true;
|
1362
|
-
}
|
1363
|
-
|
1364
|
-
return {
|
1365
|
-
ok,
|
1366
|
-
format,
|
1367
|
-
r: rgb.r,
|
1368
|
-
g: rgb.g,
|
1369
|
-
b: rgb.b,
|
1370
|
-
a: boundAlpha(a),
|
1371
|
-
};
|
1372
|
-
}
|
1373
|
-
|
1374
|
-
/**
|
1375
|
-
* @class
|
1376
|
-
* Returns a new `Color` instance.
|
1377
|
-
* @see https://github.com/bgrins/TinyColor
|
1378
|
-
*/
|
1379
|
-
class Color {
|
1380
|
-
/**
|
1381
|
-
* @constructor
|
1382
|
-
* @param {CP.ColorInput} input the given colour value
|
1383
|
-
* @param {CP.ColorFormats=} config the given format
|
1384
|
-
*/
|
1385
|
-
constructor(input, config) {
|
1386
|
-
let color = input;
|
1387
|
-
const configFormat = config && COLOR_FORMAT.includes(config)
|
1388
|
-
? config : '';
|
1389
|
-
|
1390
|
-
// If input is already a `Color`, clone its values
|
1391
|
-
if (color instanceof Color) {
|
1392
|
-
color = inputToRGB(color);
|
1393
|
-
}
|
1394
|
-
|
1395
|
-
const {
|
1396
|
-
r, g, b, a, ok, format,
|
1397
|
-
} = inputToRGB(color);
|
1398
|
-
|
1399
|
-
// bind
|
1400
|
-
const self = this;
|
1401
|
-
|
1402
|
-
/** @type {CP.ColorInput} */
|
1403
|
-
self.originalInput = input;
|
1404
|
-
/** @type {number} */
|
1405
|
-
self.r = r;
|
1406
|
-
/** @type {number} */
|
1407
|
-
self.g = g;
|
1408
|
-
/** @type {number} */
|
1409
|
-
self.b = b;
|
1410
|
-
/** @type {number} */
|
1411
|
-
self.a = a;
|
1412
|
-
/** @type {boolean} */
|
1413
|
-
self.ok = ok;
|
1414
|
-
/** @type {CP.ColorFormats} */
|
1415
|
-
self.format = configFormat || format;
|
1416
|
-
}
|
1417
|
-
|
1418
|
-
/**
|
1419
|
-
* Checks if the current input value is a valid colour.
|
1420
|
-
* @returns {boolean} the query result
|
1421
|
-
*/
|
1422
|
-
get isValid() {
|
1423
|
-
return this.ok;
|
1424
|
-
}
|
1425
|
-
|
1426
|
-
/**
|
1427
|
-
* Checks if the current colour requires a light text colour.
|
1428
|
-
* @returns {boolean} the query result
|
1429
|
-
*/
|
1430
|
-
get isDark() {
|
1431
|
-
return this.brightness < 120;
|
1432
|
-
}
|
1433
|
-
|
1434
|
-
/**
|
1435
|
-
* Returns the perceived luminance of a colour.
|
1436
|
-
* @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
1437
|
-
* @returns {number} a number in the [0, 1] range
|
1438
|
-
*/
|
1439
|
-
get luminance() {
|
1440
|
-
const { r, g, b } = this;
|
1441
|
-
let R = 0;
|
1442
|
-
let G = 0;
|
1443
|
-
let B = 0;
|
1444
|
-
|
1445
|
-
if (r <= 0.03928) {
|
1446
|
-
R = r / 12.92;
|
1447
|
-
} else {
|
1448
|
-
R = ((r + 0.055) / 1.055) ** 2.4;
|
1449
|
-
}
|
1450
|
-
if (g <= 0.03928) {
|
1451
|
-
G = g / 12.92;
|
1452
|
-
} else {
|
1453
|
-
G = ((g + 0.055) / 1.055) ** 2.4;
|
1454
|
-
}
|
1455
|
-
if (b <= 0.03928) {
|
1456
|
-
B = b / 12.92;
|
1457
|
-
} else {
|
1458
|
-
B = ((b + 0.055) / 1.055) ** 2.4;
|
1459
|
-
}
|
1460
|
-
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
1461
|
-
}
|
1462
|
-
|
1463
|
-
/**
|
1464
|
-
* Returns the perceived brightness of the colour.
|
1465
|
-
* @returns {number} a number in the [0, 255] range
|
1466
|
-
*/
|
1467
|
-
get brightness() {
|
1468
|
-
const { r, g, b } = this.toRgb();
|
1469
|
-
return (r * 299 + g * 587 + b * 114) / 1000;
|
1470
|
-
}
|
1471
|
-
|
1472
|
-
/**
|
1473
|
-
* Returns the colour as an RGBA object.
|
1474
|
-
* @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
|
1475
|
-
*/
|
1476
|
-
toRgb() {
|
1477
|
-
let {
|
1478
|
-
r, g, b, a,
|
1479
|
-
} = this;
|
1480
|
-
|
1481
|
-
[r, g, b] = [r, g, b].map((n) => roundPart(n * 255 * 100) / 100);
|
1482
|
-
a = roundPart(a * 100) / 100;
|
1483
|
-
return {
|
1484
|
-
r, g, b, a,
|
1485
|
-
};
|
1486
|
-
}
|
1487
|
-
|
1488
|
-
/**
|
1489
|
-
* Returns the RGBA values concatenated into a CSS3 Module string format.
|
1490
|
-
* * rgb(255,255,255)
|
1491
|
-
* * rgba(255,255,255,0.5)
|
1492
|
-
* @returns {string} the CSS valid colour in RGB/RGBA format
|
1493
|
-
*/
|
1494
|
-
toRgbString() {
|
1495
|
-
const {
|
1496
|
-
r, g, b, a,
|
1497
|
-
} = this.toRgb();
|
1498
|
-
const [R, G, B] = [r, g, b].map(roundPart);
|
1499
|
-
|
1500
|
-
return a === 1
|
1501
|
-
? `rgb(${R}, ${G}, ${B})`
|
1502
|
-
: `rgba(${R}, ${G}, ${B}, ${a})`;
|
1503
|
-
}
|
1504
|
-
|
1505
|
-
/**
|
1506
|
-
* Returns the RGBA values concatenated into a CSS4 Module string format.
|
1507
|
-
* * rgb(255 255 255)
|
1508
|
-
* * rgb(255 255 255 / 50%)
|
1509
|
-
* @returns {string} the CSS valid colour in CSS4 RGB format
|
1510
|
-
*/
|
1511
|
-
toRgbCSS4String() {
|
1512
|
-
const {
|
1513
|
-
r, g, b, a,
|
1514
|
-
} = this.toRgb();
|
1515
|
-
const [R, G, B] = [r, g, b].map(roundPart);
|
1516
|
-
const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
|
1517
|
-
|
1518
|
-
return `rgb(${R} ${G} ${B}${A})`;
|
1519
|
-
}
|
1520
|
-
|
1521
|
-
/**
|
1522
|
-
* Returns the hexadecimal value of the colour. When the parameter is *true*
|
1523
|
-
* it will find a 3 characters shorthand of the decimal value.
|
1524
|
-
*
|
1525
|
-
* @param {boolean=} allow3Char when `true` returns shorthand HEX
|
1526
|
-
* @returns {string} the hexadecimal colour format
|
1527
|
-
*/
|
1528
|
-
toHex(allow3Char) {
|
1529
|
-
const {
|
1530
|
-
r, g, b, a,
|
1531
|
-
} = this.toRgb();
|
1532
|
-
|
1533
|
-
return a === 1
|
1534
|
-
? rgbToHex(r, g, b, allow3Char)
|
1535
|
-
: rgbaToHex(r, g, b, a, allow3Char);
|
1536
|
-
}
|
1537
|
-
|
1538
|
-
/**
|
1539
|
-
* Returns the CSS valid hexadecimal vaue of the colour. When the parameter is *true*
|
1540
|
-
* it will find a 3 characters shorthand of the value.
|
1541
|
-
*
|
1542
|
-
* @param {boolean=} allow3Char when `true` returns shorthand HEX
|
1543
|
-
* @returns {string} the CSS valid colour in hexadecimal format
|
1544
|
-
*/
|
1545
|
-
toHexString(allow3Char) {
|
1546
|
-
return `#${this.toHex(allow3Char)}`;
|
1547
|
-
}
|
1548
|
-
|
1549
|
-
/**
|
1550
|
-
* Returns the HEX8 value of the colour.
|
1551
|
-
* @param {boolean=} allow4Char when `true` returns shorthand HEX
|
1552
|
-
* @returns {string} the CSS valid colour in hexadecimal format
|
1553
|
-
*/
|
1554
|
-
toHex8(allow4Char) {
|
1555
|
-
const {
|
1556
|
-
r, g, b, a,
|
1557
|
-
} = this.toRgb();
|
1558
|
-
|
1559
|
-
return rgbaToHex(r, g, b, a, allow4Char);
|
1560
|
-
}
|
1561
|
-
|
1562
|
-
/**
|
1563
|
-
* Returns the HEX8 value of the colour.
|
1564
|
-
* @param {boolean=} allow4Char when `true` returns shorthand HEX
|
1565
|
-
* @returns {string} the CSS valid colour in hexadecimal format
|
1566
|
-
*/
|
1567
|
-
toHex8String(allow4Char) {
|
1568
|
-
return `#${this.toHex8(allow4Char)}`;
|
1569
|
-
}
|
1570
|
-
|
1571
|
-
/**
|
1572
|
-
* Returns the colour as a HSVA object.
|
1573
|
-
* @returns {CP.HSVA} the `{h,s,v,a}` object with [0, 1] ranged values
|
1574
|
-
*/
|
1575
|
-
toHsv() {
|
1576
|
-
const {
|
1577
|
-
r, g, b, a,
|
1578
|
-
} = this;
|
1579
|
-
const { h, s, v } = rgbToHsv(r, g, b);
|
1580
|
-
|
1581
|
-
return {
|
1582
|
-
h, s, v, a,
|
1583
|
-
};
|
1584
|
-
}
|
1585
|
-
|
1586
|
-
/**
|
1587
|
-
* Returns the colour as an HSLA object.
|
1588
|
-
* @returns {CP.HSLA} the `{h,s,l,a}` object with [0, 1] ranged values
|
1589
|
-
*/
|
1590
|
-
toHsl() {
|
1591
|
-
const {
|
1592
|
-
r, g, b, a,
|
1593
|
-
} = this;
|
1594
|
-
const { h, s, l } = rgbToHsl(r, g, b);
|
1595
|
-
|
1596
|
-
return {
|
1597
|
-
h, s, l, a,
|
1598
|
-
};
|
1599
|
-
}
|
1600
|
-
|
1601
|
-
/**
|
1602
|
-
* Returns the HSLA values concatenated into a CSS3 Module format string.
|
1603
|
-
* * `hsl(150, 100%, 50%)`
|
1604
|
-
* * `hsla(150, 100%, 50%, 0.5)`
|
1605
|
-
* @returns {string} the CSS valid colour in HSL/HSLA format
|
1606
|
-
*/
|
1607
|
-
toHslString() {
|
1608
|
-
let {
|
1609
|
-
h, s, l, a,
|
1610
|
-
} = this.toHsl();
|
1611
|
-
h = roundPart(h * 360);
|
1612
|
-
s = roundPart(s * 100);
|
1613
|
-
l = roundPart(l * 100);
|
1614
|
-
a = roundPart(a * 100) / 100;
|
1615
|
-
|
1616
|
-
return a === 1
|
1617
|
-
? `hsl(${h}, ${s}%, ${l}%)`
|
1618
|
-
: `hsla(${h}, ${s}%, ${l}%, ${a})`;
|
1619
|
-
}
|
1620
|
-
|
1621
|
-
/**
|
1622
|
-
* Returns the HSLA values concatenated into a CSS4 Module format string.
|
1623
|
-
* * `hsl(150deg 100% 50%)`
|
1624
|
-
* * `hsl(150deg 100% 50% / 50%)`
|
1625
|
-
* @returns {string} the CSS valid colour in CSS4 HSL format
|
1626
|
-
*/
|
1627
|
-
toHslCSS4String() {
|
1628
|
-
let {
|
1629
|
-
h, s, l, a,
|
1630
|
-
} = this.toHsl();
|
1631
|
-
h = roundPart(h * 360);
|
1632
|
-
s = roundPart(s * 100);
|
1633
|
-
l = roundPart(l * 100);
|
1634
|
-
a = roundPart(a * 100);
|
1635
|
-
const A = a < 100 ? ` / ${roundPart(a)}%` : '';
|
1636
|
-
|
1637
|
-
return `hsl(${h}deg ${s}% ${l}%${A})`;
|
1638
|
-
}
|
1639
|
-
|
1640
|
-
/**
|
1641
|
-
* Returns the colour as an HWBA object.
|
1642
|
-
* @returns {CP.HWBA} the `{h,w,b,a}` object with [0, 1] ranged values
|
1643
|
-
*/
|
1644
|
-
toHwb() {
|
1645
|
-
const {
|
1646
|
-
r, g, b, a,
|
1647
|
-
} = this;
|
1648
|
-
const { h, w, b: bl } = rgbToHwb(r, g, b);
|
1649
|
-
return {
|
1650
|
-
h, w, b: bl, a,
|
1651
|
-
};
|
1652
|
-
}
|
1653
|
-
|
1654
|
-
/**
|
1655
|
-
* Returns the HWBA values concatenated into a string.
|
1656
|
-
* @returns {string} the CSS valid colour in HWB format
|
1657
|
-
*/
|
1658
|
-
toHwbString() {
|
1659
|
-
let {
|
1660
|
-
h, w, b, a,
|
1661
|
-
} = this.toHwb();
|
1662
|
-
h = roundPart(h * 360);
|
1663
|
-
w = roundPart(w * 100);
|
1664
|
-
b = roundPart(b * 100);
|
1665
|
-
a = roundPart(a * 100);
|
1666
|
-
const A = a < 100 ? ` / ${roundPart(a)}%` : '';
|
1667
|
-
|
1668
|
-
return `hwb(${h}deg ${w}% ${b}%${A})`;
|
1669
|
-
}
|
1670
|
-
|
1671
|
-
/**
|
1672
|
-
* Sets the alpha value of the current colour.
|
1673
|
-
* @param {number} alpha a new alpha value in the [0, 1] range.
|
1674
|
-
* @returns {Color} the `Color` instance
|
1675
|
-
*/
|
1676
|
-
setAlpha(alpha) {
|
1677
|
-
const self = this;
|
1678
|
-
if (typeof alpha !== 'number') return self;
|
1679
|
-
self.a = boundAlpha(alpha);
|
1680
|
-
return self;
|
1681
|
-
}
|
1682
|
-
|
1683
|
-
/**
|
1684
|
-
* Saturate the colour with a given amount.
|
1685
|
-
* @param {number=} amount a value in the [0, 100] range
|
1686
|
-
* @returns {Color} the `Color` instance
|
1687
|
-
*/
|
1688
|
-
saturate(amount) {
|
1689
|
-
const self = this;
|
1690
|
-
if (typeof amount !== 'number') return self;
|
1691
|
-
const { h, s, l } = self.toHsl();
|
1692
|
-
const { r, g, b } = hslToRgb(h, clamp01(s + amount / 100), l);
|
1693
|
-
|
1694
|
-
ObjectAssign(self, { r, g, b });
|
1695
|
-
return self;
|
1696
|
-
}
|
1697
|
-
|
1698
|
-
/**
|
1699
|
-
* Desaturate the colour with a given amount.
|
1700
|
-
* @param {number=} amount a value in the [0, 100] range
|
1701
|
-
* @returns {Color} the `Color` instance
|
1702
|
-
*/
|
1703
|
-
desaturate(amount) {
|
1704
|
-
return typeof amount === 'number' ? this.saturate(-amount) : this;
|
1705
|
-
}
|
1706
|
-
|
1707
|
-
/**
|
1708
|
-
* Completely desaturates a colour into greyscale.
|
1709
|
-
* Same as calling `desaturate(100)`
|
1710
|
-
* @returns {Color} the `Color` instance
|
1711
|
-
*/
|
1712
|
-
greyscale() {
|
1713
|
-
return this.saturate(-100);
|
1714
|
-
}
|
1715
|
-
|
1716
|
-
/**
|
1717
|
-
* Increase the colour lightness with a given amount.
|
1718
|
-
* @param {number=} amount a value in the [0, 100] range
|
1719
|
-
* @returns {Color} the `Color` instance
|
1720
|
-
*/
|
1721
|
-
lighten(amount) {
|
1722
|
-
const self = this;
|
1723
|
-
if (typeof amount !== 'number') return self;
|
1724
|
-
|
1725
|
-
const { h, s, l } = self.toHsl();
|
1726
|
-
const { r, g, b } = hslToRgb(h, s, clamp01(l + amount / 100));
|
1727
|
-
|
1728
|
-
ObjectAssign(self, { r, g, b });
|
1729
|
-
return self;
|
1730
|
-
}
|
1731
|
-
|
1732
|
-
/**
|
1733
|
-
* Decrease the colour lightness with a given amount.
|
1734
|
-
* @param {number=} amount a value in the [0, 100] range
|
1735
|
-
* @returns {Color} the `Color` instance
|
1736
|
-
*/
|
1737
|
-
darken(amount) {
|
1738
|
-
return typeof amount === 'number' ? this.lighten(-amount) : this;
|
1739
|
-
}
|
1740
|
-
|
1741
|
-
/**
|
1742
|
-
* Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
|
1743
|
-
* Values outside of this range will be wrapped into this range.
|
1744
|
-
*
|
1745
|
-
* @param {number=} amount a value in the [0, 100] range
|
1746
|
-
* @returns {Color} the `Color` instance
|
1747
|
-
*/
|
1748
|
-
spin(amount) {
|
1749
|
-
const self = this;
|
1750
|
-
if (typeof amount !== 'number') return self;
|
1751
|
-
|
1752
|
-
const { h, s, l } = self.toHsl();
|
1753
|
-
const { r, g, b } = hslToRgb(clamp01(((h * 360 + amount) % 360) / 360), s, l);
|
1754
|
-
|
1755
|
-
ObjectAssign(self, { r, g, b });
|
1756
|
-
return self;
|
1757
|
-
}
|
1758
|
-
|
1759
|
-
/** Returns a clone of the current `Color` instance. */
|
1760
|
-
clone() {
|
1761
|
-
return new Color(this);
|
1762
|
-
}
|
1763
|
-
|
1764
|
-
/**
|
1765
|
-
* Returns the colour value in CSS valid string format.
|
1766
|
-
* @param {boolean=} allowShort when *true*, HEX values can be shorthand
|
1767
|
-
* @returns {string} the CSS valid colour in the configured format
|
1768
|
-
*/
|
1769
|
-
toString(allowShort) {
|
1770
|
-
const self = this;
|
1771
|
-
const { format } = self;
|
1772
|
-
|
1773
|
-
if (format === 'hex') return self.toHexString(allowShort);
|
1774
|
-
if (format === 'hsl') return self.toHslString();
|
1775
|
-
if (format === 'hwb') return self.toHwbString();
|
1776
|
-
|
1777
|
-
return self.toRgbString();
|
1778
|
-
}
|
1779
|
-
}
|
1780
|
-
|
1781
|
-
ObjectAssign(Color, {
|
1782
|
-
ANGLES,
|
1783
|
-
CSS_ANGLE,
|
1784
|
-
CSS_INTEGER,
|
1785
|
-
CSS_NUMBER,
|
1786
|
-
CSS_UNIT,
|
1787
|
-
CSS_UNIT2,
|
1788
|
-
PERMISSIVE_MATCH,
|
1789
|
-
matchers,
|
1790
|
-
isOnePointZero,
|
1791
|
-
isPercentage,
|
1792
|
-
isValidCSSUnit,
|
1793
|
-
isColorName,
|
1794
|
-
pad2,
|
1795
|
-
clamp01,
|
1796
|
-
bound01,
|
1797
|
-
boundAlpha,
|
1798
|
-
getRGBFromName,
|
1799
|
-
convertHexToDecimal,
|
1800
|
-
convertDecimalToHex,
|
1801
|
-
rgbToHsl,
|
1802
|
-
rgbToHex,
|
1803
|
-
rgbToHsv,
|
1804
|
-
rgbToHwb,
|
1805
|
-
rgbaToHex,
|
1806
|
-
hslToRgb,
|
1807
|
-
hsvToRgb,
|
1808
|
-
hueToRgb,
|
1809
|
-
hwbToRgb,
|
1810
|
-
parseIntFromHex,
|
1811
|
-
stringInputToObject,
|
1812
|
-
inputToRGB,
|
1813
|
-
roundPart,
|
1814
|
-
getElementStyle,
|
1815
|
-
setElementStyle,
|
1816
|
-
ObjectAssign,
|
1817
|
-
});
|
1818
|
-
|
1819
|
-
/**
|
1820
|
-
* @class
|
1821
|
-
* Returns a color palette with a given set of parameters.
|
1822
|
-
* @example
|
1823
|
-
* new ColorPalette(0, 12, 10);
|
1824
|
-
* // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: Array<Color> }
|
1825
|
-
*/
|
1826
|
-
class ColorPalette {
|
1827
|
-
/**
|
1828
|
-
* The `hue` parameter is optional, which would be set to 0.
|
1829
|
-
* @param {number[]} args represeinting hue, hueSteps, lightSteps
|
1830
|
-
* * `args.hue` the starting Hue [0, 360]
|
1831
|
-
* * `args.hueSteps` Hue Steps Count [5, 24]
|
1832
|
-
* * `args.lightSteps` Lightness Steps Count [5, 12]
|
1833
|
-
*/
|
1834
|
-
constructor(...args) {
|
1835
|
-
let hue = 0;
|
1836
|
-
let hueSteps = 12;
|
1837
|
-
let lightSteps = 10;
|
1838
|
-
let lightnessArray = [0.5];
|
1839
|
-
|
1840
|
-
if (args.length === 3) {
|
1841
|
-
[hue, hueSteps, lightSteps] = args;
|
1842
|
-
} else if (args.length === 2) {
|
1843
|
-
[hueSteps, lightSteps] = args;
|
1844
|
-
if ([hueSteps, lightSteps].some((n) => n < 1)) {
|
1845
|
-
throw TypeError('ColorPalette: both arguments must be higher than 0.');
|
1846
|
-
}
|
1847
|
-
}
|
1848
|
-
|
1849
|
-
/** @type {*} */
|
1850
|
-
const colors = [];
|
1851
|
-
const hueStep = 360 / hueSteps;
|
1852
|
-
const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
|
1853
|
-
const steps1To13 = [0.25, 0.2, 0.15, 0.11, 0.09, 0.075];
|
1854
|
-
const lightSets = [[1, 2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13]];
|
1855
|
-
const closestSet = lightSets.find((set) => set.includes(lightSteps));
|
1856
|
-
|
1857
|
-
// find a lightStep that won't go beyond black and white
|
1858
|
-
// something within the [10-90] range of lightness
|
1859
|
-
const lightStep = closestSet
|
1860
|
-
? steps1To13[lightSets.indexOf(closestSet)]
|
1861
|
-
: (100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100);
|
1862
|
-
|
1863
|
-
// light tints
|
1864
|
-
for (let i = 1; i < half + 1; i += 1) {
|
1865
|
-
lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
|
1866
|
-
}
|
1867
|
-
|
1868
|
-
// dark tints
|
1869
|
-
for (let i = 1; i < lightSteps - half; i += 1) {
|
1870
|
-
lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
|
1871
|
-
}
|
1872
|
-
|
1873
|
-
// feed `colors` Array
|
1874
|
-
for (let i = 0; i < hueSteps; i += 1) {
|
1875
|
-
const currentHue = ((hue + i * hueStep) % 360) / 360;
|
1876
|
-
lightnessArray.forEach((l) => {
|
1877
|
-
colors.push(new Color({ h: currentHue, s: 1, l }));
|
1878
|
-
});
|
1879
|
-
}
|
1880
|
-
|
1881
|
-
this.hue = hue;
|
1882
|
-
this.hueSteps = hueSteps;
|
1883
|
-
this.lightSteps = lightSteps;
|
1884
|
-
this.colors = colors;
|
1885
|
-
}
|
1886
|
-
}
|
1887
|
-
|
1888
|
-
ObjectAssign(ColorPalette, { Color });
|
1889
|
-
|
1890
|
-
/** @type {Record<string, string>} */
|
1891
|
-
const colorPickerLabels = {
|
1892
|
-
pickerLabel: 'Colour Picker',
|
1893
|
-
appearanceLabel: 'Colour Appearance',
|
1894
|
-
valueLabel: 'Colour Value',
|
1895
|
-
toggleLabel: 'Select Colour',
|
1896
|
-
presetsLabel: 'Colour Presets',
|
1897
|
-
defaultsLabel: 'Colour Defaults',
|
1898
|
-
formatLabel: 'Format',
|
1899
|
-
alphaLabel: 'Alpha',
|
1900
|
-
hexLabel: 'Hexadecimal',
|
1901
|
-
hueLabel: 'Hue',
|
1902
|
-
whitenessLabel: 'Whiteness',
|
1903
|
-
blacknessLabel: 'Blackness',
|
1904
|
-
saturationLabel: 'Saturation',
|
1905
|
-
lightnessLabel: 'Lightness',
|
1906
|
-
redLabel: 'Red',
|
1907
|
-
greenLabel: 'Green',
|
1908
|
-
blueLabel: 'Blue',
|
1909
|
-
};
|
1910
|
-
|
1911
|
-
/**
|
1912
|
-
* A list of 17 color names used for WAI-ARIA compliance.
|
1913
|
-
* @type {string[]}
|
1914
|
-
*/
|
1915
|
-
const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
|
1916
|
-
|
1917
|
-
const tabIndex = 'tabindex';
|
1918
|
-
|
1919
|
-
/**
|
1920
|
-
* Check if a string is valid JSON string.
|
1921
|
-
* @param {string} str the string input
|
1922
|
-
* @returns {boolean} the query result
|
1923
|
-
*/
|
1924
|
-
function isValidJSON(str) {
|
1925
|
-
try {
|
1926
|
-
JSON.parse(str);
|
1927
|
-
} catch (e) {
|
1928
|
-
return false;
|
1929
|
-
}
|
1930
|
-
return true;
|
1931
|
-
}
|
1932
|
-
|
1933
|
-
/**
|
1934
|
-
* Shortcut for `String.toUpperCase()`.
|
1935
|
-
*
|
1936
|
-
* @param {string} source input string
|
1937
|
-
* @returns {string} uppercase output string
|
1938
|
-
*/
|
1939
|
-
const toUpperCase = (source) => source.toUpperCase();
|
1940
|
-
|
1941
|
-
/**
|
1942
|
-
* A global namespace for aria-haspopup.
|
1943
|
-
* @type {string}
|
1944
|
-
*/
|
1945
|
-
const ariaHasPopup = 'aria-haspopup';
|
1946
|
-
|
1947
|
-
/**
|
1948
|
-
* A global namespace for aria-hidden.
|
1949
|
-
* @type {string}
|
1950
|
-
*/
|
1951
|
-
const ariaHidden = 'aria-hidden';
|
1952
|
-
|
1953
|
-
/**
|
1954
|
-
* A global namespace for aria-labelledby.
|
1955
|
-
* @type {string}
|
1956
|
-
*/
|
1957
|
-
const ariaLabelledBy = 'aria-labelledby';
|
1958
|
-
|
1959
|
-
/**
|
1960
|
-
* This is a shortie for `document.createElement` method
|
1961
|
-
* which allows you to create a new `HTMLElement` for a given `tagName`
|
1962
|
-
* or based on an object with specific non-readonly attributes:
|
1963
|
-
* `id`, `className`, `textContent`, `style`, etc.
|
1964
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
|
1965
|
-
*
|
1966
|
-
* @param {Record<string, string> | string} param `tagName` or object
|
1967
|
-
* @return {HTMLElement | Element} a new `HTMLElement` or `Element`
|
1968
|
-
*/
|
1969
|
-
function createElement(param) {
|
1970
|
-
if (typeof param === 'string') {
|
1971
|
-
return getDocument().createElement(param);
|
1972
|
-
}
|
1973
|
-
|
1974
|
-
const { tagName } = param;
|
1975
|
-
const attr = { ...param };
|
1976
|
-
const newElement = createElement(tagName);
|
1977
|
-
delete attr.tagName;
|
1978
|
-
ObjectAssign(newElement, attr);
|
1979
|
-
return newElement;
|
1980
|
-
}
|
1981
|
-
|
1982
|
-
/**
|
1983
|
-
* This is a shortie for `document.createElementNS` method
|
1984
|
-
* which allows you to create a new `HTMLElement` for a given `tagName`
|
1985
|
-
* or based on an object with specific non-readonly attributes:
|
1986
|
-
* `id`, `className`, `textContent`, `style`, etc.
|
1987
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
|
1988
|
-
*
|
1989
|
-
* @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
|
1990
|
-
* @param {Record<string, string> | string} param `tagName` or object
|
1991
|
-
* @return {HTMLElement | Element} a new `HTMLElement` or `Element`
|
1992
|
-
*/
|
1993
|
-
function createElementNS(namespace, param) {
|
1994
|
-
if (typeof param === 'string') {
|
1995
|
-
return getDocument().createElementNS(namespace, param);
|
1996
|
-
}
|
1997
|
-
|
1998
|
-
const { tagName } = param;
|
1999
|
-
const attr = { ...param };
|
2000
|
-
const newElement = createElementNS(namespace, tagName);
|
2001
|
-
delete attr.tagName;
|
2002
|
-
ObjectAssign(newElement, attr);
|
2003
|
-
return newElement;
|
2004
|
-
}
|
2005
|
-
|
2006
|
-
const vHidden = 'v-hidden';
|
2007
|
-
|
2008
|
-
/**
|
2009
|
-
* Returns the color form for `ColorPicker`.
|
2010
|
-
*
|
2011
|
-
* @param {CP.ColorPicker} self the `ColorPicker` instance
|
2012
|
-
* @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
|
2013
|
-
*/
|
2014
|
-
function getColorForm(self) {
|
2015
|
-
const { format, id, componentLabels } = self;
|
2016
|
-
const colorForm = createElement({
|
2017
|
-
tagName: 'div',
|
2018
|
-
className: `color-form ${format}`,
|
2019
|
-
});
|
2020
|
-
|
2021
|
-
let components = ['hex'];
|
2022
|
-
if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
|
2023
|
-
else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
|
2024
|
-
else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
|
2025
|
-
|
2026
|
-
components.forEach((c) => {
|
2027
|
-
const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
|
2028
|
-
const cID = `color_${format}_${c}_${id}`;
|
2029
|
-
const formatLabel = componentLabels[`${c}Label`];
|
2030
|
-
const cInputLabel = createElement({ tagName: 'label' });
|
2031
|
-
setAttribute(cInputLabel, 'for', cID);
|
2032
|
-
cInputLabel.append(
|
2033
|
-
createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
|
2034
|
-
createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
|
2035
|
-
);
|
2036
|
-
const cInput = createElement({
|
2037
|
-
tagName: 'input',
|
2038
|
-
id: cID,
|
2039
|
-
// name: cID, - prevent saving the value to a form
|
2040
|
-
type: format === 'hex' ? 'text' : 'number',
|
2041
|
-
value: c === 'alpha' ? '100' : '0',
|
2042
|
-
className: `color-input ${c}`,
|
2043
|
-
});
|
2044
|
-
setAttribute(cInput, 'autocomplete', 'off');
|
2045
|
-
setAttribute(cInput, 'spellcheck', 'false');
|
2046
|
-
|
2047
|
-
// alpha
|
2048
|
-
let max = '100';
|
2049
|
-
let step = '1';
|
2050
|
-
if (c !== 'alpha') {
|
2051
|
-
if (format === 'rgb') {
|
2052
|
-
max = '255'; step = '1';
|
2053
|
-
} else if (c === 'hue') {
|
2054
|
-
max = '360'; step = '1';
|
2055
|
-
}
|
2056
|
-
}
|
2057
|
-
ObjectAssign(cInput, {
|
2058
|
-
min: '0',
|
2059
|
-
max,
|
2060
|
-
step,
|
2061
|
-
});
|
2062
|
-
colorForm.append(cInputLabel, cInput);
|
2063
|
-
});
|
2064
|
-
return colorForm;
|
2065
|
-
}
|
2066
|
-
|
2067
|
-
/**
|
2068
|
-
* A global namespace for aria-label.
|
2069
|
-
* @type {string}
|
2070
|
-
*/
|
2071
|
-
const ariaLabel = 'aria-label';
|
2072
|
-
|
2073
|
-
/**
|
2074
|
-
* A global namespace for aria-valuemin.
|
2075
|
-
* @type {string}
|
2076
|
-
*/
|
2077
|
-
const ariaValueMin = 'aria-valuemin';
|
2078
|
-
|
2079
|
-
/**
|
2080
|
-
* A global namespace for aria-valuemax.
|
2081
|
-
* @type {string}
|
2082
|
-
*/
|
2083
|
-
const ariaValueMax = 'aria-valuemax';
|
2084
|
-
|
2085
|
-
/**
|
2086
|
-
* Returns all color controls for `ColorPicker`.
|
2087
|
-
*
|
2088
|
-
* @param {CP.ColorPicker} self the `ColorPicker` instance
|
2089
|
-
* @returns {HTMLElement | Element} color controls
|
2090
|
-
*/
|
2091
|
-
function getColorControls(self) {
|
2092
|
-
const { format, componentLabels } = self;
|
2093
|
-
const {
|
2094
|
-
hueLabel, alphaLabel, lightnessLabel, saturationLabel,
|
2095
|
-
whitenessLabel, blacknessLabel,
|
2096
|
-
} = componentLabels;
|
2097
|
-
|
2098
|
-
const max1 = format === 'hsl' ? 360 : 100;
|
2099
|
-
const max2 = format === 'hsl' ? 100 : 360;
|
2100
|
-
const max3 = 100;
|
2101
|
-
|
2102
|
-
let ctrl1Label = format === 'hsl'
|
2103
|
-
? `${hueLabel} & ${lightnessLabel}`
|
2104
|
-
: `${lightnessLabel} & ${saturationLabel}`;
|
2105
|
-
|
2106
|
-
ctrl1Label = format === 'hwb'
|
2107
|
-
? `${whitenessLabel} & ${blacknessLabel}`
|
2108
|
-
: ctrl1Label;
|
2109
|
-
|
2110
|
-
const ctrl2Label = format === 'hsl'
|
2111
|
-
? `${saturationLabel}`
|
2112
|
-
: `${hueLabel}`;
|
2113
|
-
|
2114
|
-
const colorControls = createElement({
|
2115
|
-
tagName: 'div',
|
2116
|
-
className: `color-controls ${format}`,
|
2117
|
-
});
|
2118
|
-
|
2119
|
-
const colorPointer = 'color-pointer';
|
2120
|
-
const colorSlider = 'color-slider';
|
2121
|
-
|
2122
|
-
const controls = [
|
2123
|
-
{
|
2124
|
-
i: 1,
|
2125
|
-
c: colorPointer,
|
2126
|
-
l: ctrl1Label,
|
2127
|
-
min: 0,
|
2128
|
-
max: max1,
|
2129
|
-
},
|
2130
|
-
{
|
2131
|
-
i: 2,
|
2132
|
-
c: colorSlider,
|
2133
|
-
l: ctrl2Label,
|
2134
|
-
min: 0,
|
2135
|
-
max: max2,
|
2136
|
-
},
|
2137
|
-
{
|
2138
|
-
i: 3,
|
2139
|
-
c: colorSlider,
|
2140
|
-
l: alphaLabel,
|
2141
|
-
min: 0,
|
2142
|
-
max: max3,
|
2143
|
-
},
|
2144
|
-
];
|
2145
|
-
|
2146
|
-
controls.forEach((template) => {
|
2147
|
-
const {
|
2148
|
-
i, c, l, min, max,
|
2149
|
-
} = template;
|
2150
|
-
const control = createElement({
|
2151
|
-
tagName: 'div',
|
2152
|
-
className: 'color-control',
|
2153
|
-
});
|
2154
|
-
setAttribute(control, 'role', 'presentation');
|
2155
|
-
|
2156
|
-
control.append(
|
2157
|
-
createElement({
|
2158
|
-
tagName: 'div',
|
2159
|
-
className: `visual-control visual-control${i}`,
|
2160
|
-
}),
|
2161
|
-
);
|
2162
|
-
|
2163
|
-
const knob = createElement({
|
2164
|
-
tagName: 'div',
|
2165
|
-
className: `${c} knob`,
|
2166
|
-
ariaLive: 'polite',
|
2167
|
-
});
|
2168
|
-
|
2169
|
-
setAttribute(knob, ariaLabel, l);
|
2170
|
-
setAttribute(knob, 'role', 'slider');
|
2171
|
-
setAttribute(knob, tabIndex, '0');
|
2172
|
-
setAttribute(knob, ariaValueMin, `${min}`);
|
2173
|
-
setAttribute(knob, ariaValueMax, `${max}`);
|
2174
|
-
control.append(knob);
|
2175
|
-
colorControls.append(control);
|
2176
|
-
});
|
2177
|
-
|
2178
|
-
return colorControls;
|
2179
|
-
}
|
2180
|
-
|
2181
|
-
/**
|
2182
|
-
* Helps setting CSS variables to the color-menu.
|
2183
|
-
* @param {HTMLElement} element
|
2184
|
-
* @param {Record<string,any>} props
|
2185
|
-
*/
|
2186
|
-
function setCSSProperties(element, props) {
|
2187
|
-
ObjectKeys(props).forEach((prop) => {
|
2188
|
-
element.style.setProperty(prop, props[prop]);
|
2189
|
-
});
|
2190
|
-
}
|
2191
|
-
|
2192
|
-
/**
|
2193
|
-
* Returns a color-defaults with given values and class.
|
2194
|
-
* @param {CP.ColorPicker} self
|
2195
|
-
* @param {CP.ColorPalette | string[]} colorsSource
|
2196
|
-
* @param {string} menuClass
|
2197
|
-
* @returns {HTMLElement | Element}
|
2198
|
-
*/
|
2199
|
-
function getColorMenu(self, colorsSource, menuClass) {
|
2200
|
-
const { input, format, componentLabels } = self;
|
2201
|
-
const { defaultsLabel, presetsLabel } = componentLabels;
|
2202
|
-
const isOptionsMenu = menuClass === 'color-options';
|
2203
|
-
const isPalette = colorsSource instanceof ColorPalette;
|
2204
|
-
const menuLabel = isOptionsMenu ? presetsLabel : defaultsLabel;
|
2205
|
-
const colorsArray = isPalette ? colorsSource.colors : colorsSource;
|
2206
|
-
const colorsCount = colorsArray.length;
|
2207
|
-
const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
|
2208
|
-
const fit = lightSteps || [9, 10].find((x) => colorsCount >= x * 2 && !(colorsCount % x)) || 5;
|
2209
|
-
const isMultiLine = isOptionsMenu && colorsCount > fit;
|
2210
|
-
let rowCountHover = 2;
|
2211
|
-
rowCountHover = isMultiLine && colorsCount > fit * 2 ? 3 : rowCountHover;
|
2212
|
-
rowCountHover = isMultiLine && colorsCount > fit * 3 ? 4 : rowCountHover;
|
2213
|
-
rowCountHover = isMultiLine && colorsCount > fit * 4 ? 5 : rowCountHover;
|
2214
|
-
const rowCount = rowCountHover - (colorsCount <= fit * 3 ? 1 : 2);
|
2215
|
-
const isScrollable = isMultiLine && colorsCount > rowCount * fit;
|
2216
|
-
let finalClass = menuClass;
|
2217
|
-
finalClass += isScrollable ? ' scrollable' : '';
|
2218
|
-
finalClass += isMultiLine ? ' multiline' : '';
|
2219
|
-
const gap = isMultiLine ? '1px' : '0.25rem';
|
2220
|
-
let optionSize = isMultiLine ? 1.75 : 2;
|
2221
|
-
optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
|
2222
|
-
const menuHeight = `${rowCount * optionSize}rem`;
|
2223
|
-
const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
|
2224
|
-
/** @type {HTMLUListElement} */
|
2225
|
-
// @ts-ignore -- <UL> is an `HTMLElement`
|
2226
|
-
const menu = createElement({
|
2227
|
-
tagName: 'ul',
|
2228
|
-
className: finalClass,
|
2229
|
-
});
|
2230
|
-
setAttribute(menu, 'role', 'listbox');
|
2231
|
-
setAttribute(menu, ariaLabel, menuLabel);
|
2232
|
-
|
2233
|
-
if (isScrollable) {
|
2234
|
-
setCSSProperties(menu, {
|
2235
|
-
'--grid-item-size': `${optionSize}rem`,
|
2236
|
-
'--grid-fit': fit,
|
2237
|
-
'--grid-gap': gap,
|
2238
|
-
'--grid-height': menuHeight,
|
2239
|
-
'--grid-hover-height': menuHeightHover,
|
2240
|
-
});
|
2241
|
-
}
|
2242
|
-
|
2243
|
-
colorsArray.forEach((x) => {
|
2244
|
-
let [value, label] = typeof x === 'string' ? x.trim().split(':') : [];
|
2245
|
-
if (x instanceof Color) {
|
2246
|
-
value = x.toHexString();
|
2247
|
-
label = value;
|
2248
|
-
}
|
2249
|
-
const color = new Color(x instanceof Color ? x : value, format);
|
2250
|
-
const isActive = color.toString() === getAttribute(input, 'value');
|
2251
|
-
const active = isActive ? ' active' : '';
|
2252
|
-
|
2253
|
-
const option = createElement({
|
2254
|
-
tagName: 'li',
|
2255
|
-
className: `color-option${active}`,
|
2256
|
-
innerText: `${label || value}`,
|
2257
|
-
});
|
2258
|
-
|
2259
|
-
setAttribute(option, tabIndex, '0');
|
2260
|
-
setAttribute(option, 'data-value', `${value}`);
|
2261
|
-
setAttribute(option, 'role', 'option');
|
2262
|
-
setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
|
2263
|
-
|
2264
|
-
if (isOptionsMenu) {
|
2265
|
-
setElementStyle(option, { backgroundColor: value });
|
2266
|
-
}
|
2267
|
-
|
2268
|
-
menu.append(option);
|
2269
|
-
});
|
2270
|
-
return menu;
|
2271
|
-
}
|
2272
|
-
|
2273
|
-
/**
|
2274
|
-
* Generate HTML markup and update instance properties.
|
2275
|
-
* @param {CP.ColorPicker} self
|
2276
|
-
*/
|
2277
|
-
function setMarkup(self) {
|
2278
|
-
const {
|
2279
|
-
input, parent, format, id, componentLabels, colorKeywords, colorPresets,
|
2280
|
-
} = self;
|
2281
|
-
const colorValue = getAttribute(input, 'value') || '#fff';
|
2282
|
-
|
2283
|
-
const {
|
2284
|
-
toggleLabel, pickerLabel, formatLabel, hexLabel,
|
2285
|
-
} = componentLabels;
|
2286
|
-
|
2287
|
-
// update color
|
2288
|
-
const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
|
2289
|
-
self.color = new Color(color, format);
|
2290
|
-
|
2291
|
-
// set initial controls dimensions
|
2292
|
-
const formatString = format === 'hex' ? hexLabel : toUpperCase(format);
|
2293
|
-
|
2294
|
-
const pickerBtn = createElement({
|
2295
|
-
id: `picker-btn-${id}`,
|
2296
|
-
tagName: 'button',
|
2297
|
-
className: 'picker-toggle btn-appearance',
|
2298
|
-
});
|
2299
|
-
setAttribute(pickerBtn, ariaExpanded, 'false');
|
2300
|
-
setAttribute(pickerBtn, ariaHasPopup, 'true');
|
2301
|
-
pickerBtn.append(createElement({
|
2302
|
-
tagName: 'span',
|
2303
|
-
className: vHidden,
|
2304
|
-
innerText: `${pickerLabel}. ${formatLabel}: ${formatString}`,
|
2305
|
-
}));
|
2306
|
-
|
2307
|
-
const pickerDropdown = createElement({
|
2308
|
-
tagName: 'div',
|
2309
|
-
className: 'color-dropdown picker',
|
2310
|
-
});
|
2311
|
-
setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
|
2312
|
-
setAttribute(pickerDropdown, 'role', 'group');
|
2313
|
-
|
2314
|
-
const colorControls = getColorControls(self);
|
2315
|
-
const colorForm = getColorForm(self);
|
2316
|
-
|
2317
|
-
pickerDropdown.append(colorControls, colorForm);
|
2318
|
-
input.before(pickerBtn);
|
2319
|
-
parent.append(pickerDropdown);
|
2320
|
-
|
2321
|
-
// set colour key menu template
|
2322
|
-
if (colorKeywords || colorPresets) {
|
2323
|
-
const presetsDropdown = createElement({
|
2324
|
-
tagName: 'div',
|
2325
|
-
className: 'color-dropdown scrollable menu',
|
2326
|
-
});
|
2327
|
-
|
2328
|
-
// color presets
|
2329
|
-
if (colorPresets) {
|
2330
|
-
presetsDropdown.append(getColorMenu(self, colorPresets, 'color-options'));
|
2331
|
-
}
|
2332
|
-
|
2333
|
-
// explicit defaults [reset, initial, inherit, transparent, currentColor]
|
2334
|
-
// also custom defaults [default: #069, complementary: #930]
|
2335
|
-
if (colorKeywords && colorKeywords.length) {
|
2336
|
-
presetsDropdown.append(getColorMenu(self, colorKeywords, 'color-defaults'));
|
2337
|
-
}
|
2338
|
-
|
2339
|
-
const presetsBtn = createElement({
|
2340
|
-
tagName: 'button',
|
2341
|
-
className: 'menu-toggle btn-appearance',
|
2342
|
-
});
|
2343
|
-
setAttribute(presetsBtn, tabIndex, '-1');
|
2344
|
-
setAttribute(presetsBtn, ariaExpanded, 'false');
|
2345
|
-
setAttribute(presetsBtn, ariaHasPopup, 'true');
|
2346
|
-
|
2347
|
-
const xmlns = encodeURI('http://www.w3.org/2000/svg');
|
2348
|
-
const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
|
2349
|
-
setAttribute(presetsIcon, 'xmlns', xmlns);
|
2350
|
-
setAttribute(presetsIcon, 'viewBox', '0 0 512 512');
|
2351
|
-
setAttribute(presetsIcon, ariaHidden, 'true');
|
2352
|
-
|
2353
|
-
const path = createElementNS(xmlns, { tagName: 'path' });
|
2354
|
-
setAttribute(path, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
|
2355
|
-
setAttribute(path, 'fill', '#fff');
|
2356
|
-
presetsIcon.append(path);
|
2357
|
-
presetsBtn.append(createElement({
|
2358
|
-
tagName: 'span',
|
2359
|
-
className: vHidden,
|
2360
|
-
innerText: `${toggleLabel}`,
|
2361
|
-
}), presetsIcon);
|
2362
|
-
|
2363
|
-
parent.append(presetsBtn, presetsDropdown);
|
2364
|
-
}
|
2365
|
-
|
2366
|
-
// solve non-colors after settings save
|
2367
|
-
if (colorKeywords && nonColors.includes(colorValue)) {
|
2368
|
-
self.value = colorValue;
|
2369
|
-
}
|
2370
|
-
setAttribute(input, tabIndex, '-1');
|
2371
|
-
}
|
2372
|
-
|
2373
|
-
var version = "1.0.1";
|
2374
|
-
|
2375
|
-
const Version = version;
|
2376
|
-
|
2377
|
-
// ColorPicker GC
|
2378
|
-
// ==============
|
2379
|
-
const colorPickerString = 'color-picker';
|
2380
|
-
const colorPickerSelector = `[data-function="${colorPickerString}"]`;
|
2381
|
-
const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
|
2382
|
-
const colorPickerDefaults = {
|
2383
|
-
componentLabels: colorPickerLabels,
|
2384
|
-
colorLabels: colorNames,
|
2385
|
-
format: 'rgb',
|
2386
|
-
colorPresets: false,
|
2387
|
-
colorKeywords: false,
|
2388
|
-
};
|
2389
|
-
|
2390
|
-
// ColorPicker Static Methods
|
2391
|
-
// ==========================
|
2392
|
-
|
2393
|
-
/** @type {CP.GetInstance<ColorPicker, HTMLInputElement>} */
|
2394
|
-
const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
|
2395
|
-
|
2396
|
-
/** @type {CP.InitCallback<ColorPicker>} */
|
2397
|
-
const initColorPicker = (element) => new ColorPicker(element);
|
2398
|
-
|
2399
|
-
// ColorPicker Private Methods
|
2400
|
-
// ===========================
|
2401
|
-
|
2402
|
-
/**
|
2403
|
-
* Add / remove `ColorPicker` main event listeners.
|
2404
|
-
* @param {ColorPicker} self
|
2405
|
-
* @param {boolean=} action
|
2406
|
-
*/
|
2407
|
-
function toggleEvents(self, action) {
|
2408
|
-
const fn = action ? addListener : removeListener;
|
2409
|
-
const { input, pickerToggle, menuToggle } = self;
|
2410
|
-
|
2411
|
-
fn(input, focusinEvent, self.showPicker);
|
2412
|
-
fn(pickerToggle, mouseclickEvent, self.togglePicker);
|
2413
|
-
|
2414
|
-
if (menuToggle) {
|
2415
|
-
fn(menuToggle, mouseclickEvent, self.toggleMenu);
|
2416
|
-
}
|
2417
|
-
}
|
2418
|
-
|
2419
|
-
/**
|
2420
|
-
* Add / remove `ColorPicker` event listeners active only when open.
|
2421
|
-
* @param {ColorPicker} self
|
2422
|
-
* @param {boolean=} action
|
2423
|
-
*/
|
2424
|
-
function toggleEventsOnShown(self, action) {
|
2425
|
-
const fn = action ? addListener : removeListener;
|
2426
|
-
const { input, colorMenu, parent } = self;
|
2427
|
-
const doc = getDocument(input);
|
2428
|
-
const win = doc.defaultView;
|
2429
|
-
|
2430
|
-
fn(self.controls, pointerdownEvent, self.pointerDown);
|
2431
|
-
self.controlKnobs.forEach((x) => fn(x, keydownEvent, self.handleKnobs));
|
2432
|
-
|
2433
|
-
fn(win, scrollEvent, self.handleScroll);
|
2434
|
-
fn(win, resizeEvent, self.update);
|
2435
|
-
|
2436
|
-
[input, ...self.inputs].forEach((x) => fn(x, changeEvent, self.changeHandler));
|
2437
|
-
|
2438
|
-
if (colorMenu) {
|
2439
|
-
fn(colorMenu, mouseclickEvent, self.menuClickHandler);
|
2440
|
-
fn(colorMenu, keydownEvent, self.menuKeyHandler);
|
2441
|
-
}
|
2442
|
-
|
2443
|
-
fn(doc, pointermoveEvent, self.pointerMove);
|
2444
|
-
fn(doc, pointerupEvent, self.pointerUp);
|
2445
|
-
fn(parent, focusoutEvent, self.handleFocusOut);
|
2446
|
-
fn(doc, keyupEvent, self.handleDismiss);
|
2447
|
-
}
|
2448
|
-
|
2449
|
-
/**
|
2450
|
-
* Triggers the `ColorPicker` original event.
|
2451
|
-
* @param {ColorPicker} self
|
2452
|
-
*/
|
2453
|
-
function firePickerChange(self) {
|
2454
|
-
dispatchEvent(self.input, new CustomEvent('colorpicker.change'));
|
2455
|
-
}
|
2456
|
-
|
2457
|
-
/**
|
2458
|
-
* Hides a visible dropdown.
|
2459
|
-
* @param {HTMLElement} element
|
2460
|
-
* @returns {void}
|
2461
|
-
*/
|
2462
|
-
function removePosition(element) {
|
2463
|
-
/* istanbul ignore else */
|
2464
|
-
if (element) {
|
2465
|
-
['bottom', 'top'].forEach((x) => removeClass(element, x));
|
2466
|
-
}
|
2467
|
-
}
|
2468
|
-
|
2469
|
-
/**
|
2470
|
-
* Shows a `ColorPicker` dropdown and close the curent open dropdown.
|
2471
|
-
* @param {ColorPicker} self
|
2472
|
-
* @param {HTMLElement | Element} dropdown
|
2473
|
-
*/
|
2474
|
-
function showDropdown(self, dropdown) {
|
2475
|
-
const {
|
2476
|
-
colorPicker, colorMenu, menuToggle, pickerToggle, parent,
|
2477
|
-
} = self;
|
2478
|
-
const isPicker = dropdown === colorPicker;
|
2479
|
-
const openDropdown = isPicker ? colorMenu : colorPicker;
|
2480
|
-
const activeBtn = isPicker ? menuToggle : pickerToggle;
|
2481
|
-
const nextBtn = !isPicker ? menuToggle : pickerToggle;
|
2482
|
-
|
2483
|
-
if (!hasClass(parent, 'open')) {
|
2484
|
-
addClass(parent, 'open');
|
2485
|
-
}
|
2486
|
-
if (openDropdown) {
|
2487
|
-
removeClass(openDropdown, 'show');
|
2488
|
-
removePosition(openDropdown);
|
2489
|
-
}
|
2490
|
-
addClass(dropdown, 'bottom');
|
2491
|
-
reflow(dropdown);
|
2492
|
-
addClass(dropdown, 'show');
|
2493
|
-
|
2494
|
-
if (isPicker) self.update();
|
2495
|
-
|
2496
|
-
if (!self.isOpen) {
|
2497
|
-
toggleEventsOnShown(self, true);
|
2498
|
-
self.updateDropdownPosition();
|
2499
|
-
self.isOpen = true;
|
2500
|
-
setAttribute(self.input, tabIndex, '0');
|
2501
|
-
if (menuToggle) {
|
2502
|
-
setAttribute(menuToggle, tabIndex, '0');
|
2503
|
-
}
|
2504
|
-
}
|
2505
|
-
|
2506
|
-
setAttribute(nextBtn, ariaExpanded, 'true');
|
2507
|
-
if (activeBtn) {
|
2508
|
-
setAttribute(activeBtn, ariaExpanded, 'false');
|
2509
|
-
}
|
2510
|
-
}
|
2511
|
-
|
2512
|
-
/**
|
2513
|
-
* Color Picker Web Component
|
2514
|
-
* @see http://thednp.github.io/color-picker
|
2515
|
-
*/
|
2516
|
-
class ColorPicker {
|
2517
|
-
/**
|
2518
|
-
* Returns a new `ColorPicker` instance. The target of this constructor
|
2519
|
-
* must be an `HTMLInputElement`.
|
2520
|
-
*
|
2521
|
-
* @param {HTMLInputElement | string} target the target `<input>` element
|
2522
|
-
* @param {CP.ColorPickerOptions=} config instance options
|
2523
|
-
*/
|
2524
|
-
constructor(target, config) {
|
2525
|
-
const self = this;
|
2526
|
-
/** @type {HTMLInputElement} */
|
2527
|
-
const input = querySelector(target);
|
2528
|
-
|
2529
|
-
// invalidate
|
2530
|
-
if (!input) throw new TypeError(`ColorPicker target "${target}" cannot be found.`);
|
2531
|
-
self.input = input;
|
2532
|
-
|
2533
|
-
const parent = closest(input, colorPickerParentSelector);
|
2534
|
-
if (!parent) throw new TypeError('ColorPicker requires a specific markup to work.');
|
2535
|
-
|
2536
|
-
/** @type {HTMLElement} */
|
2537
|
-
self.parent = parent;
|
2538
|
-
|
2539
|
-
/** @type {number} */
|
2540
|
-
self.id = getUID(input, colorPickerString);
|
2541
|
-
|
2542
|
-
// set initial state
|
2543
|
-
/** @type {HTMLElement?} */
|
2544
|
-
self.dragElement = null;
|
2545
|
-
/** @type {boolean} */
|
2546
|
-
self.isOpen = false;
|
2547
|
-
/** @type {Record<string, number>} */
|
2548
|
-
self.controlPositions = {
|
2549
|
-
c1x: 0, c1y: 0, c2y: 0, c3y: 0,
|
2550
|
-
};
|
2551
|
-
/** @type {Record<string, string>} */
|
2552
|
-
self.colorLabels = {};
|
2553
|
-
/** @type {string[]=} */
|
2554
|
-
self.colorKeywords = undefined;
|
2555
|
-
/** @type {(ColorPalette | string[])=} */
|
2556
|
-
self.colorPresets = undefined;
|
2557
|
-
|
2558
|
-
// process options
|
2559
|
-
const {
|
2560
|
-
format, componentLabels, colorLabels, colorKeywords, colorPresets,
|
2561
|
-
} = normalizeOptions(this.isCE ? parent : input, colorPickerDefaults, config || {});
|
2562
|
-
|
2563
|
-
let translatedColorLabels = colorNames;
|
2564
|
-
/* istanbul ignore else */
|
2565
|
-
if (colorLabels instanceof Array && colorLabels.length === 17) {
|
2566
|
-
translatedColorLabels = colorLabels;
|
2567
|
-
} else if (colorLabels && colorLabels.split(',').length === 17) {
|
2568
|
-
translatedColorLabels = colorLabels.split(',');
|
2569
|
-
}
|
2570
|
-
|
2571
|
-
// expose colour labels to all methods
|
2572
|
-
colorNames.forEach((c, i) => {
|
2573
|
-
self.colorLabels[c] = translatedColorLabels[i].trim();
|
2574
|
-
});
|
2575
|
-
|
2576
|
-
// update and expose component labels
|
2577
|
-
const tempComponentLabels = componentLabels && isValidJSON(componentLabels)
|
2578
|
-
? JSON.parse(componentLabels) : componentLabels;
|
2579
|
-
|
2580
|
-
/** @type {Record<string, string>} */
|
2581
|
-
self.componentLabels = ObjectAssign({ ...colorPickerLabels }, tempComponentLabels);
|
2582
|
-
|
2583
|
-
/** @type {Color} */
|
2584
|
-
self.color = new Color(input.value || '#fff', format);
|
2585
|
-
|
2586
|
-
/** @type {CP.ColorFormats} */
|
2587
|
-
self.format = format;
|
2588
|
-
|
2589
|
-
// set colour defaults
|
2590
|
-
if (colorKeywords instanceof Array && colorKeywords.length) {
|
2591
|
-
self.colorKeywords = colorKeywords;
|
2592
|
-
} else if (typeof colorKeywords === 'string' && colorKeywords.length) {
|
2593
|
-
self.colorKeywords = colorKeywords.split(',').map((x) => x.trim());
|
2594
|
-
}
|
2595
|
-
|
2596
|
-
// set colour presets
|
2597
|
-
if (colorPresets instanceof Array && colorPresets.length) {
|
2598
|
-
self.colorPresets = colorPresets;
|
2599
|
-
} else if (typeof colorPresets === 'string' && colorPresets.length) {
|
2600
|
-
if (isValidJSON(colorPresets)) {
|
2601
|
-
const { hue, hueSteps, lightSteps } = JSON.parse(colorPresets);
|
2602
|
-
self.colorPresets = new ColorPalette(hue, hueSteps, lightSteps);
|
2603
|
-
} else {
|
2604
|
-
self.colorPresets = colorPresets.split(',').map((x) => x.trim());
|
2605
|
-
}
|
2606
|
-
}
|
2607
|
-
|
2608
|
-
// bind events
|
2609
|
-
self.showPicker = self.showPicker.bind(self);
|
2610
|
-
self.togglePicker = self.togglePicker.bind(self);
|
2611
|
-
self.toggleMenu = self.toggleMenu.bind(self);
|
2612
|
-
self.menuClickHandler = self.menuClickHandler.bind(self);
|
2613
|
-
self.menuKeyHandler = self.menuKeyHandler.bind(self);
|
2614
|
-
self.pointerDown = self.pointerDown.bind(self);
|
2615
|
-
self.pointerMove = self.pointerMove.bind(self);
|
2616
|
-
self.pointerUp = self.pointerUp.bind(self);
|
2617
|
-
self.update = self.update.bind(self);
|
2618
|
-
self.handleScroll = self.handleScroll.bind(self);
|
2619
|
-
self.handleFocusOut = self.handleFocusOut.bind(self);
|
2620
|
-
self.changeHandler = self.changeHandler.bind(self);
|
2621
|
-
self.handleDismiss = self.handleDismiss.bind(self);
|
2622
|
-
self.handleKnobs = self.handleKnobs.bind(self);
|
2623
|
-
|
2624
|
-
// generate markup
|
2625
|
-
setMarkup(self);
|
2626
|
-
|
2627
|
-
const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
|
2628
|
-
// set main elements
|
2629
|
-
/** @type {HTMLElement} */
|
2630
|
-
self.pickerToggle = querySelector('.picker-toggle', parent);
|
2631
|
-
/** @type {HTMLElement} */
|
2632
|
-
self.menuToggle = querySelector('.menu-toggle', parent);
|
2633
|
-
/** @type {HTMLElement} */
|
2634
|
-
self.colorPicker = colorPicker;
|
2635
|
-
/** @type {HTMLElement} */
|
2636
|
-
self.colorMenu = colorMenu;
|
2637
|
-
/** @type {HTMLInputElement[]} */
|
2638
|
-
self.inputs = [...getElementsByClassName('color-input', parent)];
|
2639
|
-
const [controls] = getElementsByClassName('color-controls', parent);
|
2640
|
-
self.controls = controls;
|
2641
|
-
/** @type {(HTMLElement | Element)[]} */
|
2642
|
-
self.controlKnobs = [...getElementsByClassName('knob', controls)];
|
2643
|
-
/** @type {(HTMLElement)[]} */
|
2644
|
-
self.visuals = [...getElementsByClassName('visual-control', controls)];
|
2645
|
-
|
2646
|
-
// update colour picker controls, inputs and visuals
|
2647
|
-
self.update();
|
2648
|
-
|
2649
|
-
// add main events listeners
|
2650
|
-
toggleEvents(self, true);
|
2651
|
-
|
2652
|
-
// set component data
|
2653
|
-
Data.set(input, colorPickerString, self);
|
2654
|
-
}
|
2655
|
-
|
2656
|
-
/** Returns the current colour value */
|
2657
|
-
get value() { return this.input.value; }
|
2658
|
-
|
2659
|
-
/**
|
2660
|
-
* Sets a new colour value.
|
2661
|
-
* @param {string} v new colour value
|
2662
|
-
*/
|
2663
|
-
set value(v) { this.input.value = v; }
|
2664
|
-
|
2665
|
-
/** Check if the colour presets include any non-colour. */
|
2666
|
-
get hasNonColor() {
|
2667
|
-
return this.colorKeywords instanceof Array
|
2668
|
-
&& this.colorKeywords.some((x) => nonColors.includes(x));
|
2669
|
-
}
|
2670
|
-
|
2671
|
-
/** Check if the parent of the target is a `ColorPickerElement` instance. */
|
2672
|
-
get isCE() { return this.parent.localName === colorPickerString; }
|
2673
|
-
|
2674
|
-
/** Returns hexadecimal value of the current colour. */
|
2675
|
-
get hex() { return this.color.toHex(true); }
|
2676
|
-
|
2677
|
-
/** Returns the current colour value in {h,s,v,a} object format. */
|
2678
|
-
get hsv() { return this.color.toHsv(); }
|
2679
|
-
|
2680
|
-
/** Returns the current colour value in {h,s,l,a} object format. */
|
2681
|
-
get hsl() { return this.color.toHsl(); }
|
2682
|
-
|
2683
|
-
/** Returns the current colour value in {h,w,b,a} object format. */
|
2684
|
-
get hwb() { return this.color.toHwb(); }
|
2685
|
-
|
2686
|
-
/** Returns the current colour value in {r,g,b,a} object format. */
|
2687
|
-
get rgb() { return this.color.toRgb(); }
|
2688
|
-
|
2689
|
-
/** Returns the current colour brightness. */
|
2690
|
-
get brightness() { return this.color.brightness; }
|
2691
|
-
|
2692
|
-
/** Returns the current colour luminance. */
|
2693
|
-
get luminance() { return this.color.luminance; }
|
2694
|
-
|
2695
|
-
/** Checks if the current colour requires a light text colour. */
|
2696
|
-
get isDark() {
|
2697
|
-
const { color, brightness } = this;
|
2698
|
-
return brightness < 120 && color.a > 0.33;
|
2699
|
-
}
|
2700
|
-
|
2701
|
-
/** Checks if the current input value is a valid colour. */
|
2702
|
-
get isValid() {
|
2703
|
-
const inputValue = this.input.value;
|
2704
|
-
return inputValue !== '' && new Color(inputValue).isValid;
|
2705
|
-
}
|
2706
|
-
|
2707
|
-
/** Returns the colour appearance, usually the closest colour name for the current value. */
|
2708
|
-
get appearance() {
|
2709
|
-
const {
|
2710
|
-
colorLabels, hsl, hsv, format,
|
2711
|
-
} = this;
|
2712
|
-
|
2713
|
-
const hue = roundPart(hsl.h * 360);
|
2714
|
-
const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
|
2715
|
-
const saturation = roundPart(saturationSource * 100);
|
2716
|
-
const lightness = roundPart(hsl.l * 100);
|
2717
|
-
const hsvl = hsv.v * 100;
|
2718
|
-
|
2719
|
-
let colorName;
|
2720
|
-
|
2721
|
-
// determine color appearance
|
2722
|
-
/* istanbul ignore else */
|
2723
|
-
if (lightness === 100 && saturation === 0) {
|
2724
|
-
colorName = colorLabels.white;
|
2725
|
-
} else if (lightness === 0) {
|
2726
|
-
colorName = colorLabels.black;
|
2727
|
-
} else if (saturation === 0) {
|
2728
|
-
colorName = colorLabels.grey;
|
2729
|
-
} else if (hue < 15 || hue >= 345) {
|
2730
|
-
colorName = colorLabels.red;
|
2731
|
-
} else if (hue >= 15 && hue < 45) {
|
2732
|
-
colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
|
2733
|
-
} else if (hue >= 45 && hue < 75) {
|
2734
|
-
const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
|
2735
|
-
const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
|
2736
|
-
colorName = isGold ? colorLabels.gold : colorLabels.yellow;
|
2737
|
-
colorName = isOlive ? colorLabels.olive : colorName;
|
2738
|
-
} else if (hue >= 75 && hue < 155) {
|
2739
|
-
colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
|
2740
|
-
} else if (hue >= 155 && hue < 175) {
|
2741
|
-
colorName = colorLabels.teal;
|
2742
|
-
} else if (hue >= 175 && hue < 195) {
|
2743
|
-
colorName = colorLabels.cyan;
|
2744
|
-
} else if (hue >= 195 && hue < 255) {
|
2745
|
-
colorName = colorLabels.blue;
|
2746
|
-
} else if (hue >= 255 && hue < 270) {
|
2747
|
-
colorName = colorLabels.violet;
|
2748
|
-
} else if (hue >= 270 && hue < 295) {
|
2749
|
-
colorName = colorLabels.magenta;
|
2750
|
-
} else if (hue >= 295 && hue < 345) {
|
2751
|
-
colorName = colorLabels.pink;
|
2752
|
-
}
|
2753
|
-
return colorName;
|
2754
|
-
}
|
2755
|
-
|
2756
|
-
/** Updates `ColorPicker` visuals. */
|
2757
|
-
updateVisuals() {
|
2758
|
-
const self = this;
|
2759
|
-
const {
|
2760
|
-
controlPositions, visuals,
|
2761
|
-
} = self;
|
2762
|
-
const [v1, v2, v3] = visuals;
|
2763
|
-
const { offsetHeight } = v1;
|
2764
|
-
const hue = controlPositions.c2y / offsetHeight;
|
2765
|
-
const { r, g, b } = new Color({ h: hue, s: 1, l: 0.5 }).toRgb();
|
2766
|
-
const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
|
2767
|
-
const alpha = 1 - controlPositions.c3y / offsetHeight;
|
2768
|
-
const roundA = roundPart((alpha * 100)) / 100;
|
2769
|
-
|
2770
|
-
const fill = new Color({
|
2771
|
-
h: hue, s: 1, l: 0.5, a: alpha,
|
2772
|
-
}).toRgbString();
|
2773
|
-
const hueGradient = `linear-gradient(
|
2774
|
-
rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
|
2775
|
-
rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
|
2776
|
-
rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
|
2777
|
-
rgb(255,0,0) 100%)`;
|
2778
|
-
setElementStyle(v1, {
|
2779
|
-
background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
|
2780
|
-
linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
|
2781
|
-
${whiteGrad}`,
|
2782
|
-
});
|
2783
|
-
setElementStyle(v2, { background: hueGradient });
|
2784
|
-
|
2785
|
-
setElementStyle(v3, {
|
2786
|
-
background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
|
2787
|
-
});
|
2788
|
-
}
|
2789
|
-
|
2790
|
-
/**
|
2791
|
-
* The `ColorPicker` *focusout* event listener when open.
|
2792
|
-
* @param {FocusEvent} e
|
2793
|
-
* @this {ColorPicker}
|
2794
|
-
*/
|
2795
|
-
handleFocusOut({ relatedTarget }) {
|
2796
|
-
if (relatedTarget && !this.parent.contains(relatedTarget)) {
|
2797
|
-
this.hide(true);
|
2798
|
-
}
|
2799
|
-
}
|
2800
|
-
|
2801
|
-
/**
|
2802
|
-
* The `ColorPicker` *keyup* event listener when open.
|
2803
|
-
* @param {KeyboardEvent} e
|
2804
|
-
* @this {ColorPicker}
|
2805
|
-
*/
|
2806
|
-
handleDismiss({ code }) {
|
2807
|
-
const self = this;
|
2808
|
-
if (self.isOpen && code === keyEscape) {
|
2809
|
-
self.hide();
|
2810
|
-
}
|
2811
|
-
}
|
2812
|
-
|
2813
|
-
/**
|
2814
|
-
* The `ColorPicker` *scroll* event listener when open.
|
2815
|
-
* @param {Event} e
|
2816
|
-
* @this {ColorPicker}
|
2817
|
-
*/
|
2818
|
-
handleScroll(e) {
|
2819
|
-
const self = this;
|
2820
|
-
const { activeElement } = getDocument(self.input);
|
2821
|
-
|
2822
|
-
self.updateDropdownPosition();
|
2823
|
-
|
2824
|
-
/* istanbul ignore next */
|
2825
|
-
if (([pointermoveEvent, touchmoveEvent].includes(e.type) && self.dragElement)
|
2826
|
-
|| (activeElement && self.controlKnobs.includes(activeElement))) {
|
2827
|
-
e.stopPropagation();
|
2828
|
-
e.preventDefault();
|
2829
|
-
}
|
2830
|
-
}
|
2831
|
-
|
2832
|
-
/**
|
2833
|
-
* The `ColorPicker` keyboard event listener for menu navigation.
|
2834
|
-
* @param {KeyboardEvent} e
|
2835
|
-
* @this {ColorPicker}
|
2836
|
-
*/
|
2837
|
-
menuKeyHandler(e) {
|
2838
|
-
const { target, code } = e;
|
2839
|
-
const { previousElementSibling, nextElementSibling, parentElement } = target;
|
2840
|
-
const isColorOptionsMenu = parentElement && hasClass(parentElement, 'color-options');
|
2841
|
-
const allSiblings = [...parentElement.children];
|
2842
|
-
const columnsCount = isColorOptionsMenu
|
2843
|
-
&& getElementStyle(parentElement, 'grid-template-columns').split(' ').length;
|
2844
|
-
const currentIndex = allSiblings.indexOf(target);
|
2845
|
-
const previousElement = currentIndex > -1
|
2846
|
-
&& columnsCount && allSiblings[currentIndex - columnsCount];
|
2847
|
-
const nextElement = currentIndex > -1
|
2848
|
-
&& columnsCount && allSiblings[currentIndex + columnsCount];
|
2849
|
-
|
2850
|
-
if ([keyArrowDown, keyArrowUp, keySpace].includes(code)) {
|
2851
|
-
// prevent scroll when navigating the menu via arrow keys / Space
|
2852
|
-
e.preventDefault();
|
2853
|
-
}
|
2854
|
-
if (isColorOptionsMenu) {
|
2855
|
-
if (previousElement && code === keyArrowUp) {
|
2856
|
-
focus(previousElement);
|
2857
|
-
} else if (nextElement && code === keyArrowDown) {
|
2858
|
-
focus(nextElement);
|
2859
|
-
} else if (previousElementSibling && code === keyArrowLeft) {
|
2860
|
-
focus(previousElementSibling);
|
2861
|
-
} else if (nextElementSibling && code === keyArrowRight) {
|
2862
|
-
focus(nextElementSibling);
|
2863
|
-
}
|
2864
|
-
} else if (previousElementSibling && [keyArrowLeft, keyArrowUp].includes(code)) {
|
2865
|
-
focus(previousElementSibling);
|
2866
|
-
} else if (nextElementSibling && [keyArrowRight, keyArrowDown].includes(code)) {
|
2867
|
-
focus(nextElementSibling);
|
2868
|
-
}
|
2869
|
-
|
2870
|
-
if ([keyEnter, keySpace].includes(code)) {
|
2871
|
-
this.menuClickHandler({ target });
|
2872
|
-
}
|
2873
|
-
}
|
2874
|
-
|
2875
|
-
/**
|
2876
|
-
* The `ColorPicker` click event listener for the colour menu presets / defaults.
|
2877
|
-
* @param {Event} e
|
2878
|
-
* @this {ColorPicker}
|
2879
|
-
*/
|
2880
|
-
menuClickHandler(e) {
|
2881
|
-
const self = this;
|
2882
|
-
const { target } = e;
|
2883
|
-
const { colorMenu } = self;
|
2884
|
-
const newOption = (getAttribute(target, 'data-value') || '').trim();
|
2885
|
-
// invalidate for targets other than color options
|
2886
|
-
if (!newOption.length) return;
|
2887
|
-
const currentActive = querySelector('li.active', colorMenu);
|
2888
|
-
let newColor = newOption;
|
2889
|
-
newColor = nonColors.includes(newColor) ? 'white' : newColor;
|
2890
|
-
newColor = newColor === 'transparent' ? 'rgba(0,0,0,0)' : newColor;
|
2891
|
-
|
2892
|
-
const {
|
2893
|
-
r, g, b, a,
|
2894
|
-
} = new Color(newColor);
|
2895
|
-
|
2896
|
-
ObjectAssign(self.color, {
|
2897
|
-
r, g, b, a,
|
2898
|
-
});
|
2899
|
-
|
2900
|
-
self.update();
|
2901
|
-
|
2902
|
-
/* istanbul ignore else */
|
2903
|
-
if (currentActive !== target) {
|
2904
|
-
/* istanbul ignore else */
|
2905
|
-
if (currentActive) {
|
2906
|
-
removeClass(currentActive, 'active');
|
2907
|
-
removeAttribute(currentActive, ariaSelected);
|
2908
|
-
}
|
2909
|
-
|
2910
|
-
addClass(target, 'active');
|
2911
|
-
setAttribute(target, ariaSelected, 'true');
|
2912
|
-
|
2913
|
-
if (nonColors.includes(newOption)) {
|
2914
|
-
self.value = newOption;
|
2915
|
-
}
|
2916
|
-
firePickerChange(self);
|
2917
|
-
}
|
2918
|
-
}
|
2919
|
-
|
2920
|
-
/**
|
2921
|
-
* The `ColorPicker` *touchstart* / *mousedown* events listener for control knobs.
|
2922
|
-
* @param {PointerEvent} e
|
2923
|
-
* @this {ColorPicker}
|
2924
|
-
*/
|
2925
|
-
pointerDown(e) {
|
2926
|
-
const self = this;
|
2927
|
-
/** @type {*} */
|
2928
|
-
const { target, pageX, pageY } = e;
|
2929
|
-
const { colorMenu, visuals, controlKnobs } = self;
|
2930
|
-
const [v1, v2, v3] = visuals;
|
2931
|
-
const [c1, c2, c3] = controlKnobs;
|
2932
|
-
/** @type {HTMLElement} */
|
2933
|
-
const visual = controlKnobs.includes(target) ? target.previousElementSibling : target;
|
2934
|
-
const visualRect = getBoundingClientRect(visual);
|
2935
|
-
const html = getDocumentElement(v1);
|
2936
|
-
const offsetX = pageX - html.scrollLeft - visualRect.left;
|
2937
|
-
const offsetY = pageY - html.scrollTop - visualRect.top;
|
2938
|
-
|
2939
|
-
/* istanbul ignore else */
|
2940
|
-
if (target === v1 || target === c1) {
|
2941
|
-
self.dragElement = visual;
|
2942
|
-
self.changeControl1(offsetX, offsetY);
|
2943
|
-
} else if (target === v2 || target === c2) {
|
2944
|
-
self.dragElement = visual;
|
2945
|
-
self.changeControl2(offsetY);
|
2946
|
-
} else if (target === v3 || target === c3) {
|
2947
|
-
self.dragElement = visual;
|
2948
|
-
self.changeAlpha(offsetY);
|
2949
|
-
}
|
2950
|
-
|
2951
|
-
if (colorMenu) {
|
2952
|
-
const currentActive = querySelector('li.active', colorMenu);
|
2953
|
-
if (currentActive) {
|
2954
|
-
removeClass(currentActive, 'active');
|
2955
|
-
removeAttribute(currentActive, ariaSelected);
|
2956
|
-
}
|
2957
|
-
}
|
2958
|
-
e.preventDefault();
|
2959
|
-
}
|
2960
|
-
|
2961
|
-
/**
|
2962
|
-
* The `ColorPicker` *touchend* / *mouseup* events listener for control knobs.
|
2963
|
-
* @param {PointerEvent} e
|
2964
|
-
* @this {ColorPicker}
|
2965
|
-
*/
|
2966
|
-
pointerUp({ target }) {
|
2967
|
-
const self = this;
|
2968
|
-
const { parent } = self;
|
2969
|
-
const doc = getDocument(parent);
|
2970
|
-
const currentOpen = querySelector(`${colorPickerParentSelector}.open`, doc) !== null;
|
2971
|
-
const selection = doc.getSelection();
|
2972
|
-
|
2973
|
-
if (!self.dragElement && !selection.toString().length
|
2974
|
-
&& !parent.contains(target)) {
|
2975
|
-
self.hide(currentOpen);
|
2976
|
-
}
|
2977
|
-
|
2978
|
-
self.dragElement = null;
|
2979
|
-
}
|
2980
|
-
|
2981
|
-
/**
|
2982
|
-
* The `ColorPicker` *touchmove* / *mousemove* events listener for control knobs.
|
2983
|
-
* @param {PointerEvent} e
|
2984
|
-
*/
|
2985
|
-
pointerMove(e) {
|
2986
|
-
const self = this;
|
2987
|
-
const { dragElement, visuals } = self;
|
2988
|
-
const [v1, v2, v3] = visuals;
|
2989
|
-
const { pageX, pageY } = e;
|
2990
|
-
|
2991
|
-
if (!dragElement) return;
|
2992
|
-
|
2993
|
-
const controlRect = getBoundingClientRect(dragElement);
|
2994
|
-
const win = getDocumentElement(v1);
|
2995
|
-
const offsetX = pageX - win.scrollLeft - controlRect.left;
|
2996
|
-
const offsetY = pageY - win.scrollTop - controlRect.top;
|
2997
|
-
|
2998
|
-
if (dragElement === v1) {
|
2999
|
-
self.changeControl1(offsetX, offsetY);
|
3000
|
-
}
|
3001
|
-
|
3002
|
-
if (dragElement === v2) {
|
3003
|
-
self.changeControl2(offsetY);
|
3004
|
-
}
|
3005
|
-
|
3006
|
-
if (dragElement === v3) {
|
3007
|
-
self.changeAlpha(offsetY);
|
3008
|
-
}
|
3009
|
-
}
|
3010
|
-
|
3011
|
-
/**
|
3012
|
-
* The `ColorPicker` *keydown* event listener for control knobs.
|
3013
|
-
* @param {KeyboardEvent} e
|
3014
|
-
*/
|
3015
|
-
handleKnobs(e) {
|
3016
|
-
const { target, code } = e;
|
3017
|
-
const self = this;
|
3018
|
-
|
3019
|
-
// only react to arrow buttons
|
3020
|
-
if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
|
3021
|
-
e.preventDefault();
|
3022
|
-
|
3023
|
-
const { controlKnobs, visuals } = self;
|
3024
|
-
const { offsetWidth, offsetHeight } = visuals[0];
|
3025
|
-
const [c1, c2, c3] = controlKnobs;
|
3026
|
-
const { activeElement } = getDocument(c1);
|
3027
|
-
const currentKnob = controlKnobs.find((x) => x === activeElement);
|
3028
|
-
const yRatio = offsetHeight / 360;
|
3029
|
-
|
3030
|
-
/* istanbul ignore else */
|
3031
|
-
if (currentKnob) {
|
3032
|
-
let offsetX = 0;
|
3033
|
-
let offsetY = 0;
|
3034
|
-
|
3035
|
-
/* istanbul ignore else */
|
3036
|
-
if (target === c1) {
|
3037
|
-
const xRatio = offsetWidth / 100;
|
3038
|
-
|
3039
|
-
/* istanbul ignore else */
|
3040
|
-
if ([keyArrowLeft, keyArrowRight].includes(code)) {
|
3041
|
-
self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
|
3042
|
-
} else if ([keyArrowUp, keyArrowDown].includes(code)) {
|
3043
|
-
self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
|
3044
|
-
}
|
3045
|
-
|
3046
|
-
offsetX = self.controlPositions.c1x;
|
3047
|
-
offsetY = self.controlPositions.c1y;
|
3048
|
-
self.changeControl1(offsetX, offsetY);
|
3049
|
-
} else if (target === c2) {
|
3050
|
-
self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
|
3051
|
-
? yRatio
|
3052
|
-
: -yRatio;
|
3053
|
-
|
3054
|
-
offsetY = self.controlPositions.c2y;
|
3055
|
-
self.changeControl2(offsetY);
|
3056
|
-
} else if (target === c3) {
|
3057
|
-
self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
|
3058
|
-
? yRatio
|
3059
|
-
: -yRatio;
|
3060
|
-
|
3061
|
-
offsetY = self.controlPositions.c3y;
|
3062
|
-
self.changeAlpha(offsetY);
|
3063
|
-
}
|
3064
|
-
self.handleScroll(e);
|
3065
|
-
}
|
3066
|
-
}
|
3067
|
-
|
3068
|
-
/** The event listener of the colour form inputs. */
|
3069
|
-
changeHandler() {
|
3070
|
-
const self = this;
|
3071
|
-
let colorSource;
|
3072
|
-
const {
|
3073
|
-
inputs, format, value: currentValue, input, controlPositions, visuals,
|
3074
|
-
} = self;
|
3075
|
-
/** @type {*} */
|
3076
|
-
const { activeElement } = getDocument(input);
|
3077
|
-
const { offsetHeight } = visuals[0];
|
3078
|
-
const [i1,,, i4] = inputs;
|
3079
|
-
const [v1, v2, v3, v4] = format === 'rgb'
|
3080
|
-
? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
|
3081
|
-
: inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
|
3082
|
-
const isNonColorValue = self.hasNonColor && nonColors.includes(currentValue);
|
3083
|
-
const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
|
3084
|
-
|
3085
|
-
/* istanbul ignore else */
|
3086
|
-
if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
|
3087
|
-
if (activeElement === input) {
|
3088
|
-
if (isNonColorValue) {
|
3089
|
-
colorSource = currentValue === 'transparent' ? 'rgba(0,0,0,0)' : 'rgb(0,0,0)';
|
3090
|
-
} else {
|
3091
|
-
colorSource = currentValue;
|
3092
|
-
}
|
3093
|
-
} else if (format === 'hex') {
|
3094
|
-
colorSource = i1.value;
|
3095
|
-
} else if (format === 'hsl') {
|
3096
|
-
colorSource = {
|
3097
|
-
h: v1, s: v2, l: v3, a: alpha,
|
3098
|
-
};
|
3099
|
-
} else if (format === 'hwb') {
|
3100
|
-
colorSource = {
|
3101
|
-
h: v1, w: v2, b: v3, a: alpha,
|
3102
|
-
};
|
3103
|
-
} else {
|
3104
|
-
colorSource = {
|
3105
|
-
r: v1, g: v2, b: v3, a: alpha,
|
3106
|
-
};
|
3107
|
-
}
|
3108
|
-
|
3109
|
-
const {
|
3110
|
-
r, g, b, a,
|
3111
|
-
} = new Color(colorSource);
|
3112
|
-
|
3113
|
-
ObjectAssign(self.color, {
|
3114
|
-
r, g, b, a,
|
3115
|
-
});
|
3116
|
-
self.setControlPositions();
|
3117
|
-
self.updateAppearance();
|
3118
|
-
self.updateInputs();
|
3119
|
-
self.updateControls();
|
3120
|
-
self.updateVisuals();
|
3121
|
-
|
3122
|
-
// set non-color keyword
|
3123
|
-
if (activeElement === input && isNonColorValue) {
|
3124
|
-
self.value = currentValue;
|
3125
|
-
}
|
3126
|
-
}
|
3127
|
-
}
|
3128
|
-
|
3129
|
-
/**
|
3130
|
-
* Updates `ColorPicker` first control:
|
3131
|
-
* * `lightness` and `saturation` for HEX/RGB;
|
3132
|
-
* * `lightness` and `hue` for HSL.
|
3133
|
-
*
|
3134
|
-
* @param {number} X the X component of the offset
|
3135
|
-
* @param {number} Y the Y component of the offset
|
3136
|
-
*/
|
3137
|
-
changeControl1(X, Y) {
|
3138
|
-
const self = this;
|
3139
|
-
let [offsetX, offsetY] = [0, 0];
|
3140
|
-
const { controlPositions, visuals } = self;
|
3141
|
-
const { offsetHeight, offsetWidth } = visuals[0];
|
3142
|
-
|
3143
|
-
if (X > offsetWidth) offsetX = offsetWidth;
|
3144
|
-
else if (X >= 0) offsetX = X;
|
3145
|
-
|
3146
|
-
if (Y > offsetHeight) offsetY = offsetHeight;
|
3147
|
-
else if (Y >= 0) offsetY = Y;
|
3148
|
-
|
3149
|
-
const hue = controlPositions.c2y / offsetHeight;
|
3150
|
-
|
3151
|
-
const saturation = offsetX / offsetWidth;
|
3152
|
-
|
3153
|
-
const lightness = 1 - offsetY / offsetHeight;
|
3154
|
-
const alpha = 1 - controlPositions.c3y / offsetHeight;
|
3155
|
-
|
3156
|
-
// new color
|
3157
|
-
const {
|
3158
|
-
r, g, b, a,
|
3159
|
-
} = new Color({
|
3160
|
-
h: hue, s: saturation, v: lightness, a: alpha,
|
3161
|
-
});
|
3162
|
-
|
3163
|
-
ObjectAssign(self.color, {
|
3164
|
-
r, g, b, a,
|
3165
|
-
});
|
3166
|
-
|
3167
|
-
// new positions
|
3168
|
-
self.controlPositions.c1x = offsetX;
|
3169
|
-
self.controlPositions.c1y = offsetY;
|
3170
|
-
|
3171
|
-
// update color picker
|
3172
|
-
self.updateAppearance();
|
3173
|
-
self.updateInputs();
|
3174
|
-
self.updateControls();
|
3175
|
-
self.updateVisuals();
|
3176
|
-
}
|
3177
|
-
|
3178
|
-
/**
|
3179
|
-
* Updates `ColorPicker` second control:
|
3180
|
-
* * `hue` for HEX/RGB/HWB;
|
3181
|
-
* * `saturation` for HSL.
|
3182
|
-
*
|
3183
|
-
* @param {number} Y the Y offset
|
3184
|
-
*/
|
3185
|
-
changeControl2(Y) {
|
3186
|
-
const self = this;
|
3187
|
-
const {
|
3188
|
-
controlPositions, visuals,
|
3189
|
-
} = self;
|
3190
|
-
const { offsetHeight, offsetWidth } = visuals[0];
|
3191
|
-
|
3192
|
-
let offsetY = 0;
|
3193
|
-
|
3194
|
-
if (Y > offsetHeight) offsetY = offsetHeight;
|
3195
|
-
else if (Y >= 0) offsetY = Y;
|
3196
|
-
|
3197
|
-
const hue = offsetY / offsetHeight;
|
3198
|
-
const saturation = controlPositions.c1x / offsetWidth;
|
3199
|
-
const lightness = 1 - controlPositions.c1y / offsetHeight;
|
3200
|
-
const alpha = 1 - controlPositions.c3y / offsetHeight;
|
3201
|
-
|
3202
|
-
// new color
|
3203
|
-
const {
|
3204
|
-
r, g, b, a,
|
3205
|
-
} = new Color({
|
3206
|
-
h: hue, s: saturation, v: lightness, a: alpha,
|
3207
|
-
});
|
3208
|
-
|
3209
|
-
ObjectAssign(self.color, {
|
3210
|
-
r, g, b, a,
|
3211
|
-
});
|
3212
|
-
|
3213
|
-
// new position
|
3214
|
-
self.controlPositions.c2y = offsetY;
|
3215
|
-
// update color picker
|
3216
|
-
self.updateAppearance();
|
3217
|
-
self.updateInputs();
|
3218
|
-
self.updateControls();
|
3219
|
-
self.updateVisuals();
|
3220
|
-
}
|
3221
|
-
|
3222
|
-
/**
|
3223
|
-
* Updates `ColorPicker` last control,
|
3224
|
-
* the `alpha` channel.
|
3225
|
-
*
|
3226
|
-
* @param {number} Y
|
3227
|
-
*/
|
3228
|
-
changeAlpha(Y) {
|
3229
|
-
const self = this;
|
3230
|
-
const { visuals } = self;
|
3231
|
-
const { offsetHeight } = visuals[0];
|
3232
|
-
let offsetY = 0;
|
3233
|
-
|
3234
|
-
if (Y > offsetHeight) offsetY = offsetHeight;
|
3235
|
-
else if (Y >= 0) offsetY = Y;
|
3236
|
-
|
3237
|
-
// update color alpha
|
3238
|
-
const alpha = 1 - offsetY / offsetHeight;
|
3239
|
-
self.color.setAlpha(alpha);
|
3240
|
-
// update position
|
3241
|
-
self.controlPositions.c3y = offsetY;
|
3242
|
-
// update color picker
|
3243
|
-
self.updateAppearance();
|
3244
|
-
self.updateInputs();
|
3245
|
-
self.updateControls();
|
3246
|
-
self.updateVisuals();
|
3247
|
-
}
|
3248
|
-
|
3249
|
-
/**
|
3250
|
-
* Updates `ColorPicker` control positions on:
|
3251
|
-
* * initialization
|
3252
|
-
* * window resize
|
3253
|
-
*/
|
3254
|
-
update() {
|
3255
|
-
const self = this;
|
3256
|
-
self.updateDropdownPosition();
|
3257
|
-
self.updateAppearance();
|
3258
|
-
self.setControlPositions();
|
3259
|
-
self.updateInputs(true);
|
3260
|
-
self.updateControls();
|
3261
|
-
self.updateVisuals();
|
3262
|
-
}
|
3263
|
-
|
3264
|
-
/** Updates the open dropdown position on *scroll* event. */
|
3265
|
-
updateDropdownPosition() {
|
3266
|
-
const self = this;
|
3267
|
-
const { input, colorPicker, colorMenu } = self;
|
3268
|
-
const elRect = getBoundingClientRect(input);
|
3269
|
-
const { top, bottom } = elRect;
|
3270
|
-
const { offsetHeight: elHeight } = input;
|
3271
|
-
const windowHeight = getDocumentElement(input).clientHeight;
|
3272
|
-
const isPicker = hasClass(colorPicker, 'show');
|
3273
|
-
const dropdown = isPicker ? colorPicker : colorMenu;
|
3274
|
-
if (!dropdown) return;
|
3275
|
-
const { offsetHeight: dropHeight } = dropdown;
|
3276
|
-
const distanceBottom = windowHeight - bottom;
|
3277
|
-
const distanceTop = top;
|
3278
|
-
const bottomExceed = top + dropHeight + elHeight > windowHeight; // show
|
3279
|
-
const topExceed = top - dropHeight < 0; // show-top
|
3280
|
-
|
3281
|
-
if ((hasClass(dropdown, 'bottom') || !topExceed) && distanceBottom < distanceTop && bottomExceed) {
|
3282
|
-
removeClass(dropdown, 'bottom');
|
3283
|
-
addClass(dropdown, 'top');
|
3284
|
-
} else {
|
3285
|
-
removeClass(dropdown, 'top');
|
3286
|
-
addClass(dropdown, 'bottom');
|
3287
|
-
}
|
3288
|
-
}
|
3289
|
-
|
3290
|
-
/** Updates control knobs' positions. */
|
3291
|
-
setControlPositions() {
|
3292
|
-
const self = this;
|
3293
|
-
const {
|
3294
|
-
visuals, color, hsv,
|
3295
|
-
} = self;
|
3296
|
-
const { offsetHeight, offsetWidth } = visuals[0];
|
3297
|
-
const alpha = color.a;
|
3298
|
-
const hue = hsv.h;
|
3299
|
-
|
3300
|
-
const saturation = hsv.s;
|
3301
|
-
const lightness = hsv.v;
|
3302
|
-
|
3303
|
-
self.controlPositions.c1x = saturation * offsetWidth;
|
3304
|
-
self.controlPositions.c1y = (1 - lightness) * offsetHeight;
|
3305
|
-
self.controlPositions.c2y = hue * offsetHeight;
|
3306
|
-
self.controlPositions.c3y = (1 - alpha) * offsetHeight;
|
3307
|
-
}
|
3308
|
-
|
3309
|
-
/** Update the visual appearance label and control knob labels. */
|
3310
|
-
updateAppearance() {
|
3311
|
-
const self = this;
|
3312
|
-
const {
|
3313
|
-
componentLabels, color, parent,
|
3314
|
-
hsv, hex, format, controlKnobs,
|
3315
|
-
} = self;
|
3316
|
-
const {
|
3317
|
-
appearanceLabel, hexLabel, valueLabel,
|
3318
|
-
} = componentLabels;
|
3319
|
-
let { r, g, b } = color.toRgb();
|
3320
|
-
const [knob1, knob2, knob3] = controlKnobs;
|
3321
|
-
const hue = roundPart(hsv.h * 360);
|
3322
|
-
const alpha = color.a;
|
3323
|
-
const saturation = roundPart(hsv.s * 100);
|
3324
|
-
const lightness = roundPart(hsv.v * 100);
|
3325
|
-
const colorName = self.appearance;
|
3326
|
-
|
3327
|
-
let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
|
3328
|
-
|
3329
|
-
if (format === 'hwb') {
|
3330
|
-
const { hwb } = self;
|
3331
|
-
const whiteness = roundPart(hwb.w * 100);
|
3332
|
-
const blackness = roundPart(hwb.b * 100);
|
3333
|
-
colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
|
3334
|
-
setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
|
3335
|
-
setAttribute(knob1, ariaValueNow, `${whiteness}`);
|
3336
|
-
setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
|
3337
|
-
setAttribute(knob2, ariaValueText, `${hue}%`);
|
3338
|
-
setAttribute(knob2, ariaValueNow, `${hue}`);
|
3339
|
-
} else {
|
3340
|
-
[r, g, b] = [r, g, b].map(roundPart);
|
3341
|
-
colorLabel = format === 'hsl' ? `HSL: ${hue}°, ${saturation}%, ${lightness}%` : colorLabel;
|
3342
|
-
colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
|
3343
|
-
|
3344
|
-
setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
|
3345
|
-
setAttribute(knob1, ariaValueNow, `${lightness}`);
|
3346
|
-
setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
|
3347
|
-
setAttribute(knob2, ariaValueText, `${hue}°`);
|
3348
|
-
setAttribute(knob2, ariaValueNow, `${hue}`);
|
3349
|
-
}
|
3350
|
-
|
3351
|
-
const alphaValue = roundPart(alpha * 100);
|
3352
|
-
setAttribute(knob3, ariaValueText, `${alphaValue}%`);
|
3353
|
-
setAttribute(knob3, ariaValueNow, `${alphaValue}`);
|
3354
|
-
|
3355
|
-
// update the input backgroundColor
|
3356
|
-
const newColor = color.toString();
|
3357
|
-
setElementStyle(self.input, { backgroundColor: newColor });
|
3358
|
-
|
3359
|
-
// toggle dark/light classes will also style the placeholder
|
3360
|
-
// dark sets color white, light sets color black
|
3361
|
-
// isDark ? '#000' : '#fff'
|
3362
|
-
if (!self.isDark) {
|
3363
|
-
if (hasClass(parent, 'txt-dark')) removeClass(parent, 'txt-dark');
|
3364
|
-
if (!hasClass(parent, 'txt-light')) addClass(parent, 'txt-light');
|
3365
|
-
} else {
|
3366
|
-
if (hasClass(parent, 'txt-light')) removeClass(parent, 'txt-light');
|
3367
|
-
if (!hasClass(parent, 'txt-dark')) addClass(parent, 'txt-dark');
|
3368
|
-
}
|
3369
|
-
}
|
3370
|
-
|
3371
|
-
/** Updates the control knobs actual positions. */
|
3372
|
-
updateControls() {
|
3373
|
-
const { controlKnobs, controlPositions } = this;
|
3374
|
-
let {
|
3375
|
-
c1x, c1y, c2y, c3y,
|
3376
|
-
} = controlPositions;
|
3377
|
-
const [control1, control2, control3] = controlKnobs;
|
3378
|
-
// round control positions
|
3379
|
-
[c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
|
3380
|
-
|
3381
|
-
setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
|
3382
|
-
setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
|
3383
|
-
setElementStyle(control3, { transform: `translate3d(0,${c3y - 4}px,0)` });
|
3384
|
-
}
|
3385
|
-
|
3386
|
-
/**
|
3387
|
-
* Updates all color form inputs.
|
3388
|
-
* @param {boolean=} isPrevented when `true`, the component original event is prevented
|
3389
|
-
*/
|
3390
|
-
updateInputs(isPrevented) {
|
3391
|
-
const self = this;
|
3392
|
-
const {
|
3393
|
-
value: oldColor, format, inputs, color, hsl,
|
3394
|
-
} = self;
|
3395
|
-
const [i1, i2, i3, i4] = inputs;
|
3396
|
-
const alpha = roundPart(color.a * 100);
|
3397
|
-
const hue = roundPart(hsl.h * 360);
|
3398
|
-
let newColor;
|
3399
|
-
|
3400
|
-
/* istanbul ignore else */
|
3401
|
-
if (format === 'hex') {
|
3402
|
-
newColor = self.color.toHexString(true);
|
3403
|
-
i1.value = self.hex;
|
3404
|
-
} else if (format === 'hsl') {
|
3405
|
-
const lightness = roundPart(hsl.l * 100);
|
3406
|
-
const saturation = roundPart(hsl.s * 100);
|
3407
|
-
newColor = self.color.toHslString();
|
3408
|
-
i1.value = `${hue}`;
|
3409
|
-
i2.value = `${saturation}`;
|
3410
|
-
i3.value = `${lightness}`;
|
3411
|
-
i4.value = `${alpha}`;
|
3412
|
-
} else if (format === 'hwb') {
|
3413
|
-
const { w, b } = self.hwb;
|
3414
|
-
const whiteness = roundPart(w * 100);
|
3415
|
-
const blackness = roundPart(b * 100);
|
3416
|
-
|
3417
|
-
newColor = self.color.toHwbString();
|
3418
|
-
i1.value = `${hue}`;
|
3419
|
-
i2.value = `${whiteness}`;
|
3420
|
-
i3.value = `${blackness}`;
|
3421
|
-
i4.value = `${alpha}`;
|
3422
|
-
} else if (format === 'rgb') {
|
3423
|
-
let { r, g, b } = self.rgb;
|
3424
|
-
[r, g, b] = [r, g, b].map(roundPart);
|
3425
|
-
|
3426
|
-
newColor = self.color.toRgbString();
|
3427
|
-
i1.value = `${r}`;
|
3428
|
-
i2.value = `${g}`;
|
3429
|
-
i3.value = `${b}`;
|
3430
|
-
i4.value = `${alpha}`;
|
3431
|
-
}
|
3432
|
-
|
3433
|
-
// update the color value
|
3434
|
-
self.value = `${newColor}`;
|
3435
|
-
|
3436
|
-
// don't trigger the custom event unless it's really changed
|
3437
|
-
if (!isPrevented && newColor !== oldColor) {
|
3438
|
-
firePickerChange(self);
|
3439
|
-
}
|
3440
|
-
}
|
3441
|
-
|
3442
|
-
/**
|
3443
|
-
* Toggle the `ColorPicker` dropdown visibility.
|
3444
|
-
* @param {Event=} e
|
3445
|
-
* @this {ColorPicker}
|
3446
|
-
*/
|
3447
|
-
togglePicker(e) {
|
3448
|
-
if (e) e.preventDefault();
|
3449
|
-
const self = this;
|
3450
|
-
const { colorPicker } = self;
|
3451
|
-
|
3452
|
-
if (self.isOpen && hasClass(colorPicker, 'show')) {
|
3453
|
-
self.hide(true);
|
3454
|
-
} else {
|
3455
|
-
showDropdown(self, colorPicker);
|
3456
|
-
}
|
3457
|
-
}
|
3458
|
-
|
3459
|
-
/** Shows the `ColorPicker` dropdown. */
|
3460
|
-
showPicker() {
|
3461
|
-
const self = this;
|
3462
|
-
const { colorPicker } = self;
|
3463
|
-
|
3464
|
-
if (!['top', 'bottom'].some((c) => hasClass(colorPicker, c))) {
|
3465
|
-
showDropdown(self, colorPicker);
|
3466
|
-
}
|
3467
|
-
}
|
3468
|
-
|
3469
|
-
/**
|
3470
|
-
* Toggles the visibility of the `ColorPicker` presets menu.
|
3471
|
-
* @param {Event=} e
|
3472
|
-
* @this {ColorPicker}
|
3473
|
-
*/
|
3474
|
-
toggleMenu(e) {
|
3475
|
-
if (e) e.preventDefault();
|
3476
|
-
const self = this;
|
3477
|
-
const { colorMenu } = self;
|
3478
|
-
|
3479
|
-
if (self.isOpen && hasClass(colorMenu, 'show')) {
|
3480
|
-
self.hide(true);
|
3481
|
-
} else {
|
3482
|
-
showDropdown(self, colorMenu);
|
3483
|
-
}
|
3484
|
-
}
|
3485
|
-
|
3486
|
-
/**
|
3487
|
-
* Hides the currently open `ColorPicker` dropdown.
|
3488
|
-
* @param {boolean=} focusPrevented
|
3489
|
-
*/
|
3490
|
-
hide(focusPrevented) {
|
3491
|
-
const self = this;
|
3492
|
-
if (self.isOpen) {
|
3493
|
-
const {
|
3494
|
-
pickerToggle, menuToggle, colorPicker, colorMenu, parent, input,
|
3495
|
-
} = self;
|
3496
|
-
const openPicker = hasClass(colorPicker, 'show');
|
3497
|
-
const openDropdown = openPicker ? colorPicker : colorMenu;
|
3498
|
-
const relatedBtn = openPicker ? pickerToggle : menuToggle;
|
3499
|
-
const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
|
3500
|
-
|
3501
|
-
self.value = self.color.toString(true);
|
3502
|
-
|
3503
|
-
/* istanbul ignore else */
|
3504
|
-
if (openDropdown) {
|
3505
|
-
removeClass(openDropdown, 'show');
|
3506
|
-
setAttribute(relatedBtn, ariaExpanded, 'false');
|
3507
|
-
setTimeout(() => {
|
3508
|
-
removePosition(openDropdown);
|
3509
|
-
/* istanbul ignore else */
|
3510
|
-
if (!querySelector('.show', parent)) {
|
3511
|
-
removeClass(parent, 'open');
|
3512
|
-
toggleEventsOnShown(self);
|
3513
|
-
self.isOpen = false;
|
3514
|
-
}
|
3515
|
-
}, animationDuration);
|
3516
|
-
}
|
3517
|
-
|
3518
|
-
if (!focusPrevented) {
|
3519
|
-
focus(pickerToggle);
|
3520
|
-
}
|
3521
|
-
setAttribute(input, tabIndex, '-1');
|
3522
|
-
if (relatedBtn === menuToggle) {
|
3523
|
-
setAttribute(menuToggle, tabIndex, '-1');
|
3524
|
-
}
|
3525
|
-
}
|
3526
|
-
}
|
3527
|
-
|
3528
|
-
/** Removes `ColorPicker` from target `<input>`. */
|
3529
|
-
dispose() {
|
3530
|
-
const self = this;
|
3531
|
-
const { input, parent } = self;
|
3532
|
-
self.hide(true);
|
3533
|
-
toggleEvents(self);
|
3534
|
-
[...parent.children].forEach((el) => {
|
3535
|
-
if (el !== input) el.remove();
|
3536
|
-
});
|
3537
|
-
|
3538
|
-
removeAttribute(input, tabIndex);
|
3539
|
-
setElementStyle(input, { backgroundColor: '' });
|
3540
|
-
|
3541
|
-
['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
|
3542
|
-
Data.remove(input, colorPickerString);
|
3543
|
-
}
|
3544
|
-
}
|
3545
|
-
|
3546
|
-
ObjectAssign(ColorPicker, {
|
3547
|
-
Color,
|
3548
|
-
ColorPalette,
|
3549
|
-
Version,
|
3550
|
-
getInstance: getColorPickerInstance,
|
3551
|
-
init: initColorPicker,
|
3552
|
-
selector: colorPickerSelector,
|
3553
|
-
// utils important for render
|
3554
|
-
roundPart,
|
3555
|
-
setElementStyle,
|
3556
|
-
setAttribute,
|
3557
|
-
getBoundingClientRect,
|
3558
|
-
});
|
3559
|
-
|
3560
|
-
/**
|
3561
|
-
* A single import is required to add the `CP` namespace to `src` sources.
|
3562
|
-
* @typedef {import("../../types/index")}
|
3563
|
-
*/
|
3564
|
-
|
3565
|
-
export { ColorPicker as default };
|