pickit-color 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +44 -0
- package/README.md +170 -0
- package/dist/colorpicker.css +881 -0
- package/dist/colorpicker.js +929 -0
- package/dist/colorpicker.min.css +1 -0
- package/dist/colorpicker.min.js +2 -0
- package/dist/esm/index.js +820 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/index.d.ts +79 -0
- package/package.json +69 -0
- package/src/README.md +292 -0
- package/src/colorpicker.d.ts +52 -0
- package/src/colorpicker.styl +719 -0
- package/src/index.ts +1181 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pickit Color Picker
|
|
3
|
+
* An accessible, lightweight color picker inspired by flatpickr
|
|
4
|
+
* Supports HSL, RGB, HEX formats with keyboard navigation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ColorPickerOptions {
|
|
8
|
+
defaultColor?: string;
|
|
9
|
+
format?: "hex" | "rgb" | "hsl";
|
|
10
|
+
showAlpha?: boolean;
|
|
11
|
+
sliderMode?: boolean;
|
|
12
|
+
eyeDropper?: boolean;
|
|
13
|
+
presetColors?: string[];
|
|
14
|
+
presetLabels?: string[];
|
|
15
|
+
presetsOnly?: boolean;
|
|
16
|
+
listView?: boolean;
|
|
17
|
+
inline?: boolean;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
inputPreview?: boolean;
|
|
20
|
+
onChange?: (color: string) => void;
|
|
21
|
+
onOpen?: () => void;
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
appendTo?: HTMLElement;
|
|
24
|
+
position?: "auto" | "above" | "below";
|
|
25
|
+
closeOnSelect?: boolean;
|
|
26
|
+
ariaLabels?: {
|
|
27
|
+
hue?: string;
|
|
28
|
+
saturation?: string;
|
|
29
|
+
lightness?: string;
|
|
30
|
+
alpha?: string;
|
|
31
|
+
presets?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface HSL {
|
|
36
|
+
h: number; // 0-360
|
|
37
|
+
s: number; // 0-100
|
|
38
|
+
l: number; // 0-100
|
|
39
|
+
a: number; // 0-1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RGB {
|
|
43
|
+
r: number; // 0-255
|
|
44
|
+
g: number; // 0-255
|
|
45
|
+
b: number; // 0-255
|
|
46
|
+
a: number; // 0-1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ColorPicker {
|
|
50
|
+
private input: HTMLInputElement;
|
|
51
|
+
private options: Required<ColorPickerOptions>;
|
|
52
|
+
private container: HTMLElement | null = null;
|
|
53
|
+
private colorBox: HTMLElement | null = null;
|
|
54
|
+
private hueSlider: HTMLInputElement | null = null;
|
|
55
|
+
private saturationSlider: HTMLInputElement | null = null;
|
|
56
|
+
private lightnessSlider: HTMLInputElement | null = null;
|
|
57
|
+
private alphaSlider: HTMLInputElement | null = null;
|
|
58
|
+
private hexInput: HTMLInputElement | null = null;
|
|
59
|
+
private currentColor: HSL = { h: 0, s: 100, l: 50, a: 1 };
|
|
60
|
+
private isOpen = false;
|
|
61
|
+
private saturationPointer: HTMLElement | null = null;
|
|
62
|
+
private compactButton: HTMLButtonElement | null = null;
|
|
63
|
+
private inputPreview: HTMLElement | null = null;
|
|
64
|
+
|
|
65
|
+
private static instances: Map<HTMLElement, ColorPicker> = new Map();
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
element: string | HTMLInputElement,
|
|
69
|
+
options: ColorPickerOptions = {}
|
|
70
|
+
) {
|
|
71
|
+
this.input =
|
|
72
|
+
typeof element === "string"
|
|
73
|
+
? (document.querySelector(element) as HTMLInputElement)
|
|
74
|
+
: element;
|
|
75
|
+
|
|
76
|
+
if (!this.input) {
|
|
77
|
+
throw new Error("ColorPicker: Invalid element selector");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Read preset colors from data attribute if available
|
|
81
|
+
const dataPresets = this.input.getAttribute("data-preset-colors");
|
|
82
|
+
const presetsFromAttr = dataPresets ? dataPresets.split(",").map(c => c.trim()) : null;
|
|
83
|
+
|
|
84
|
+
this.options = {
|
|
85
|
+
defaultColor: options.defaultColor || "#3b82f6",
|
|
86
|
+
format: options.format || "hex",
|
|
87
|
+
showAlpha: options.showAlpha ?? false,
|
|
88
|
+
sliderMode: options.sliderMode ?? false,
|
|
89
|
+
eyeDropper: options.eyeDropper ?? false,
|
|
90
|
+
presetColors: options.presetColors || presetsFromAttr || [
|
|
91
|
+
"#ef4444",
|
|
92
|
+
"#f59e0b",
|
|
93
|
+
"#10b981",
|
|
94
|
+
"#3b82f6",
|
|
95
|
+
"#8b5cf6",
|
|
96
|
+
"#ec4899",
|
|
97
|
+
"#000000",
|
|
98
|
+
"#ffffff",
|
|
99
|
+
],
|
|
100
|
+
presetLabels: options.presetLabels || [],
|
|
101
|
+
presetsOnly: options.presetsOnly ?? false,
|
|
102
|
+
listView: options.listView ?? false,
|
|
103
|
+
inline: options.inline ?? false,
|
|
104
|
+
compact: options.compact ?? false,
|
|
105
|
+
inputPreview: options.inputPreview ?? false,
|
|
106
|
+
onChange: options.onChange || (() => {}),
|
|
107
|
+
onOpen: options.onOpen || (() => {}),
|
|
108
|
+
onClose: options.onClose || (() => {}),
|
|
109
|
+
appendTo: options.appendTo || document.body,
|
|
110
|
+
position: options.position || "auto",
|
|
111
|
+
closeOnSelect: options.closeOnSelect ?? true,
|
|
112
|
+
ariaLabels: {
|
|
113
|
+
hue: options.ariaLabels?.hue || "Hue",
|
|
114
|
+
saturation:
|
|
115
|
+
options.ariaLabels?.saturation || "Saturation and Lightness",
|
|
116
|
+
lightness: options.ariaLabels?.lightness || "Lightness",
|
|
117
|
+
alpha: options.ariaLabels?.alpha || "Alpha",
|
|
118
|
+
presets: options.ariaLabels?.presets || "Preset colors",
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
this.init();
|
|
123
|
+
ColorPicker.instances.set(this.input, this);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private init(): void {
|
|
127
|
+
// Parse initial color
|
|
128
|
+
const initialColor = this.input.value || this.options.defaultColor;
|
|
129
|
+
this.currentColor = this.parseColor(initialColor);
|
|
130
|
+
|
|
131
|
+
// Create compact button if needed
|
|
132
|
+
if (this.options.compact) {
|
|
133
|
+
this.createCompactButton();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create input preview if needed
|
|
137
|
+
if (this.options.inputPreview && !this.options.compact) {
|
|
138
|
+
this.createInputPreview();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build UI
|
|
142
|
+
this.buildColorPicker();
|
|
143
|
+
|
|
144
|
+
// Setup event listeners
|
|
145
|
+
this.setupEventListeners();
|
|
146
|
+
|
|
147
|
+
// Update display
|
|
148
|
+
this.updateColorDisplay();
|
|
149
|
+
|
|
150
|
+
// Open if inline
|
|
151
|
+
if (this.options.inline) {
|
|
152
|
+
this.open();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private buildColorPicker(): void {
|
|
157
|
+
this.container = document.createElement("div");
|
|
158
|
+
this.container.className = "colorpicker-container";
|
|
159
|
+
if (this.options.presetsOnly) {
|
|
160
|
+
this.container.classList.add("colorpicker-presets-only");
|
|
161
|
+
}
|
|
162
|
+
if (this.options.inline) {
|
|
163
|
+
this.container.classList.add("colorpicker-inline");
|
|
164
|
+
}
|
|
165
|
+
this.container.setAttribute("role", "dialog");
|
|
166
|
+
this.container.setAttribute("aria-label", "Color picker");
|
|
167
|
+
this.container.style.display = "none";
|
|
168
|
+
|
|
169
|
+
const content = `
|
|
170
|
+
<div class="colorpicker-content">
|
|
171
|
+
${!this.options.presetsOnly ? `
|
|
172
|
+
${!this.options.sliderMode ? `
|
|
173
|
+
<div class="colorpicker-saturation"
|
|
174
|
+
role="slider"
|
|
175
|
+
aria-label="${this.options.ariaLabels.saturation}"
|
|
176
|
+
aria-valuemin="0"
|
|
177
|
+
aria-valuemax="100"
|
|
178
|
+
aria-valuenow="${this.currentColor.s}"
|
|
179
|
+
tabindex="0">
|
|
180
|
+
<div class="colorpicker-saturation-overlay"></div>
|
|
181
|
+
<div class="colorpicker-saturation-pointer" role="presentation"></div>
|
|
182
|
+
</div>
|
|
183
|
+
` : ''}
|
|
184
|
+
|
|
185
|
+
<div class="colorpicker-controls">
|
|
186
|
+
<div class="colorpicker-sliders${this.options.sliderMode ? ' colorpicker-sliders-only' : ''}">
|
|
187
|
+
<div class="colorpicker-slider-group">
|
|
188
|
+
<label for="colorpicker-hue">
|
|
189
|
+
<span class="colorpicker-label">${
|
|
190
|
+
this.options.ariaLabels.hue
|
|
191
|
+
}</span>
|
|
192
|
+
</label>
|
|
193
|
+
<input
|
|
194
|
+
type="range"
|
|
195
|
+
id="colorpicker-hue"
|
|
196
|
+
class="colorpicker-slider colorpicker-hue-slider"
|
|
197
|
+
min="0"
|
|
198
|
+
max="360"
|
|
199
|
+
value="${this.currentColor.h}"
|
|
200
|
+
aria-label="${this.options.ariaLabels.hue}"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
${
|
|
204
|
+
this.options.sliderMode
|
|
205
|
+
? `
|
|
206
|
+
<div class="colorpicker-slider-group">
|
|
207
|
+
<label for="colorpicker-saturation">
|
|
208
|
+
<span class="colorpicker-label">${
|
|
209
|
+
this.options.ariaLabels.saturation
|
|
210
|
+
}</span>
|
|
211
|
+
</label>
|
|
212
|
+
<input
|
|
213
|
+
type="range"
|
|
214
|
+
id="colorpicker-saturation"
|
|
215
|
+
class="colorpicker-slider colorpicker-saturation-slider"
|
|
216
|
+
min="0"
|
|
217
|
+
max="100"
|
|
218
|
+
value="${this.currentColor.s}"
|
|
219
|
+
aria-label="${this.options.ariaLabels.saturation}"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="colorpicker-slider-group">
|
|
223
|
+
<label for="colorpicker-lightness">
|
|
224
|
+
<span class="colorpicker-label">${
|
|
225
|
+
this.options.ariaLabels.lightness
|
|
226
|
+
}</span>
|
|
227
|
+
</label>
|
|
228
|
+
<input
|
|
229
|
+
type="range"
|
|
230
|
+
id="colorpicker-lightness"
|
|
231
|
+
class="colorpicker-slider colorpicker-lightness-slider"
|
|
232
|
+
min="0"
|
|
233
|
+
max="100"
|
|
234
|
+
value="${this.currentColor.l}"
|
|
235
|
+
aria-label="${this.options.ariaLabels.lightness}"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
`
|
|
239
|
+
: ""
|
|
240
|
+
}
|
|
241
|
+
${
|
|
242
|
+
this.options.showAlpha
|
|
243
|
+
? `
|
|
244
|
+
<div class="colorpicker-slider-group">
|
|
245
|
+
<label for="colorpicker-alpha">
|
|
246
|
+
<span class="colorpicker-label">${
|
|
247
|
+
this.options.ariaLabels.alpha
|
|
248
|
+
}</span>
|
|
249
|
+
</label>
|
|
250
|
+
<input
|
|
251
|
+
type="range"
|
|
252
|
+
id="colorpicker-alpha"
|
|
253
|
+
class="colorpicker-slider colorpicker-alpha-slider"
|
|
254
|
+
min="0"
|
|
255
|
+
max="100"
|
|
256
|
+
value="${this.currentColor.a * 100}"
|
|
257
|
+
aria-label="${this.options.ariaLabels.alpha}"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
`
|
|
261
|
+
: ""
|
|
262
|
+
}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="colorpicker-preview">
|
|
266
|
+
<div class="colorpicker-preview-color" role="presentation"></div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="colorpicker-input-wrapper">
|
|
271
|
+
<label for="colorpicker-hex">
|
|
272
|
+
<span class="colorpicker-sr-only">Color value</span>
|
|
273
|
+
</label>
|
|
274
|
+
<div class="colorpicker-input-row">
|
|
275
|
+
<input
|
|
276
|
+
type="text"
|
|
277
|
+
id="colorpicker-hex"
|
|
278
|
+
class="colorpicker-input"
|
|
279
|
+
placeholder="${this.getPlaceholder()}"
|
|
280
|
+
aria-label="Color value in ${this.options.format} format"
|
|
281
|
+
/>
|
|
282
|
+
${
|
|
283
|
+
this.options.eyeDropper
|
|
284
|
+
? `
|
|
285
|
+
${this.supportsEyeDropper() ? `
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
class="colorpicker-eyedropper-btn"
|
|
289
|
+
aria-label="Pick color from screen"
|
|
290
|
+
title="Pick color from screen"
|
|
291
|
+
>
|
|
292
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
293
|
+
<path d="M2 22l1-1"/>
|
|
294
|
+
<path d="M8.5 16.5l-1-1"/>
|
|
295
|
+
<path d="M17 3l4 4"/>
|
|
296
|
+
<path d="M12 8l4 4"/>
|
|
297
|
+
<path d="M3 21l9-9"/>
|
|
298
|
+
<path d="M14.5 9.5l-1 1"/>
|
|
299
|
+
<path d="M20 14l-8 8"/>
|
|
300
|
+
</svg>
|
|
301
|
+
</button>
|
|
302
|
+
` : ''}
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
class="colorpicker-system-picker-btn"
|
|
306
|
+
aria-label="Open system color picker"
|
|
307
|
+
title="Open system color picker"
|
|
308
|
+
>
|
|
309
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
310
|
+
<circle cx="12" cy="12" r="10"/>
|
|
311
|
+
<path d="M12 2v20"/>
|
|
312
|
+
<path d="M2 12h20"/>
|
|
313
|
+
<circle cx="12" cy="12" r="3"/>
|
|
314
|
+
</svg>
|
|
315
|
+
</button>
|
|
316
|
+
<input
|
|
317
|
+
type="color"
|
|
318
|
+
class="colorpicker-system-picker-input"
|
|
319
|
+
style="position: absolute; opacity: 0; pointer-events: none; width: 0; height: 0;"
|
|
320
|
+
/>
|
|
321
|
+
`
|
|
322
|
+
: ""
|
|
323
|
+
}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
` : ''}
|
|
327
|
+
|
|
328
|
+
${
|
|
329
|
+
this.options.presetColors.length > 0
|
|
330
|
+
? `
|
|
331
|
+
<div class="colorpicker-presets${this.options.listView ? ' colorpicker-presets-list' : ''}" role="group" aria-label="${
|
|
332
|
+
this.options.ariaLabels.presets
|
|
333
|
+
}">
|
|
334
|
+
${this.options.presetColors
|
|
335
|
+
.map(
|
|
336
|
+
(color, index) => {
|
|
337
|
+
const label = this.options.presetLabels[index] || '';
|
|
338
|
+
return this.options.listView && label
|
|
339
|
+
? `
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
class="colorpicker-preset colorpicker-preset-list-item"
|
|
343
|
+
data-color="${color}"
|
|
344
|
+
aria-label="Select color ${label}"
|
|
345
|
+
>
|
|
346
|
+
<span class="colorpicker-preset-color" style="background-color: ${color}"></span>
|
|
347
|
+
<span class="colorpicker-preset-label">${label}</span>
|
|
348
|
+
</button>
|
|
349
|
+
`
|
|
350
|
+
: `
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
class="colorpicker-preset"
|
|
354
|
+
style="background-color: ${color}"
|
|
355
|
+
data-color="${color}"
|
|
356
|
+
aria-label="Select color ${label || color}"
|
|
357
|
+
></button>
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
.join("")}
|
|
362
|
+
</div>
|
|
363
|
+
`
|
|
364
|
+
: ""
|
|
365
|
+
}
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
this.container.innerHTML = content;
|
|
370
|
+
|
|
371
|
+
// Cache element references
|
|
372
|
+
this.colorBox = this.container.querySelector(".colorpicker-saturation");
|
|
373
|
+
this.saturationPointer = this.container.querySelector(
|
|
374
|
+
".colorpicker-saturation-pointer"
|
|
375
|
+
);
|
|
376
|
+
this.hueSlider = this.container.querySelector(".colorpicker-hue-slider");
|
|
377
|
+
this.saturationSlider = this.container.querySelector(".colorpicker-saturation-slider");
|
|
378
|
+
this.lightnessSlider = this.container.querySelector(".colorpicker-lightness-slider");
|
|
379
|
+
this.alphaSlider = this.container.querySelector(
|
|
380
|
+
".colorpicker-alpha-slider"
|
|
381
|
+
);
|
|
382
|
+
this.hexInput = this.container.querySelector(".colorpicker-input");
|
|
383
|
+
|
|
384
|
+
// Append to DOM
|
|
385
|
+
if (this.options.inline) {
|
|
386
|
+
// For inline mode, insert after the input
|
|
387
|
+
this.input.parentNode?.insertBefore(this.container, this.input.nextSibling);
|
|
388
|
+
// Hide the original input
|
|
389
|
+
this.input.style.display = "none";
|
|
390
|
+
} else {
|
|
391
|
+
// For popup mode, append to specified container
|
|
392
|
+
this.options.appendTo.appendChild(this.container);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private createCompactButton(): void {
|
|
397
|
+
// Hide the original input
|
|
398
|
+
this.input.style.position = "absolute";
|
|
399
|
+
this.input.style.opacity = "0";
|
|
400
|
+
this.input.style.pointerEvents = "none";
|
|
401
|
+
this.input.style.width = "0";
|
|
402
|
+
this.input.style.height = "0";
|
|
403
|
+
|
|
404
|
+
// Create compact button
|
|
405
|
+
this.compactButton = document.createElement("button") as HTMLButtonElement;
|
|
406
|
+
this.compactButton.type = "button";
|
|
407
|
+
this.compactButton.className = "colorpicker-compact-button";
|
|
408
|
+
this.compactButton.setAttribute("aria-label", "Select color");
|
|
409
|
+
this.compactButton.tabIndex = 0;
|
|
410
|
+
|
|
411
|
+
// Compact Mode: Immer Farbvorschau
|
|
412
|
+
const preview = document.createElement("span");
|
|
413
|
+
preview.className = "colorpicker-compact-preview";
|
|
414
|
+
preview.style.backgroundColor = this.input.value || this.options.defaultColor;
|
|
415
|
+
|
|
416
|
+
this.compactButton.appendChild(preview);
|
|
417
|
+
|
|
418
|
+
// Insert after input
|
|
419
|
+
this.input.parentNode?.insertBefore(this.compactButton, this.input.nextSibling);
|
|
420
|
+
|
|
421
|
+
// Click handler
|
|
422
|
+
this.compactButton.addEventListener("click", (e) => {
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
e.stopPropagation();
|
|
425
|
+
this.toggle();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Keyboard handler
|
|
429
|
+
this.compactButton.addEventListener("keydown", (e) => {
|
|
430
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
e.stopPropagation();
|
|
433
|
+
this.toggle();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private createInputPreview(): void {
|
|
439
|
+
// Create wrapper
|
|
440
|
+
const wrapper = document.createElement("div");
|
|
441
|
+
wrapper.className = "colorpicker-input-group";
|
|
442
|
+
|
|
443
|
+
// Create preview element
|
|
444
|
+
this.inputPreview = document.createElement("span");
|
|
445
|
+
this.inputPreview.className = "colorpicker-input-preview";
|
|
446
|
+
this.inputPreview.style.backgroundColor = this.input.value || this.options.defaultColor;
|
|
447
|
+
|
|
448
|
+
// Wrap input
|
|
449
|
+
this.input.parentNode?.insertBefore(wrapper, this.input);
|
|
450
|
+
wrapper.appendChild(this.inputPreview);
|
|
451
|
+
wrapper.appendChild(this.input);
|
|
452
|
+
|
|
453
|
+
// Add class to input for styling
|
|
454
|
+
this.input.classList.add("colorpicker-has-preview");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private supportsEyeDropper(): boolean {
|
|
458
|
+
return typeof window !== 'undefined' && 'EyeDropper' in window;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async openEyeDropper(): Promise<void> {
|
|
462
|
+
if (!this.supportsEyeDropper()) {
|
|
463
|
+
return; // Silently fail if not supported
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
// @ts-ignore - EyeDropper API not yet in TypeScript lib
|
|
468
|
+
const eyeDropper = new EyeDropper();
|
|
469
|
+
const result = await eyeDropper.open();
|
|
470
|
+
|
|
471
|
+
if (result.sRGBHex) {
|
|
472
|
+
this.currentColor = this.parseColor(result.sRGBHex);
|
|
473
|
+
this.input.value = result.sRGBHex;
|
|
474
|
+
this.updateColorDisplay();
|
|
475
|
+
this.options.onChange(result.sRGBHex);
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
// User cancelled - do nothing
|
|
479
|
+
if ((error as Error).name !== 'AbortError') {
|
|
480
|
+
console.error('EyeDropper error:', error);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private setupEventListeners(): void {
|
|
486
|
+
// Input click/keyboard to open (not in compact mode)
|
|
487
|
+
if (!this.options.compact) {
|
|
488
|
+
this.input.addEventListener("click", () => {
|
|
489
|
+
if (!this.options.inline) {
|
|
490
|
+
this.toggle();
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
this.input.addEventListener("keydown", (e) => {
|
|
495
|
+
if ((e.key === "Enter" || e.key === " ") && !this.options.inline) {
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
this.toggle();
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Input change
|
|
503
|
+
this.input.addEventListener("change", () => {
|
|
504
|
+
this.currentColor = this.parseColor(this.input.value);
|
|
505
|
+
this.updateColorDisplay();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Hue slider
|
|
509
|
+
if (this.hueSlider) {
|
|
510
|
+
this.hueSlider.addEventListener("input", (e) => {
|
|
511
|
+
this.currentColor.h = parseInt((e.target as HTMLInputElement).value);
|
|
512
|
+
this.updateColorDisplay();
|
|
513
|
+
this.announceColorChange();
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Saturation slider (sliderMode only)
|
|
518
|
+
if (this.saturationSlider) {
|
|
519
|
+
this.saturationSlider.addEventListener("input", (e) => {
|
|
520
|
+
this.currentColor.s = parseInt((e.target as HTMLInputElement).value);
|
|
521
|
+
this.updateColorDisplay();
|
|
522
|
+
this.announceColorChange();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Lightness slider (sliderMode only)
|
|
527
|
+
if (this.lightnessSlider) {
|
|
528
|
+
this.lightnessSlider.addEventListener("input", (e) => {
|
|
529
|
+
this.currentColor.l = parseInt((e.target as HTMLInputElement).value);
|
|
530
|
+
this.updateColorDisplay();
|
|
531
|
+
this.announceColorChange();
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Alpha slider
|
|
536
|
+
if (this.alphaSlider) {
|
|
537
|
+
this.alphaSlider.addEventListener("input", (e) => {
|
|
538
|
+
this.currentColor.a =
|
|
539
|
+
parseInt((e.target as HTMLInputElement).value) / 100;
|
|
540
|
+
this.updateColorDisplay();
|
|
541
|
+
this.announceColorChange();
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Saturation box
|
|
546
|
+
if (this.colorBox) {
|
|
547
|
+
this.colorBox.addEventListener("mousedown", (e) =>
|
|
548
|
+
this.onSaturationMouseDown(e)
|
|
549
|
+
);
|
|
550
|
+
this.colorBox.addEventListener("keydown", (e) =>
|
|
551
|
+
this.onSaturationKeyDown(e)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Color value input (accepts all formats)
|
|
556
|
+
if (this.hexInput) {
|
|
557
|
+
this.hexInput.addEventListener("input", (e) => {
|
|
558
|
+
const value = (e.target as HTMLInputElement).value.trim();
|
|
559
|
+
if (this.isValidColor(value)) {
|
|
560
|
+
this.currentColor = this.parseColor(value);
|
|
561
|
+
this.updateColorDisplay(false);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Preset colors
|
|
567
|
+
const presets = this.container?.querySelectorAll(".colorpicker-preset");
|
|
568
|
+
presets?.forEach((preset) => {
|
|
569
|
+
preset.addEventListener("click", (e) => {
|
|
570
|
+
const color = (e.currentTarget as HTMLElement).dataset.color!;
|
|
571
|
+
this.currentColor = this.parseColor(color);
|
|
572
|
+
this.updateColorDisplay();
|
|
573
|
+
if (this.options.closeOnSelect) {
|
|
574
|
+
this.close();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// EyeDropper button
|
|
580
|
+
const eyeDropperBtn = this.container?.querySelector(".colorpicker-eyedropper-btn");
|
|
581
|
+
if (eyeDropperBtn) {
|
|
582
|
+
eyeDropperBtn.addEventListener("click", async (e) => {
|
|
583
|
+
e.preventDefault();
|
|
584
|
+
e.stopPropagation();
|
|
585
|
+
await this.openEyeDropper();
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// System Color Picker button (Safari fallback)
|
|
590
|
+
const systemPickerBtn = this.container?.querySelector(".colorpicker-system-picker-btn");
|
|
591
|
+
const systemPickerInput = this.container?.querySelector(".colorpicker-system-picker-input") as HTMLInputElement;
|
|
592
|
+
if (systemPickerBtn && systemPickerInput) {
|
|
593
|
+
// Set current color to system picker
|
|
594
|
+
systemPickerInput.value = this.formatColor(this.currentColor).substring(0, 7); // HEX only
|
|
595
|
+
|
|
596
|
+
systemPickerBtn.addEventListener("click", (e) => {
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
e.stopPropagation();
|
|
599
|
+
systemPickerInput.click();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
systemPickerInput.addEventListener("change", () => {
|
|
603
|
+
const color = systemPickerInput.value;
|
|
604
|
+
this.currentColor = this.parseColor(color);
|
|
605
|
+
this.input.value = color;
|
|
606
|
+
this.updateColorDisplay();
|
|
607
|
+
this.options.onChange(color);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Close on outside click
|
|
612
|
+
if (!this.options.inline) {
|
|
613
|
+
document.addEventListener("mousedown", (e) => {
|
|
614
|
+
if (
|
|
615
|
+
this.isOpen &&
|
|
616
|
+
!this.container?.contains(e.target as Node) &&
|
|
617
|
+
e.target !== this.input
|
|
618
|
+
) {
|
|
619
|
+
this.close();
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Escape key to close
|
|
625
|
+
document.addEventListener("keydown", (e) => {
|
|
626
|
+
if (e.key === "Escape" && this.isOpen && !this.options.inline) {
|
|
627
|
+
this.close();
|
|
628
|
+
this.input.focus();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private onSaturationMouseDown(e: MouseEvent): void {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
this.updateSaturationFromMouse(e);
|
|
636
|
+
|
|
637
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
638
|
+
this.updateSaturationFromMouse(e);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const onMouseUp = () => {
|
|
642
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
643
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
647
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private updateSaturationFromMouse(e: MouseEvent): void {
|
|
651
|
+
if (!this.colorBox) return;
|
|
652
|
+
|
|
653
|
+
const rect = this.colorBox.getBoundingClientRect();
|
|
654
|
+
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
655
|
+
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
656
|
+
|
|
657
|
+
this.currentColor.s = (x / rect.width) * 100;
|
|
658
|
+
this.currentColor.l = 100 - (y / rect.height) * 100;
|
|
659
|
+
|
|
660
|
+
this.updateColorDisplay();
|
|
661
|
+
this.announceColorChange();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private onSaturationKeyDown(e: KeyboardEvent): void {
|
|
665
|
+
const step = e.shiftKey ? 10 : 1;
|
|
666
|
+
let handled = false;
|
|
667
|
+
|
|
668
|
+
switch (e.key) {
|
|
669
|
+
case "ArrowRight":
|
|
670
|
+
this.currentColor.s = Math.min(100, this.currentColor.s + step);
|
|
671
|
+
handled = true;
|
|
672
|
+
break;
|
|
673
|
+
case "ArrowLeft":
|
|
674
|
+
this.currentColor.s = Math.max(0, this.currentColor.s - step);
|
|
675
|
+
handled = true;
|
|
676
|
+
break;
|
|
677
|
+
case "ArrowUp":
|
|
678
|
+
this.currentColor.l = Math.min(100, this.currentColor.l + step);
|
|
679
|
+
handled = true;
|
|
680
|
+
break;
|
|
681
|
+
case "ArrowDown":
|
|
682
|
+
this.currentColor.l = Math.max(0, this.currentColor.l - step);
|
|
683
|
+
handled = true;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (handled) {
|
|
688
|
+
e.preventDefault();
|
|
689
|
+
this.updateColorDisplay();
|
|
690
|
+
this.announceColorChange();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private updateColorDisplay(updateInput = true): void {
|
|
695
|
+
// Update saturation box background
|
|
696
|
+
if (this.colorBox) {
|
|
697
|
+
this.colorBox.style.backgroundColor = `hsl(${this.currentColor.h}, 100%, 50%)`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Update saturation pointer position
|
|
701
|
+
if (this.saturationPointer && this.colorBox) {
|
|
702
|
+
const x = (this.currentColor.s / 100) * 100;
|
|
703
|
+
const y = (1 - this.currentColor.l / 100) * 100;
|
|
704
|
+
this.saturationPointer.style.left = `${x}%`;
|
|
705
|
+
this.saturationPointer.style.top = `${y}%`;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Update slider backgrounds (sliderMode)
|
|
709
|
+
if (this.saturationSlider) {
|
|
710
|
+
this.saturationSlider.style.background = `linear-gradient(to right, hsl(${this.currentColor.h}, 0%, 50%), hsl(${this.currentColor.h}, 100%, 50%))`;
|
|
711
|
+
}
|
|
712
|
+
if (this.lightnessSlider) {
|
|
713
|
+
this.lightnessSlider.style.background = `linear-gradient(to right, hsl(${this.currentColor.h}, ${this.currentColor.s}%, 0%), hsl(${this.currentColor.h}, ${this.currentColor.s}%, 50%), hsl(${this.currentColor.h}, ${this.currentColor.s}%, 100%))`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Update preview
|
|
717
|
+
const preview = this.container?.querySelector(
|
|
718
|
+
".colorpicker-preview-color"
|
|
719
|
+
) as HTMLElement;
|
|
720
|
+
if (preview) {
|
|
721
|
+
preview.style.backgroundColor = this.toHSLString(this.currentColor);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Update color value input (shows current format)
|
|
725
|
+
if (this.hexInput && updateInput) {
|
|
726
|
+
this.hexInput.value = this.formatColor(this.currentColor);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Update sliders
|
|
730
|
+
if (this.hueSlider) {
|
|
731
|
+
this.hueSlider.value = String(this.currentColor.h);
|
|
732
|
+
}
|
|
733
|
+
if (this.saturationSlider) {
|
|
734
|
+
this.saturationSlider.value = String(this.currentColor.s);
|
|
735
|
+
}
|
|
736
|
+
if (this.lightnessSlider) {
|
|
737
|
+
this.lightnessSlider.value = String(this.currentColor.l);
|
|
738
|
+
}
|
|
739
|
+
if (this.alphaSlider) {
|
|
740
|
+
this.alphaSlider.value = String(this.currentColor.a * 100);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Update input field
|
|
744
|
+
if (updateInput) {
|
|
745
|
+
this.input.value = this.formatColor(this.currentColor);
|
|
746
|
+
this.options.onChange(this.input.value);
|
|
747
|
+
|
|
748
|
+
// Update compact button preview
|
|
749
|
+
if (this.compactButton) {
|
|
750
|
+
const preview = this.compactButton.querySelector('.colorpicker-compact-preview') as HTMLElement;
|
|
751
|
+
if (preview) {
|
|
752
|
+
preview.style.backgroundColor = this.toHSLString(this.currentColor);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Update input preview
|
|
757
|
+
if (this.inputPreview) {
|
|
758
|
+
this.inputPreview.style.backgroundColor = this.toHSLString(this.currentColor);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private formatColor(color: HSL): string {
|
|
764
|
+
switch (this.options.format) {
|
|
765
|
+
case "hsl":
|
|
766
|
+
return this.toHSLString(color);
|
|
767
|
+
case "rgb":
|
|
768
|
+
return this.toRGBString(this.hslToRgb(color));
|
|
769
|
+
case "hex":
|
|
770
|
+
default:
|
|
771
|
+
return this.toHex(color);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private getPlaceholder(): string {
|
|
776
|
+
switch (this.options.format) {
|
|
777
|
+
case "hsl":
|
|
778
|
+
return this.options.showAlpha ? "hsla(0, 0%, 0%, 1)" : "hsl(0, 0%, 0%)";
|
|
779
|
+
case "rgb":
|
|
780
|
+
return this.options.showAlpha ? "rgba(0, 0, 0, 1)" : "rgb(0, 0, 0)";
|
|
781
|
+
case "hex":
|
|
782
|
+
default:
|
|
783
|
+
return "#000000";
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private parseColor(colorString: string): HSL {
|
|
788
|
+
colorString = colorString.trim();
|
|
789
|
+
|
|
790
|
+
// Try hex
|
|
791
|
+
if (colorString.startsWith("#")) {
|
|
792
|
+
return this.hexToHsl(colorString);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Try rgb/rgba
|
|
796
|
+
const rgbMatch = colorString.match(
|
|
797
|
+
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
|
|
798
|
+
);
|
|
799
|
+
if (rgbMatch) {
|
|
800
|
+
const rgb: RGB = {
|
|
801
|
+
r: parseInt(rgbMatch[1]),
|
|
802
|
+
g: parseInt(rgbMatch[2]),
|
|
803
|
+
b: parseInt(rgbMatch[3]),
|
|
804
|
+
a: rgbMatch[4] ? parseFloat(rgbMatch[4]) : 1,
|
|
805
|
+
};
|
|
806
|
+
return this.rgbToHsl(rgb);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Try hsl/hsla
|
|
810
|
+
const hslMatch = colorString.match(
|
|
811
|
+
/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*([\d.]+))?\)/
|
|
812
|
+
);
|
|
813
|
+
if (hslMatch) {
|
|
814
|
+
return {
|
|
815
|
+
h: parseInt(hslMatch[1]),
|
|
816
|
+
s: parseInt(hslMatch[2]),
|
|
817
|
+
l: parseInt(hslMatch[3]),
|
|
818
|
+
a: hslMatch[4] ? parseFloat(hslMatch[4]) : 1,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Default to current color
|
|
823
|
+
return this.currentColor;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private hexToHsl(hex: string): HSL {
|
|
827
|
+
hex = hex.replace("#", "");
|
|
828
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
829
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
830
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
831
|
+
const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1;
|
|
832
|
+
|
|
833
|
+
return this.rgbToHsl({ r: r * 255, g: g * 255, b: b * 255, a });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private rgbToHsl(rgb: RGB): HSL {
|
|
837
|
+
const r = rgb.r / 255;
|
|
838
|
+
const g = rgb.g / 255;
|
|
839
|
+
const b = rgb.b / 255;
|
|
840
|
+
|
|
841
|
+
const max = Math.max(r, g, b);
|
|
842
|
+
const min = Math.min(r, g, b);
|
|
843
|
+
let h = 0;
|
|
844
|
+
let s = 0;
|
|
845
|
+
const l = (max + min) / 2;
|
|
846
|
+
|
|
847
|
+
if (max !== min) {
|
|
848
|
+
const d = max - min;
|
|
849
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
850
|
+
|
|
851
|
+
switch (max) {
|
|
852
|
+
case r:
|
|
853
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
854
|
+
break;
|
|
855
|
+
case g:
|
|
856
|
+
h = ((b - r) / d + 2) / 6;
|
|
857
|
+
break;
|
|
858
|
+
case b:
|
|
859
|
+
h = ((r - g) / d + 4) / 6;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
h: Math.round(h * 360),
|
|
866
|
+
s: Math.round(s * 100),
|
|
867
|
+
l: Math.round(l * 100),
|
|
868
|
+
a: rgb.a,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private hslToRgb(hsl: HSL): RGB {
|
|
873
|
+
const h = hsl.h / 360;
|
|
874
|
+
const s = hsl.s / 100;
|
|
875
|
+
const l = hsl.l / 100;
|
|
876
|
+
|
|
877
|
+
let r, g, b;
|
|
878
|
+
|
|
879
|
+
if (s === 0) {
|
|
880
|
+
r = g = b = l;
|
|
881
|
+
} else {
|
|
882
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
883
|
+
if (t < 0) t += 1;
|
|
884
|
+
if (t > 1) t -= 1;
|
|
885
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
886
|
+
if (t < 1 / 2) return q;
|
|
887
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
888
|
+
return p;
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
892
|
+
const p = 2 * l - q;
|
|
893
|
+
|
|
894
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
895
|
+
g = hue2rgb(p, q, h);
|
|
896
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
r: Math.round(r * 255),
|
|
901
|
+
g: Math.round(g * 255),
|
|
902
|
+
b: Math.round(b * 255),
|
|
903
|
+
a: hsl.a,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private toHex(hsl: HSL): string {
|
|
908
|
+
const rgb = this.hslToRgb(hsl);
|
|
909
|
+
const toHex = (n: number) => n.toString(16).padStart(2, "0");
|
|
910
|
+
|
|
911
|
+
// Include alpha channel if showAlpha is enabled and alpha < 1
|
|
912
|
+
if (this.options.showAlpha && hsl.a < 1) {
|
|
913
|
+
const alpha = Math.round(hsl.a * 255);
|
|
914
|
+
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}${toHex(alpha)}`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private toHSLString(hsl: HSL): string {
|
|
921
|
+
if (this.options.showAlpha && hsl.a < 1) {
|
|
922
|
+
return `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${hsl.a})`;
|
|
923
|
+
}
|
|
924
|
+
return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private toRGBString(rgb: RGB): string {
|
|
928
|
+
if (this.options.showAlpha && rgb.a < 1) {
|
|
929
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
|
|
930
|
+
}
|
|
931
|
+
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private isValidHex(hex: string): boolean {
|
|
935
|
+
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(hex);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private isValidColor(value: string): boolean {
|
|
939
|
+
// Check if valid HEX
|
|
940
|
+
if (this.isValidHex(value)) return true;
|
|
941
|
+
|
|
942
|
+
// Check if valid RGB/RGBA
|
|
943
|
+
if (/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/.test(value)) return true;
|
|
944
|
+
|
|
945
|
+
// Check if valid HSL/HSLA
|
|
946
|
+
if (/^hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[\d.]+\s*)?\)$/.test(value)) return true;
|
|
947
|
+
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private announceColorChange(): void {
|
|
952
|
+
// Throttled ARIA live region announcement
|
|
953
|
+
if (!this.announceTimeout) {
|
|
954
|
+
this.announceTimeout = setTimeout(() => {
|
|
955
|
+
const announcement = document.createElement("div");
|
|
956
|
+
announcement.setAttribute("role", "status");
|
|
957
|
+
announcement.setAttribute("aria-live", "polite");
|
|
958
|
+
announcement.className = "colorpicker-sr-only";
|
|
959
|
+
announcement.textContent = `Color changed to ${this.formatColor(
|
|
960
|
+
this.currentColor
|
|
961
|
+
)}`;
|
|
962
|
+
this.container?.appendChild(announcement);
|
|
963
|
+
setTimeout(() => announcement.remove(), 1000);
|
|
964
|
+
this.announceTimeout = null;
|
|
965
|
+
}, 500);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
private announceTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
969
|
+
|
|
970
|
+
public open(): void {
|
|
971
|
+
if (this.isOpen || !this.container) return;
|
|
972
|
+
|
|
973
|
+
this.isOpen = true;
|
|
974
|
+
this.container.style.display = "block";
|
|
975
|
+
|
|
976
|
+
if (!this.options.inline) {
|
|
977
|
+
this.positionPicker();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
this.options.onOpen();
|
|
981
|
+
|
|
982
|
+
// Focus first interactive element
|
|
983
|
+
setTimeout(() => {
|
|
984
|
+
this.colorBox?.focus();
|
|
985
|
+
}, 0);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
public close(): void {
|
|
989
|
+
if (!this.isOpen || !this.container) return;
|
|
990
|
+
|
|
991
|
+
this.isOpen = false;
|
|
992
|
+
if (!this.options.inline) {
|
|
993
|
+
this.container.style.display = "none";
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
this.options.onClose();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
public toggle(): void {
|
|
1000
|
+
if (this.isOpen) {
|
|
1001
|
+
this.close();
|
|
1002
|
+
} else {
|
|
1003
|
+
this.open();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private positionPicker(): void {
|
|
1008
|
+
if (!this.container) return;
|
|
1009
|
+
|
|
1010
|
+
const inputRect = this.input.getBoundingClientRect();
|
|
1011
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1012
|
+
const viewportHeight = window.innerHeight;
|
|
1013
|
+
|
|
1014
|
+
let top = inputRect.bottom + window.scrollY + 4;
|
|
1015
|
+
const left = inputRect.left + window.scrollX;
|
|
1016
|
+
|
|
1017
|
+
// Check if there's enough space below
|
|
1018
|
+
if (this.options.position === "auto") {
|
|
1019
|
+
const spaceBelow = viewportHeight - inputRect.bottom;
|
|
1020
|
+
const spaceAbove = inputRect.top;
|
|
1021
|
+
|
|
1022
|
+
if (spaceBelow < containerRect.height && spaceAbove > spaceBelow) {
|
|
1023
|
+
top = inputRect.top + window.scrollY - containerRect.height - 4;
|
|
1024
|
+
}
|
|
1025
|
+
} else if (this.options.position === "above") {
|
|
1026
|
+
top = inputRect.top + window.scrollY - containerRect.height - 4;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
this.container.style.position = "absolute";
|
|
1030
|
+
this.container.style.top = `${top}px`;
|
|
1031
|
+
this.container.style.left = `${left}px`;
|
|
1032
|
+
this.container.style.zIndex = "9999";
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
public setColor(color: string): void {
|
|
1036
|
+
this.currentColor = this.parseColor(color);
|
|
1037
|
+
this.updateColorDisplay();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
public getColor(): string {
|
|
1041
|
+
return this.formatColor(this.currentColor);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
public destroy(): void {
|
|
1045
|
+
this.container?.remove();
|
|
1046
|
+
this.compactButton?.remove();
|
|
1047
|
+
|
|
1048
|
+
// Restore input if it was hidden for compact mode
|
|
1049
|
+
if (this.options.compact && this.input) {
|
|
1050
|
+
this.input.style.position = "";
|
|
1051
|
+
this.input.style.opacity = "";
|
|
1052
|
+
this.input.style.pointerEvents = "";
|
|
1053
|
+
this.input.style.width = "";
|
|
1054
|
+
this.input.style.height = "";
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Restore input if it had preview
|
|
1058
|
+
if (this.options.inputPreview && this.input) {
|
|
1059
|
+
this.input.classList.remove("colorpicker-has-preview");
|
|
1060
|
+
const wrapper = this.input.parentElement;
|
|
1061
|
+
if (wrapper && wrapper.classList.contains("colorpicker-input-group")) {
|
|
1062
|
+
wrapper.parentNode?.insertBefore(this.input, wrapper);
|
|
1063
|
+
wrapper.remove();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
ColorPicker.instances.delete(this.input);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
public static getInstance(element: HTMLElement): ColorPicker | undefined {
|
|
1071
|
+
return ColorPicker.instances.get(element);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Factory function
|
|
1076
|
+
export default function colorpicker(
|
|
1077
|
+
selector: string | HTMLInputElement,
|
|
1078
|
+
options?: ColorPickerOptions
|
|
1079
|
+
): ColorPicker {
|
|
1080
|
+
return new ColorPicker(selector, options);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Auto-initialization helper
|
|
1084
|
+
export function initColorPickers(root: Document | HTMLElement = document): ColorPicker[] {
|
|
1085
|
+
const pickers: ColorPicker[] = [];
|
|
1086
|
+
const elements = root.querySelectorAll<HTMLInputElement>(
|
|
1087
|
+
'[data-colorpicker], .colorpicker, input[type="color"][data-format]'
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
elements.forEach((element) => {
|
|
1091
|
+
// Skip if already initialized
|
|
1092
|
+
if (ColorPicker.getInstance(element)) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const dataset = element.dataset;
|
|
1097
|
+
const options: ColorPickerOptions = {};
|
|
1098
|
+
|
|
1099
|
+
// Parse data-colorpicker attribute
|
|
1100
|
+
if (dataset.colorpicker) {
|
|
1101
|
+
const config = dataset.colorpicker;
|
|
1102
|
+
const parts = config.split(/[,;]/);
|
|
1103
|
+
|
|
1104
|
+
parts.forEach((part) => {
|
|
1105
|
+
const [key, value] = part.split(':').map(s => s.trim());
|
|
1106
|
+
|
|
1107
|
+
switch (key) {
|
|
1108
|
+
case 'format':
|
|
1109
|
+
if (value === 'hex' || value === 'rgb' || value === 'hsl') {
|
|
1110
|
+
options.format = value;
|
|
1111
|
+
}
|
|
1112
|
+
break;
|
|
1113
|
+
case 'alpha':
|
|
1114
|
+
case 'showAlpha':
|
|
1115
|
+
options.showAlpha = value === 'true' || value === '1';
|
|
1116
|
+
break;
|
|
1117
|
+
case 'compact':
|
|
1118
|
+
options.compact = value === 'true' || value === '1';
|
|
1119
|
+
break;
|
|
1120
|
+
case 'inline':
|
|
1121
|
+
options.inline = value === 'true' || value === '1';
|
|
1122
|
+
break;
|
|
1123
|
+
case 'presets':
|
|
1124
|
+
options.presetsOnly = value === 'true' || value === '1';
|
|
1125
|
+
break;
|
|
1126
|
+
case 'list':
|
|
1127
|
+
options.listView = value === 'true' || value === '1';
|
|
1128
|
+
break;
|
|
1129
|
+
case 'sliderMode':
|
|
1130
|
+
options.sliderMode = value === 'true' || value === '1';
|
|
1131
|
+
break;
|
|
1132
|
+
case 'eyeDropper':
|
|
1133
|
+
options.eyeDropper = value === 'true' || value === '1';
|
|
1134
|
+
break;
|
|
1135
|
+
case 'inputPreview':
|
|
1136
|
+
options.inputPreview = value === 'true' || value === '1';
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Parse individual data attributes
|
|
1143
|
+
if (dataset.format && (dataset.format === 'hex' || dataset.format === 'rgb' || dataset.format === 'hsl')) {
|
|
1144
|
+
options.format = dataset.format as "hex" | "rgb" | "hsl";
|
|
1145
|
+
}
|
|
1146
|
+
if (dataset.alpha !== undefined) {
|
|
1147
|
+
options.showAlpha = dataset.alpha === 'true' || dataset.alpha === '1';
|
|
1148
|
+
}
|
|
1149
|
+
if (dataset.compact !== undefined) {
|
|
1150
|
+
options.compact = dataset.compact === 'true' || dataset.compact === '1';
|
|
1151
|
+
}
|
|
1152
|
+
if (dataset.inline !== undefined) {
|
|
1153
|
+
options.inline = dataset.inline === 'true' || dataset.inline === '1';
|
|
1154
|
+
}
|
|
1155
|
+
if (dataset.presetsOnly !== undefined) {
|
|
1156
|
+
options.presetsOnly = dataset.presetsOnly === 'true' || dataset.presetsOnly === '1';
|
|
1157
|
+
}
|
|
1158
|
+
if (dataset.listView !== undefined) {
|
|
1159
|
+
options.listView = dataset.listView === 'true' || dataset.listView === '1';
|
|
1160
|
+
}
|
|
1161
|
+
if (dataset.defaultColor) {
|
|
1162
|
+
options.defaultColor = dataset.defaultColor;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Create picker instance
|
|
1166
|
+
const picker = new ColorPicker(element, options);
|
|
1167
|
+
pickers.push(picker);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
return pickers;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Auto-init on DOMContentLoaded
|
|
1174
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
1175
|
+
if (document.readyState === 'loading') {
|
|
1176
|
+
document.addEventListener('DOMContentLoaded', () => initColorPickers());
|
|
1177
|
+
} else {
|
|
1178
|
+
// DOM already loaded
|
|
1179
|
+
initColorPickers();
|
|
1180
|
+
}
|
|
1181
|
+
}
|