@iobroker/gui-components 10.2.0 → 10.2.2
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/README.md +16 -1
- package/build/Components/ColorPicker.d.ts +94 -12
- package/build/Components/ColorPicker.js +567 -111
- package/build/Components/ColorPicker.js.map +1 -1
- package/build/Components/ObjectBrowser/ObjectBrowserClass.d.ts +17 -1
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js +99 -36
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js.map +1 -1
- package/build/Components/ObjectBrowser/dialogs.js +14 -5
- package/build/Components/ObjectBrowser/dialogs.js.map +1 -1
- package/build/Components/ObjectBrowser/renderLeaf.js +29 -10
- package/build/Components/ObjectBrowser/renderLeaf.js.map +1 -1
- package/build/Components/ObjectBrowser/toolbar.js +11 -10
- package/build/Components/ObjectBrowser/toolbar.js.map +1 -1
- package/build/Components/Schedule.js +176 -78
- package/build/Components/Schedule.js.map +1 -1
- package/build/Components/TreeTable.d.ts +6 -2
- package/build/Components/TreeTable.js +183 -103
- package/build/Components/TreeTable.js.map +1 -1
- package/build/Dialogs/Error.js +26 -5
- package/build/Dialogs/Error.js.map +1 -1
- package/build/i18n/de.json +20 -0
- package/build/i18n/en.json +20 -0
- package/build/i18n/es.json +20 -0
- package/build/i18n/fr.json +20 -0
- package/build/i18n/it.json +20 -0
- package/build/i18n/nl.json +20 -0
- package/build/i18n/pl.json +20 -0
- package/build/i18n/pt.json +20 -0
- package/build/i18n/ru.json +20 -0
- package/build/i18n/uk.json +20 -0
- package/build/i18n/zh-cn.json +20 -0
- package/build/index.d.ts +2 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/i18n/de.json +20 -0
- package/i18n/en.json +20 -0
- package/i18n/es.json +20 -0
- package/i18n/fr.json +20 -0
- package/i18n/it.json +20 -0
- package/i18n/nl.json +20 -0
- package/i18n/pl.json +20 -0
- package/i18n/pt.json +20 -0
- package/i18n/ru.json +20 -0
- package/i18n/uk.json +20 -0
- package/i18n/zh-cn.json +20 -0
- package/package.json +10 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2018-
|
|
2
|
+
* Copyright 2018-2026 Denis Haev (bluefox) <dogafox@gmail.com>
|
|
3
3
|
*
|
|
4
4
|
* Licensed under the Creative Commons Attribution-NonCommercial License, Version 4.0 (the "License");
|
|
5
5
|
* you may not use this file except in compliance with the License.
|
|
@@ -14,77 +14,418 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import React, { Component } from 'react';
|
|
17
|
-
import {
|
|
18
|
-
import { TextField,
|
|
19
|
-
import { Delete as IconDelete,
|
|
17
|
+
import { RgbaColorPicker, RgbColorPicker } from 'react-colorful';
|
|
18
|
+
import { TextField, Popover, IconButton, Button, Box, Tooltip, MenuItem, Select } from '@mui/material';
|
|
19
|
+
import { Delete as IconDelete, Colorize as IconColorize } from '@mui/icons-material';
|
|
20
20
|
import { I18n } from '../i18n';
|
|
21
|
+
const RECENT_COLORS_KEY = 'iob-color-picker-recent';
|
|
22
|
+
const MAX_RECENT_COLORS = 12;
|
|
23
|
+
/** Small checkerboard, so a semi-transparent color is recognizable as such */
|
|
24
|
+
const ALPHA_BACKGROUND = 'linear-gradient(45deg, #c0c0c0 25%, transparent 25%), linear-gradient(-45deg, #c0c0c0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #c0c0c0 75%), linear-gradient(-45deg, transparent 75%, #c0c0c0 75%)';
|
|
25
|
+
const ALPHA_BACKGROUND_SIZE = '8px 8px';
|
|
26
|
+
const ALPHA_BACKGROUND_POSITION = '0 0, 0 4px, 4px -4px, -4px 0px';
|
|
21
27
|
const styles = {
|
|
22
|
-
color: {
|
|
23
|
-
width: 36,
|
|
24
|
-
height: 14,
|
|
25
|
-
borderRadius: 2,
|
|
26
|
-
},
|
|
27
|
-
delButton: {
|
|
28
|
-
marginTop: 16,
|
|
29
|
-
},
|
|
30
28
|
swatch: {
|
|
31
29
|
mt: '16px',
|
|
32
|
-
p: '
|
|
30
|
+
p: '4px',
|
|
33
31
|
backgroundColor: 'background.paper',
|
|
34
|
-
borderRadius: '
|
|
35
|
-
boxShadow: '0 0 0 1px rgba(
|
|
32
|
+
borderRadius: '6px',
|
|
33
|
+
boxShadow: '0 0 0 1px rgba(128,128,128,.35)',
|
|
36
34
|
display: 'inline-block',
|
|
37
35
|
cursor: 'pointer',
|
|
38
36
|
verticalAlign: 'middle',
|
|
37
|
+
transition: 'box-shadow 0.15s ease-in-out',
|
|
38
|
+
'&:hover': {
|
|
39
|
+
boxShadow: '0 0 0 2px rgba(128,128,128,.6)',
|
|
40
|
+
},
|
|
39
41
|
},
|
|
40
42
|
swatchDisabled: {
|
|
41
43
|
opacity: 0.5,
|
|
42
44
|
cursor: 'default',
|
|
45
|
+
'&:hover': {
|
|
46
|
+
boxShadow: '0 0 0 1px rgba(128,128,128,.35)',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
color: {
|
|
50
|
+
height: 14,
|
|
51
|
+
borderRadius: '3px',
|
|
52
|
+
backgroundImage: ALPHA_BACKGROUND,
|
|
53
|
+
backgroundSize: ALPHA_BACKGROUND_SIZE,
|
|
54
|
+
backgroundPosition: ALPHA_BACKGROUND_POSITION,
|
|
55
|
+
},
|
|
56
|
+
delButton: {
|
|
57
|
+
marginTop: 16,
|
|
58
|
+
},
|
|
59
|
+
textDense: {
|
|
60
|
+
mt: 0,
|
|
61
|
+
mb: 0,
|
|
43
62
|
},
|
|
44
63
|
popover: {
|
|
45
64
|
'& .MuiPaper-root': {
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
borderRadius: '12px',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
// The chrome around the react-colorful area
|
|
69
|
+
pickerWrapper: (theme) => ({
|
|
70
|
+
p: '12px',
|
|
71
|
+
backgroundColor: theme.palette.background.paper,
|
|
72
|
+
borderRadius: '12px',
|
|
73
|
+
display: 'flex',
|
|
74
|
+
flexDirection: 'column',
|
|
75
|
+
gap: '10px',
|
|
76
|
+
// restyle react-colorful, so it fits into the ioBroker themes
|
|
77
|
+
'& .react-colorful': {
|
|
78
|
+
width: '100%',
|
|
79
|
+
height: 'auto',
|
|
80
|
+
gap: '10px',
|
|
81
|
+
},
|
|
82
|
+
'& .react-colorful__saturation': {
|
|
83
|
+
borderRadius: '8px',
|
|
84
|
+
border: 'none',
|
|
85
|
+
height: 150,
|
|
48
86
|
boxShadow: 'none',
|
|
49
87
|
},
|
|
88
|
+
'& .react-colorful__hue, & .react-colorful__alpha': {
|
|
89
|
+
height: 14,
|
|
90
|
+
borderRadius: '7px',
|
|
91
|
+
},
|
|
92
|
+
'& .react-colorful__alpha': {
|
|
93
|
+
backgroundImage: ALPHA_BACKGROUND,
|
|
94
|
+
backgroundSize: ALPHA_BACKGROUND_SIZE,
|
|
95
|
+
backgroundPosition: ALPHA_BACKGROUND_POSITION,
|
|
96
|
+
},
|
|
97
|
+
'& .react-colorful__last-control': {
|
|
98
|
+
borderRadius: '7px',
|
|
99
|
+
},
|
|
100
|
+
'& .react-colorful__pointer': {
|
|
101
|
+
width: 18,
|
|
102
|
+
height: 18,
|
|
103
|
+
borderWidth: 2,
|
|
104
|
+
},
|
|
105
|
+
'& .react-colorful__saturation-pointer': {
|
|
106
|
+
width: 20,
|
|
107
|
+
height: 20,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
controlRow: {
|
|
111
|
+
display: 'flex',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
gap: '6px',
|
|
50
114
|
},
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
preview: {
|
|
116
|
+
width: 28,
|
|
117
|
+
height: 28,
|
|
118
|
+
flexShrink: 0,
|
|
119
|
+
borderRadius: '6px',
|
|
120
|
+
boxShadow: '0 0 0 1px rgba(128,128,128,.35) inset',
|
|
121
|
+
backgroundImage: ALPHA_BACKGROUND,
|
|
122
|
+
backgroundSize: ALPHA_BACKGROUND_SIZE,
|
|
123
|
+
backgroundPosition: ALPHA_BACKGROUND_POSITION,
|
|
53
124
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
|
|
125
|
+
valueInput: {
|
|
126
|
+
flexGrow: 1,
|
|
127
|
+
minWidth: 60,
|
|
128
|
+
'& .MuiInputBase-input': {
|
|
129
|
+
fontFamily: 'monospace',
|
|
130
|
+
fontSize: '0.75rem',
|
|
131
|
+
textOverflow: 'ellipsis',
|
|
59
132
|
},
|
|
60
|
-
}),
|
|
61
|
-
cover: {
|
|
62
|
-
position: 'fixed',
|
|
63
|
-
top: 0,
|
|
64
|
-
right: 0,
|
|
65
|
-
bottom: 0,
|
|
66
|
-
left: 0,
|
|
67
133
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
134
|
+
formatSelect: {
|
|
135
|
+
flexShrink: 0,
|
|
136
|
+
fontSize: '0.7rem',
|
|
137
|
+
opacity: 0.75,
|
|
138
|
+
'& .MuiSelect-select': {
|
|
139
|
+
py: '4px',
|
|
140
|
+
pl: '2px',
|
|
141
|
+
pr: '18px !important',
|
|
142
|
+
},
|
|
143
|
+
'& .MuiSelect-icon': {
|
|
144
|
+
right: 0,
|
|
145
|
+
},
|
|
71
146
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
height: 16,
|
|
147
|
+
sectionTitle: {
|
|
148
|
+
fontSize: '0.7rem',
|
|
149
|
+
opacity: 0.6,
|
|
150
|
+
textTransform: 'uppercase',
|
|
151
|
+
letterSpacing: '0.06em',
|
|
78
152
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
153
|
+
swatchRow: {
|
|
154
|
+
display: 'flex',
|
|
155
|
+
flexWrap: 'wrap',
|
|
156
|
+
gap: '4px',
|
|
157
|
+
mt: '4px',
|
|
158
|
+
},
|
|
159
|
+
paletteItem: {
|
|
160
|
+
width: 22,
|
|
161
|
+
height: 22,
|
|
162
|
+
p: 0,
|
|
163
|
+
minWidth: 22,
|
|
164
|
+
borderRadius: '5px',
|
|
165
|
+
boxShadow: '0 0 0 1px rgba(128,128,128,.35) inset',
|
|
166
|
+
backgroundImage: ALPHA_BACKGROUND,
|
|
167
|
+
backgroundSize: ALPHA_BACKGROUND_SIZE,
|
|
168
|
+
backgroundPosition: ALPHA_BACKGROUND_POSITION,
|
|
169
|
+
'&:hover': {
|
|
170
|
+
boxShadow: '0 0 0 2px rgba(128,128,128,.7) inset',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
fill: {
|
|
174
|
+
width: '100%',
|
|
175
|
+
height: '100%',
|
|
176
|
+
borderRadius: 'inherit',
|
|
84
177
|
},
|
|
85
178
|
};
|
|
179
|
+
const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };
|
|
180
|
+
let canvasContext;
|
|
181
|
+
/**
|
|
182
|
+
* Normalize any CSS color notation (color names, `color()`, ...) with the help of a canvas.
|
|
183
|
+
*
|
|
184
|
+
* @param color The color string to normalize.
|
|
185
|
+
* @returns the normalized color as `#rrggbb`/`rgba(...)` string or null if the string is no valid color.
|
|
186
|
+
*/
|
|
187
|
+
function normalizeCssColor(color) {
|
|
188
|
+
if (canvasContext === undefined) {
|
|
189
|
+
canvasContext = typeof document === 'undefined' ? null : document.createElement('canvas').getContext('2d');
|
|
190
|
+
}
|
|
191
|
+
if (!canvasContext) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
// `fillStyle` keeps its previous value if the assigned string is no valid color,
|
|
195
|
+
// so it must be probed with two different defaults
|
|
196
|
+
canvasContext.fillStyle = '#000000';
|
|
197
|
+
canvasContext.fillStyle = color;
|
|
198
|
+
const first = canvasContext.fillStyle;
|
|
199
|
+
canvasContext.fillStyle = '#ffffff';
|
|
200
|
+
canvasContext.fillStyle = color;
|
|
201
|
+
return first === canvasContext.fillStyle ? first : null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Convert h, s, l (0..1) to r, g, b (0..255).
|
|
205
|
+
*
|
|
206
|
+
* @param h The hue in the range 0..1.
|
|
207
|
+
* @param s The saturation in the range 0..1.
|
|
208
|
+
* @param l The lightness in the range 0..1.
|
|
209
|
+
*/
|
|
210
|
+
function hsl2rgb(h, s, l) {
|
|
211
|
+
if (!s) {
|
|
212
|
+
const v = Math.round(l * 255);
|
|
213
|
+
return { r: v, g: v, b: v };
|
|
214
|
+
}
|
|
215
|
+
const hue2rgb = (p, q, t) => {
|
|
216
|
+
let _t = t;
|
|
217
|
+
if (_t < 0) {
|
|
218
|
+
_t += 1;
|
|
219
|
+
}
|
|
220
|
+
if (_t > 1) {
|
|
221
|
+
_t -= 1;
|
|
222
|
+
}
|
|
223
|
+
if (_t < 1 / 6) {
|
|
224
|
+
return p + (q - p) * 6 * _t;
|
|
225
|
+
}
|
|
226
|
+
if (_t < 1 / 2) {
|
|
227
|
+
return q;
|
|
228
|
+
}
|
|
229
|
+
if (_t < 2 / 3) {
|
|
230
|
+
return p + (q - p) * (2 / 3 - _t) * 6;
|
|
231
|
+
}
|
|
232
|
+
return p;
|
|
233
|
+
};
|
|
234
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
235
|
+
const p = 2 * l - q;
|
|
236
|
+
return {
|
|
237
|
+
r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
|
|
238
|
+
g: Math.round(hue2rgb(p, q, h) * 255),
|
|
239
|
+
b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Convert r, g, b (0..255) to h (0..360), s, l (0..100).
|
|
244
|
+
*
|
|
245
|
+
* @param color The color to convert.
|
|
246
|
+
*/
|
|
247
|
+
function rgb2hsl(color) {
|
|
248
|
+
const r = color.r / 255;
|
|
249
|
+
const g = color.g / 255;
|
|
250
|
+
const b = color.b / 255;
|
|
251
|
+
const max = Math.max(r, g, b);
|
|
252
|
+
const min = Math.min(r, g, b);
|
|
253
|
+
const l = (max + min) / 2;
|
|
254
|
+
if (max === min) {
|
|
255
|
+
return { h: 0, s: 0, l: Math.round(l * 100) };
|
|
256
|
+
}
|
|
257
|
+
const d = max - min;
|
|
258
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
259
|
+
let h;
|
|
260
|
+
if (max === r) {
|
|
261
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
262
|
+
}
|
|
263
|
+
else if (max === g) {
|
|
264
|
+
h = (b - r) / d + 2;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
h = (r - g) / d + 4;
|
|
268
|
+
}
|
|
269
|
+
return { h: Math.round((h / 6) * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Clamp a number into the given range.
|
|
273
|
+
*
|
|
274
|
+
* @param value The value to clamp.
|
|
275
|
+
* @param min The lower limit.
|
|
276
|
+
* @param max The upper limit.
|
|
277
|
+
*/
|
|
278
|
+
function clamp(value, min, max) {
|
|
279
|
+
return Math.min(max, Math.max(min, value));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Read a color channel that may be given as `128` or as `50%`.
|
|
283
|
+
*
|
|
284
|
+
* @param value The channel as string.
|
|
285
|
+
* @param max The maximal value of this channel.
|
|
286
|
+
*/
|
|
287
|
+
function readChannel(value, max) {
|
|
288
|
+
const trimmed = value.trim();
|
|
289
|
+
const parsed = parseFloat(trimmed);
|
|
290
|
+
if (Number.isNaN(parsed)) {
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
return clamp(Math.round(trimmed.endsWith('%') ? (parsed / 100) * max : parsed), 0, max);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Read the alpha channel that may be given as `0.5` or as `50%`.
|
|
297
|
+
*
|
|
298
|
+
* @param value The alpha channel as string or undefined if it is not given.
|
|
299
|
+
*/
|
|
300
|
+
function readAlpha(value) {
|
|
301
|
+
if (value === undefined || value === '') {
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
const trimmed = value.trim();
|
|
305
|
+
const parsed = trimmed.endsWith('%') ? parseFloat(trimmed) / 100 : parseFloat(trimmed);
|
|
306
|
+
return Number.isNaN(parsed) ? 1 : clamp(parsed, 0, 1);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Parse any CSS color notation into `{ r, g, b, a }`.
|
|
310
|
+
*
|
|
311
|
+
* Supported are `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, `hsl()`, `hsla()` (comma or space
|
|
312
|
+
* separated, with or without the `/ alpha` part) and every color name the browser knows.
|
|
313
|
+
*
|
|
314
|
+
* @param color The color string to parse.
|
|
315
|
+
* @returns the parsed color or null if the string could not be interpreted.
|
|
316
|
+
*/
|
|
317
|
+
export function parseColor(color) {
|
|
318
|
+
if (!color || typeof color !== 'string') {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const str = color.trim().toLowerCase();
|
|
322
|
+
if (!str) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
if (str === 'transparent') {
|
|
326
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
327
|
+
}
|
|
328
|
+
const hex = /^#([\da-f]{3,8})$/.exec(str);
|
|
329
|
+
if (hex) {
|
|
330
|
+
const h = hex[1];
|
|
331
|
+
if (h.length === 3 || h.length === 4) {
|
|
332
|
+
return {
|
|
333
|
+
r: parseInt(h[0] + h[0], 16),
|
|
334
|
+
g: parseInt(h[1] + h[1], 16),
|
|
335
|
+
b: parseInt(h[2] + h[2], 16),
|
|
336
|
+
a: h.length === 4 ? Math.round((parseInt(h[3] + h[3], 16) / 255) * 100) / 100 : 1,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (h.length === 6 || h.length === 8) {
|
|
340
|
+
return {
|
|
341
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
342
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
343
|
+
b: parseInt(h.substring(4, 6), 16),
|
|
344
|
+
a: h.length === 8 ? Math.round((parseInt(h.substring(6, 8), 16) / 255) * 100) / 100 : 1,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const func = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(str);
|
|
350
|
+
if (func) {
|
|
351
|
+
// both `1, 2, 3, 0.5` and `1 2 3 / 50%` are valid
|
|
352
|
+
const parts = func[2]
|
|
353
|
+
.replace('/', ',')
|
|
354
|
+
.split(/[\s,]+/)
|
|
355
|
+
.filter(part => part !== '');
|
|
356
|
+
if (parts.length < 3) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
if (func[1].startsWith('rgb')) {
|
|
360
|
+
return {
|
|
361
|
+
r: readChannel(parts[0], 255),
|
|
362
|
+
g: readChannel(parts[1], 255),
|
|
363
|
+
b: readChannel(parts[2], 255),
|
|
364
|
+
a: readAlpha(parts[3]),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const hue = ((parseFloat(parts[0]) % 360) + 360) % 360;
|
|
368
|
+
const rgb = hsl2rgb(hue / 360, clamp(parseFloat(parts[1]) / 100, 0, 1), clamp(parseFloat(parts[2]) / 100, 0, 1));
|
|
369
|
+
return { ...rgb, a: readAlpha(parts[3]) };
|
|
370
|
+
}
|
|
371
|
+
// color names and everything else the browser understands
|
|
372
|
+
const normalized = normalizeCssColor(str);
|
|
373
|
+
return normalized && normalized !== str ? parseColor(normalized) : null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Render `{ r, g, b, a }` in the requested notation.
|
|
377
|
+
*
|
|
378
|
+
* @param color The color to render.
|
|
379
|
+
* @param format The desired notation.
|
|
380
|
+
* @param noAlpha If true, the alpha channel is dropped.
|
|
381
|
+
* @returns the color as CSS string.
|
|
382
|
+
*/
|
|
383
|
+
export function formatColor(color, format, noAlpha) {
|
|
384
|
+
const alpha = noAlpha ? 1 : (color.a ?? 1);
|
|
385
|
+
const opaque = alpha >= 1;
|
|
386
|
+
const rounded = Math.round(alpha * 100) / 100;
|
|
387
|
+
if (format === 'hex') {
|
|
388
|
+
const hex = `#${color.r.toString(16).padStart(2, '0')}${color.g.toString(16).padStart(2, '0')}${color.b.toString(16).padStart(2, '0')}`;
|
|
389
|
+
const hexAlpha = Math.round(alpha * 255)
|
|
390
|
+
.toString(16)
|
|
391
|
+
.padStart(2, '0');
|
|
392
|
+
return opaque ? hex : `${hex}${hexAlpha}`;
|
|
393
|
+
}
|
|
394
|
+
if (format === 'hsl') {
|
|
395
|
+
const { h, s, l } = rgb2hsl(color);
|
|
396
|
+
return opaque ? `hsl(${h}, ${s}%, ${l}%)` : `hsla(${h}, ${s}%, ${l}%, ${rounded})`;
|
|
397
|
+
}
|
|
398
|
+
return opaque ? `rgb(${color.r}, ${color.g}, ${color.b})` : `rgba(${color.r}, ${color.g}, ${color.b}, ${rounded})`;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Guess the notation of the given color string.
|
|
402
|
+
*
|
|
403
|
+
* @param color The color string.
|
|
404
|
+
*/
|
|
405
|
+
function detectFormat(color) {
|
|
406
|
+
const str = (color || '').trim().toLowerCase();
|
|
407
|
+
if (str.startsWith('#')) {
|
|
408
|
+
return 'hex';
|
|
409
|
+
}
|
|
410
|
+
if (str.startsWith('rgb')) {
|
|
411
|
+
return 'rgb';
|
|
412
|
+
}
|
|
413
|
+
if (str.startsWith('hsl')) {
|
|
414
|
+
return 'hsl';
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
86
418
|
/**
|
|
87
|
-
*
|
|
419
|
+
* Read the `EyeDropper` constructor, which is not part of the DOM types yet.
|
|
420
|
+
*/
|
|
421
|
+
function getEyeDropper() {
|
|
422
|
+
if (typeof window === 'undefined') {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
return window.EyeDropper;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* A color picker component with alpha channel, eye dropper and HEX/RGB/HSL notations.
|
|
88
429
|
*/
|
|
89
430
|
export class ColorPicker extends Component {
|
|
90
431
|
/**
|
|
@@ -94,21 +435,36 @@ export class ColorPicker extends Component {
|
|
|
94
435
|
*/
|
|
95
436
|
constructor(props) {
|
|
96
437
|
super(props);
|
|
438
|
+
const value = props.value || props.color || '';
|
|
97
439
|
this.state = {
|
|
98
440
|
displayColorPicker: false,
|
|
99
|
-
|
|
441
|
+
rgba: parseColor(value) || { ...DEFAULT_COLOR },
|
|
442
|
+
textValue: value,
|
|
443
|
+
format: props.format || detectFormat(value) || 'rgb',
|
|
100
444
|
anchorEl: null,
|
|
445
|
+
recentColors: [],
|
|
101
446
|
};
|
|
102
447
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
|
|
448
|
+
/**
|
|
449
|
+
* If the props are updated from outside, they should override the state
|
|
450
|
+
*
|
|
451
|
+
* @param prevProps The previous properties.
|
|
452
|
+
*/
|
|
453
|
+
componentDidUpdate(prevProps) {
|
|
454
|
+
const prevValue = prevProps.value ?? prevProps.color ?? '';
|
|
455
|
+
const value = this.props.value ?? this.props.color ?? '';
|
|
456
|
+
// React only on real changes from outside. If the value equals what is shown, it is the own echo.
|
|
457
|
+
if (prevValue !== value && value !== this.state.textValue) {
|
|
458
|
+
this.setState({
|
|
459
|
+
rgba: parseColor(value) || { ...DEFAULT_COLOR },
|
|
460
|
+
textValue: value,
|
|
461
|
+
format: this.props.format || detectFormat(value) || this.state.format,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
else if (this.props.format && this.props.format !== prevProps.format) {
|
|
465
|
+
this.setState({ format: this.props.format });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
112
468
|
/**
|
|
113
469
|
* Convert the given color to hex ('#rrggbb') or rgba ('rgba(r,g,b,a)') format.
|
|
114
470
|
*
|
|
@@ -119,17 +475,11 @@ export class ColorPicker extends Component {
|
|
|
119
475
|
static getColor(color, isHex) {
|
|
120
476
|
if (color && typeof color === 'object') {
|
|
121
477
|
const oColor = color;
|
|
122
|
-
|
|
123
|
-
if (isHex) {
|
|
124
|
-
return `#${oColor.rgb.r.toString(16).padStart(2, '0')}${oColor.rgb.g.toString(16).padStart(2, '0')}${oColor.rgb.b.toString(16).padStart(2, '0')}`;
|
|
125
|
-
}
|
|
126
|
-
return `rgba(${oColor.rgb.r},${oColor.rgb.g},${oColor.rgb.b},${oColor.rgb.a})`;
|
|
127
|
-
}
|
|
128
|
-
const rColor = color;
|
|
478
|
+
const rColor = oColor.rgb ? oColor.rgb : color;
|
|
129
479
|
if (isHex) {
|
|
130
480
|
return `#${rColor.r.toString(16).padStart(2, '0')}${rColor.g.toString(16).padStart(2, '0')}${rColor.b.toString(16).padStart(2, '0')}`;
|
|
131
481
|
}
|
|
132
|
-
return `rgba(${rColor.r},${rColor.g},${rColor.b},${rColor.a})`;
|
|
482
|
+
return `rgba(${rColor.r},${rColor.g},${rColor.b},${rColor.a ?? 1})`;
|
|
133
483
|
}
|
|
134
484
|
if (typeof color === 'string') {
|
|
135
485
|
return isHex ? ColorPicker.rgb2hex(color || '') : color || '';
|
|
@@ -142,7 +492,7 @@ export class ColorPicker extends Component {
|
|
|
142
492
|
* @param rgb The color in rgb() or rgba() format. if not in this format, the color will be returned as is.
|
|
143
493
|
*/
|
|
144
494
|
static rgb2hex(rgb) {
|
|
145
|
-
const m =
|
|
495
|
+
const m = /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i.exec(rgb);
|
|
146
496
|
if (m) {
|
|
147
497
|
const r = parseInt(m[1], 10).toString(16).padStart(2, '0');
|
|
148
498
|
const g = parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
@@ -151,72 +501,178 @@ export class ColorPicker extends Component {
|
|
|
151
501
|
}
|
|
152
502
|
return rgb;
|
|
153
503
|
}
|
|
154
|
-
handleChange = (color) => {
|
|
155
|
-
this.setState({ color }, () => this.props.onChange && this.props.onChange(ColorPicker.getColor(color)));
|
|
156
|
-
};
|
|
157
504
|
/**
|
|
158
|
-
*
|
|
505
|
+
* Read the recently used colors from the local storage.
|
|
506
|
+
*/
|
|
507
|
+
static readRecentColors() {
|
|
508
|
+
try {
|
|
509
|
+
const stored = window.localStorage.getItem(RECENT_COLORS_KEY);
|
|
510
|
+
const parsed = stored ? JSON.parse(stored) : null;
|
|
511
|
+
if (Array.isArray(parsed)) {
|
|
512
|
+
return parsed.filter((item) => typeof item === 'string').slice(0, MAX_RECENT_COLORS);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// ignore a broken or unavailable local storage
|
|
517
|
+
}
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Remember the given color as recently used.
|
|
159
522
|
*
|
|
160
|
-
* @param
|
|
161
|
-
* @param prevState The previous state.
|
|
523
|
+
* @param color The color to remember.
|
|
162
524
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
525
|
+
storeRecentColor(color) {
|
|
526
|
+
if (this.props.noRecentColors || !color || this.state.recentColors[0] === color) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const recentColors = [color, ...this.state.recentColors.filter(item => item !== color)].slice(0, MAX_RECENT_COLORS);
|
|
530
|
+
this.setState({ recentColors });
|
|
531
|
+
try {
|
|
532
|
+
window.localStorage.setItem(RECENT_COLORS_KEY, JSON.stringify(recentColors));
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
// ignore an unavailable local storage
|
|
167
536
|
}
|
|
168
537
|
}
|
|
169
|
-
|
|
170
|
-
|
|
538
|
+
handleClick = (e) => {
|
|
539
|
+
const open = !this.state.displayColorPicker;
|
|
540
|
+
this.setState({
|
|
541
|
+
displayColorPicker: open,
|
|
542
|
+
anchorEl: open ? e.currentTarget : null,
|
|
543
|
+
// read the recent colors on every open, so colors picked in another instance show up too
|
|
544
|
+
recentColors: open && !this.props.noRecentColors ? ColorPicker.readRecentColors() : this.state.recentColors,
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
handleClose = () => {
|
|
548
|
+
this.storeRecentColor(this.state.textValue);
|
|
549
|
+
this.setState({ displayColorPicker: false, anchorEl: null });
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* The user moved a slider, clicked a swatch or used the eye dropper.
|
|
553
|
+
*
|
|
554
|
+
* @param rgba The new color.
|
|
555
|
+
*/
|
|
556
|
+
handleColorChange = (rgba) => {
|
|
557
|
+
const textValue = formatColor(rgba, this.state.format, this.props.noAlpha);
|
|
558
|
+
this.setState({ rgba, textValue }, () => this.props.onChange?.(textValue));
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* The user typed into the input field. The text is taken as it is and only parsed for the preview.
|
|
562
|
+
*
|
|
563
|
+
* @param textValue The entered text.
|
|
564
|
+
*/
|
|
565
|
+
handleTextChange = (textValue) => {
|
|
566
|
+
const rgba = parseColor(textValue);
|
|
567
|
+
this.setState({ textValue, rgba: rgba || this.state.rgba }, () => this.props.onChange?.(textValue));
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Switch between the #rrggbb, rgba() and hsla() notations.
|
|
571
|
+
*
|
|
572
|
+
* @param format The new notation.
|
|
573
|
+
*/
|
|
574
|
+
handleFormatChange = (format) => {
|
|
575
|
+
if (!this.state.textValue) {
|
|
576
|
+
this.setState({ format });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const textValue = formatColor(this.state.rgba, format, this.props.noAlpha);
|
|
580
|
+
this.setState({ format, textValue }, () => this.props.onChange?.(textValue));
|
|
581
|
+
};
|
|
582
|
+
handleClear = () => {
|
|
583
|
+
this.setState({ textValue: '', rgba: { ...DEFAULT_COLOR } }, () => this.props.onChange?.(''));
|
|
584
|
+
};
|
|
585
|
+
handleEyeDropper = () => {
|
|
586
|
+
const EyeDropper = getEyeDropper();
|
|
587
|
+
if (!EyeDropper) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
void new EyeDropper()
|
|
591
|
+
.open()
|
|
592
|
+
.then(result => {
|
|
593
|
+
const rgba = parseColor(result.sRGBHex);
|
|
594
|
+
if (rgba) {
|
|
595
|
+
// the eye dropper always returns an opaque color, so the current alpha is kept
|
|
596
|
+
this.handleColorChange({ ...rgba, a: this.props.noAlpha ? 1 : (this.state.rgba.a ?? 1) });
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
.catch(() => {
|
|
600
|
+
// the user aborted the selection with ESC
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
/**
|
|
604
|
+
* Render a row of predefined colors.
|
|
605
|
+
*
|
|
606
|
+
* @param colors The colors to show.
|
|
607
|
+
* @param title The title above the row.
|
|
608
|
+
*/
|
|
609
|
+
renderSwatches(colors, title) {
|
|
610
|
+
if (!colors.length) {
|
|
171
611
|
return null;
|
|
172
612
|
}
|
|
173
|
-
return (React.createElement(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
613
|
+
return (React.createElement(Box, { component: "div" },
|
|
614
|
+
React.createElement(Box, { component: "div", sx: styles.sectionTitle }, title),
|
|
615
|
+
React.createElement(Box, { component: "div", sx: styles.swatchRow }, colors.map((color, i) => (React.createElement(Button, { sx: styles.paletteItem, key: `${color}_${i}`, title: color, onClick: () => {
|
|
616
|
+
const rgba = parseColor(color);
|
|
617
|
+
if (rgba) {
|
|
618
|
+
this.handleColorChange(rgba);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
this.handleTextChange(color);
|
|
622
|
+
}
|
|
623
|
+
} },
|
|
624
|
+
React.createElement(Box, { component: "div", sx: styles.fill, style: { background: color } })))))));
|
|
625
|
+
}
|
|
626
|
+
renderPicker() {
|
|
627
|
+
const { rgba, textValue, format } = this.state;
|
|
628
|
+
const Picker = this.props.noAlpha ? RgbColorPicker : RgbaColorPicker;
|
|
629
|
+
return (React.createElement(Box, { component: "div", sx: styles.pickerWrapper, style: { width: this.props.pickerWidth || 280 } },
|
|
630
|
+
React.createElement(Picker, { color: rgba, onChange: (color) => this.handleColorChange({ a: rgba.a ?? 1, ...color }) }),
|
|
631
|
+
React.createElement(Box, { component: "div", sx: styles.controlRow },
|
|
632
|
+
getEyeDropper() ? (React.createElement(Tooltip, { title: I18n.t('ra_Pick color from screen') },
|
|
633
|
+
React.createElement(IconButton, { size: "small", onClick: this.handleEyeDropper },
|
|
634
|
+
React.createElement(IconColorize, { fontSize: "small" })))) : null,
|
|
635
|
+
React.createElement(Box, { component: "div", sx: styles.preview },
|
|
636
|
+
React.createElement(Box, { component: "div", sx: styles.fill, style: { background: textValue || 'transparent' } })),
|
|
637
|
+
React.createElement(TextField, { variant: "standard", size: "small", sx: styles.valueInput, value: textValue, placeholder: I18n.t('ra_Select color'), onChange: e => this.handleTextChange(e.target.value) }),
|
|
638
|
+
React.createElement(Select, { variant: "standard", disableUnderline: true, sx: styles.formatSelect, value: format, onChange: e => this.handleFormatChange(e.target.value) },
|
|
639
|
+
React.createElement(MenuItem, { value: "hex" }, "HEX"),
|
|
640
|
+
React.createElement(MenuItem, { value: "rgb" }, "RGB"),
|
|
641
|
+
React.createElement(MenuItem, { value: "hsl" }, "HSL")),
|
|
642
|
+
React.createElement(Tooltip, { title: I18n.t('ra_Clear color') },
|
|
643
|
+
React.createElement("span", null,
|
|
644
|
+
React.createElement(IconButton, { size: "small", disabled: !textValue, onClick: this.handleClear },
|
|
645
|
+
React.createElement(IconDelete, { fontSize: "small" }))))),
|
|
646
|
+
this.renderSwatches(this.props.customPalette || [], I18n.t('ra_Palette')),
|
|
647
|
+
this.renderSwatches(this.props.noRecentColors ? [] : this.state.recentColors, I18n.t('ra_Recently used colors'))));
|
|
178
648
|
}
|
|
179
649
|
render() {
|
|
180
650
|
const style = { ...(this.props.style || {}) };
|
|
181
651
|
style.position = 'relative';
|
|
182
|
-
const {
|
|
652
|
+
const { textValue } = this.state;
|
|
653
|
+
const hasLabel = !!(this.props.label || this.props.name);
|
|
183
654
|
return (React.createElement(Box, { component: "div", style: style, sx: this.props.sx || undefined, className: this.props.className || '' },
|
|
184
|
-
this.props.noInputField ? null : (React.createElement(TextField, { disabled: this.props.disabled, variant: "standard", id:
|
|
655
|
+
this.props.noInputField ? null : (React.createElement(TextField, { disabled: this.props.disabled, variant: "standard", id: this.props.id || 'ar_color_picker_name', label: this.props.label || this.props.name, value: textValue, margin: "dense", sx: {
|
|
185
656
|
'&.MuiFormControl-root': styles.textDense,
|
|
186
|
-
width:
|
|
187
|
-
mr:
|
|
188
|
-
}, onChange: e => this.
|
|
189
|
-
!this.props.noInputField &&
|
|
657
|
+
width: textValue ? 'calc(100% - 80px)' : 'calc(100% - 56px)',
|
|
658
|
+
mr: textValue ? undefined : 1,
|
|
659
|
+
}, onChange: e => this.handleTextChange(e.target.value) })),
|
|
660
|
+
!this.props.noInputField && textValue ? (React.createElement(IconButton, { disabled: this.props.disabled, onClick: this.handleClear, size: "small", style: hasLabel ? styles.delButton : undefined },
|
|
190
661
|
React.createElement(IconDelete, null))) : null,
|
|
191
662
|
React.createElement(Box, { component: "div", onClick: e => !this.props.disabled && this.handleClick(e), title: I18n.t('ra_Select color'), sx: {
|
|
192
663
|
...styles.swatch,
|
|
193
664
|
...(this.props.disabled ? styles.swatchDisabled : undefined),
|
|
194
|
-
|
|
195
|
-
border: color ? undefined : '1px dashed #ccc',
|
|
196
|
-
boxSizing: 'border-box',
|
|
197
|
-
marginTop: this.props.noInputField || !(this.props.label || this.props.name) ? 0 : undefined,
|
|
665
|
+
marginTop: this.props.noInputField || !hasLabel ? 0 : undefined,
|
|
198
666
|
} },
|
|
199
|
-
React.createElement("div",
|
|
667
|
+
React.createElement(Box, { component: "div", sx: {
|
|
200
668
|
...styles.color,
|
|
201
|
-
|
|
669
|
+
border: textValue ? undefined : '1px dashed #888',
|
|
670
|
+
boxSizing: 'border-box',
|
|
671
|
+
}, style: {
|
|
202
672
|
width: this.props.noInputField ? this.props.barWidth || 16 : this.props.barWidth || 36,
|
|
203
|
-
} }
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'&. MuiMenu-list': styles.popoverList,
|
|
207
|
-
}, anchorEl: this.state.anchorEl, open: !0, onClose: () => this.handleClose() },
|
|
208
|
-
React.createElement(ChromePicker, { color: this.state.color || undefined, onChangeComplete: (_color) => this.handleChange(_color.rgb), styles: {
|
|
209
|
-
default: {
|
|
210
|
-
picker: {
|
|
211
|
-
backgroundColor: this.props.theme?.palette.background.paper || '#888',
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
} }),
|
|
215
|
-
color && this.props.noInputField ? (React.createElement(IconButton, { sx: styles.closeButton, onClick: () => this.handleChange('') },
|
|
216
|
-
React.createElement(IconDelete, null))) : null,
|
|
217
|
-
React.createElement(IconButton, { sx: styles.closeButton, onClick: () => this.handleClose() },
|
|
218
|
-
React.createElement(IconClose, null)),
|
|
219
|
-
this.renderCustomPalette())) : null));
|
|
673
|
+
} },
|
|
674
|
+
React.createElement(Box, { component: "div", sx: styles.fill, style: { background: textValue || 'transparent' } }))),
|
|
675
|
+
this.state.displayColorPicker && !this.props.disabled ? (React.createElement(Popover, { sx: styles.popover, anchorEl: this.state.anchorEl, open: !0, onClose: this.handleClose, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' } }, this.renderPicker())) : null));
|
|
220
676
|
}
|
|
221
677
|
}
|
|
222
678
|
//# sourceMappingURL=ColorPicker.js.map
|