@vanilla-bean/components 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/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- package/utils/string.js +72 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { TinyColor, random as randomColor } from '@ctrl/tinycolor';
|
|
2
|
+
|
|
3
|
+
import { debounce, throttle, convertRange } from '../../utils';
|
|
4
|
+
import { Component } from '../../Component';
|
|
5
|
+
import { styled } from '../../styled';
|
|
6
|
+
import theme from '../../theme';
|
|
7
|
+
import { Input } from '../Input';
|
|
8
|
+
import { Button } from '../Button';
|
|
9
|
+
import { saturation } from './svg';
|
|
10
|
+
|
|
11
|
+
const StyledInput = styled(
|
|
12
|
+
Input,
|
|
13
|
+
({ colors }) => `
|
|
14
|
+
background-color: ${colors.light(colors.gray)};
|
|
15
|
+
padding: 18px 18px 12px;
|
|
16
|
+
border-radius: 3px;
|
|
17
|
+
margin-bottom: 6px;
|
|
18
|
+
text-indent: 0;
|
|
19
|
+
border: none;
|
|
20
|
+
|
|
21
|
+
& input {
|
|
22
|
+
margin: 18px auto 9px;
|
|
23
|
+
width: 90%;
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const PickerArea = styled(
|
|
29
|
+
Component,
|
|
30
|
+
({ colors }) => `
|
|
31
|
+
position: relative;
|
|
32
|
+
width: 150px;
|
|
33
|
+
height: 150px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
box-shadow: 0px 0px 0px 2px ${colors.darker(colors.gray)};
|
|
36
|
+
|
|
37
|
+
& * {
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const PickerIndicator = styled(
|
|
44
|
+
Component,
|
|
45
|
+
() => `
|
|
46
|
+
position: absolute;
|
|
47
|
+
top: -4px;
|
|
48
|
+
left: -4px;
|
|
49
|
+
width: 8px;
|
|
50
|
+
height: 8px;
|
|
51
|
+
border-radius: 50%;
|
|
52
|
+
border: 1.5px solid white;
|
|
53
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
|
|
54
|
+
background: transparent;
|
|
55
|
+
pointer-events: none;
|
|
56
|
+
`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const HueArea = styled(
|
|
60
|
+
Component,
|
|
61
|
+
({ colors }) => `
|
|
62
|
+
position: relative;
|
|
63
|
+
width: 150px;
|
|
64
|
+
height: 30px;
|
|
65
|
+
margin: 10px auto 0;
|
|
66
|
+
box-shadow: 0px 0px 0px 2px ${colors.darker(colors.gray)};
|
|
67
|
+
cursor: ew-resize;
|
|
68
|
+
background: linear-gradient(to right,
|
|
69
|
+
hsl(0,100%,50%), hsl(10,100%,50%), hsl(20,100%,50%), hsl(30,100%,50%),
|
|
70
|
+
hsl(40,100%,50%), hsl(50,100%,50%), hsl(60,100%,50%), hsl(70,100%,50%),
|
|
71
|
+
hsl(80,100%,50%), hsl(90,100%,50%), hsl(100,100%,50%), hsl(110,100%,50%),
|
|
72
|
+
hsl(120,100%,50%), hsl(130,100%,50%), hsl(140,100%,50%), hsl(150,100%,50%),
|
|
73
|
+
hsl(160,100%,50%), hsl(170,100%,50%), hsl(180,100%,50%), hsl(190,100%,50%),
|
|
74
|
+
hsl(200,100%,50%), hsl(210,100%,50%), hsl(220,100%,50%), hsl(230,100%,50%),
|
|
75
|
+
hsl(240,100%,50%), hsl(250,100%,50%), hsl(260,100%,50%), hsl(270,100%,50%),
|
|
76
|
+
hsl(280,100%,50%), hsl(290,100%,50%), hsl(300,100%,50%), hsl(310,100%,50%),
|
|
77
|
+
hsl(320,100%,50%), hsl(330,100%,50%), hsl(340,100%,50%), hsl(350,100%,50%),
|
|
78
|
+
hsl(360,100%,50%));
|
|
79
|
+
background-size: 100% 100%;
|
|
80
|
+
background-repeat: repeat-x;
|
|
81
|
+
|
|
82
|
+
& * {
|
|
83
|
+
pointer-events: none;
|
|
84
|
+
}
|
|
85
|
+
`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const HueIndicator = styled(
|
|
89
|
+
Component,
|
|
90
|
+
() => `
|
|
91
|
+
position: absolute;
|
|
92
|
+
width: 8px;
|
|
93
|
+
top: -3px;
|
|
94
|
+
bottom: -3px;
|
|
95
|
+
left: calc(50% - 4px);
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
border: 1.5px solid white;
|
|
98
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
|
|
99
|
+
background: transparent;
|
|
100
|
+
pointer-events: none;
|
|
101
|
+
`,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const ColorSwatch = styled(
|
|
105
|
+
Button,
|
|
106
|
+
({ colors }) => `
|
|
107
|
+
margin-top: 3px;
|
|
108
|
+
margin-right: 3px;
|
|
109
|
+
|
|
110
|
+
&.rainbow {
|
|
111
|
+
animation: rainbow 2s linear;
|
|
112
|
+
animation-iteration-count: infinite;
|
|
113
|
+
color: ${colors.black};
|
|
114
|
+
|
|
115
|
+
@keyframes rainbow {
|
|
116
|
+
100%,0%{
|
|
117
|
+
background-color: ${colors.light(colors.red)};
|
|
118
|
+
}
|
|
119
|
+
12%{
|
|
120
|
+
background-color: ${colors.light(colors.orange)};
|
|
121
|
+
}
|
|
122
|
+
25%{
|
|
123
|
+
background-color: ${colors.light(colors.yellow)};
|
|
124
|
+
}
|
|
125
|
+
37%{
|
|
126
|
+
background-color: ${colors.light(colors.green)};
|
|
127
|
+
}
|
|
128
|
+
50%{
|
|
129
|
+
background-color: ${colors.light(colors.teal)};
|
|
130
|
+
}
|
|
131
|
+
62%{
|
|
132
|
+
background-color: ${colors.light(colors.blue)};
|
|
133
|
+
}
|
|
134
|
+
75%{
|
|
135
|
+
background-color: ${colors.light(colors.purple)};
|
|
136
|
+
}
|
|
137
|
+
87%{
|
|
138
|
+
background-color: ${colors.light(colors.pink)};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`,
|
|
143
|
+
{ icon: 'fill-drip' },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const defaultOptions = { tag: 'div', value: '#666', onChange: () => {} };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Interactive color picker component with HSL controls and color swatches.
|
|
150
|
+
*
|
|
151
|
+
* Provides visual color selection with saturation/lightness area and hue slider controls.
|
|
152
|
+
* Supports text input, predefined color swatches, and random color generation.
|
|
153
|
+
* @param {object} [options={}] - ColorPicker configuration options
|
|
154
|
+
* @param {string} [options.tag='div'] - HTML tag for the component wrapper
|
|
155
|
+
* @param {string} [options.value='#666'] - Initial color value (CSS color, 'random' for random color)
|
|
156
|
+
* @param {Function} [options.onChange] - Callback function called when color changes
|
|
157
|
+
* @param {Array<string>} [options.swatches] - Array of predefined color values for swatch buttons
|
|
158
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
159
|
+
* @returns {ColorPicker} ColorPicker component instance
|
|
160
|
+
*/
|
|
161
|
+
class ColorPicker extends StyledInput {
|
|
162
|
+
defaultOptions = { ...super.defaultOptions, ...defaultOptions };
|
|
163
|
+
|
|
164
|
+
constructor(options = {}, ...children) {
|
|
165
|
+
options = { ...defaultOptions, ...options };
|
|
166
|
+
|
|
167
|
+
if (options.value === 'random') options.value = randomColor().toHslString();
|
|
168
|
+
|
|
169
|
+
super(options, ...children);
|
|
170
|
+
|
|
171
|
+
this.pointers = {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
build() {
|
|
175
|
+
this.textInput = new Input({
|
|
176
|
+
type: 'text',
|
|
177
|
+
value: this.options.subscriber('value', value => this.hslString || value),
|
|
178
|
+
onChange: ({ value }) => this.change(value),
|
|
179
|
+
onKeyUp: debounce(({ target: { value } }) => this.change(value), 700),
|
|
180
|
+
prependTo: this.elem,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
this.hueArea = new HueArea({
|
|
184
|
+
prependTo: this.elem,
|
|
185
|
+
onPointerDown: this.interactionInit.bind(this),
|
|
186
|
+
});
|
|
187
|
+
this.hueIndicator = new HueIndicator({ appendTo: this.hueArea });
|
|
188
|
+
|
|
189
|
+
this.pickerArea = new PickerArea({
|
|
190
|
+
innerHTML: saturation(this.uniqueId),
|
|
191
|
+
prependTo: this.elem,
|
|
192
|
+
onPointerDown: this.interactionInit.bind(this),
|
|
193
|
+
});
|
|
194
|
+
this.pickerIndicator = new PickerIndicator({
|
|
195
|
+
appendTo: this.pickerArea,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const sizeObserver = new ResizeObserver(() => {
|
|
199
|
+
if (this.pickerArea.elem.clientWidth > 0) {
|
|
200
|
+
this._positionIndicators();
|
|
201
|
+
sizeObserver.disconnect();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
sizeObserver.observe(this.pickerArea.elem);
|
|
205
|
+
this.addCleanup('sizeObserver', () => sizeObserver.disconnect());
|
|
206
|
+
|
|
207
|
+
if (this.options.swatches) {
|
|
208
|
+
this.options.swatches.forEach(color => {
|
|
209
|
+
new ColorSwatch({
|
|
210
|
+
appendTo: this.elem,
|
|
211
|
+
|
|
212
|
+
onPointerPress: () => this.change(color),
|
|
213
|
+
...(color === 'random'
|
|
214
|
+
? { addClass: ['rainbow'] }
|
|
215
|
+
: {
|
|
216
|
+
style: {
|
|
217
|
+
backgroundColor: color,
|
|
218
|
+
color: theme.colors.mostReadable(color, [theme.colors.white, theme.colors.black]),
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Changes the color value and triggers change event.
|
|
228
|
+
* @param {string|object} value - New color value (CSS string, HSL object, or 'random')
|
|
229
|
+
*/
|
|
230
|
+
change(value) {
|
|
231
|
+
this.options.value = this.parseValue(value).hslString;
|
|
232
|
+
|
|
233
|
+
this.elem.dispatchEvent(new CustomEvent('change', { detail: { value: this.options.value } }));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// HSV ↔ HSL conversion helpers (internal, not part of public API)
|
|
237
|
+
static hsvToHsl(h, sv, v) {
|
|
238
|
+
const l = v * (1 - sv / 2);
|
|
239
|
+
const s = l === 0 || l === 1 ? 0 : (v - l) / Math.min(l, 1 - l);
|
|
240
|
+
return { h, s, l };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static hslToHsv(h, s, l) {
|
|
244
|
+
const v = l + s * Math.min(l, 1 - l);
|
|
245
|
+
const sv = v === 0 ? 0 : 2 * (1 - l / v);
|
|
246
|
+
return { h, sv, v };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Parses color value into HSL components and TinyColor instance.
|
|
251
|
+
* @param {string|object} value - Color value to parse
|
|
252
|
+
* @returns {object} Parsed color data
|
|
253
|
+
*/
|
|
254
|
+
parseValue(value) {
|
|
255
|
+
const color = value === 'random' ? randomColor() : new TinyColor(value);
|
|
256
|
+
const { h, s, l } =
|
|
257
|
+
typeof value === 'object' ? { h: value.h ?? this.hue, s: value.s ?? 0, l: value.l ?? 0.5 } : color.toHsl();
|
|
258
|
+
const hslString =
|
|
259
|
+
typeof value === 'object'
|
|
260
|
+
? `hsl(${Math.round(h)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`
|
|
261
|
+
: color.toHslString();
|
|
262
|
+
const { sv, v } = ColorPicker.hslToHsv(h, s, l);
|
|
263
|
+
|
|
264
|
+
return { h, s, l, sv, v, color, hslString };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_setHueBackground(hue, width) {
|
|
268
|
+
// Shift the repeating gradient so the selected hue sits under the center needle.
|
|
269
|
+
// background-position-x shifts the image rightward by the offset value,
|
|
270
|
+
// so a positive value moves the gradient right (earlier hues appear at center).
|
|
271
|
+
const offset = width * (0.5 - hue / 360);
|
|
272
|
+
this.hueArea.elem.style.backgroundPositionX = `${offset}px`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_positionIndicators() {
|
|
276
|
+
if (!this.pickerArea?.elem || !this.hueArea?.elem) return;
|
|
277
|
+
|
|
278
|
+
const pickerW = this.pickerArea.elem.clientWidth;
|
|
279
|
+
const pickerH = this.pickerArea.elem.clientHeight;
|
|
280
|
+
const hueW = this.hueArea.elem.clientWidth;
|
|
281
|
+
|
|
282
|
+
if (!pickerW || !hueW) return;
|
|
283
|
+
|
|
284
|
+
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
|
|
285
|
+
|
|
286
|
+
// HSV: x = saturation, y = (1 - value) — matches the gradient exactly
|
|
287
|
+
const pickerX = clamp((this.sv ?? 0) * pickerW, 0, pickerW);
|
|
288
|
+
const pickerY = clamp((1 - (this.v ?? 1)) * pickerH, 0, pickerH);
|
|
289
|
+
this.pickerIndicator.elem.style.transform = `translate3d(${pickerX}px, ${pickerY}px, 0)`;
|
|
290
|
+
|
|
291
|
+
// Hue: shift the gradient so selected hue appears under the center needle
|
|
292
|
+
this._setHueBackground(this.hue ?? 0, hueW);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
static handlers = {
|
|
296
|
+
value(value) {
|
|
297
|
+
if (!value) return;
|
|
298
|
+
|
|
299
|
+
const { h, sv, v, color, hslString } = this.parseValue(value);
|
|
300
|
+
|
|
301
|
+
this.hslString = hslString;
|
|
302
|
+
this.color = color;
|
|
303
|
+
// Preserve hue when achromatic (sv=0 loses hue info) or when TinyColor
|
|
304
|
+
// wraps 360 to 0 (same color, different position on the slider)
|
|
305
|
+
const isCircularWrap = sv > 0 && h === 0 && this.hue > 350;
|
|
306
|
+
if (!isCircularWrap) this.hue = sv > 0 ? h : (this.hue ?? h);
|
|
307
|
+
this.sv = sv;
|
|
308
|
+
this.v = v;
|
|
309
|
+
|
|
310
|
+
this.elem.style.backgroundColor = hslString;
|
|
311
|
+
|
|
312
|
+
this.pickerArea?.elem && (this.pickerArea.elem.style.backgroundColor = `hsl(${this.hue}, 100%, 50%)`);
|
|
313
|
+
|
|
314
|
+
const tryPosition = () => {
|
|
315
|
+
if (this.pickerArea?.elem?.clientWidth > 0) {
|
|
316
|
+
this._positionIndicators();
|
|
317
|
+
} else {
|
|
318
|
+
requestAnimationFrame(tryPosition);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
requestAnimationFrame(tryPosition);
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
getPosition({ clientX, clientY }) {
|
|
326
|
+
return { x: clientX, y: clientY };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
normalizePosition(event, parent, offsetX, offsetY) {
|
|
330
|
+
const position = this.getPosition(event);
|
|
331
|
+
const boundingRect = parent.getBoundingClientRect();
|
|
332
|
+
|
|
333
|
+
// Account for parent element position
|
|
334
|
+
position.x -= boundingRect.left;
|
|
335
|
+
position.y -= boundingRect.top;
|
|
336
|
+
|
|
337
|
+
// Account for indicator size
|
|
338
|
+
position.x += offsetX;
|
|
339
|
+
position.y += offsetY;
|
|
340
|
+
|
|
341
|
+
// Account for parent element bounds
|
|
342
|
+
position.x = Math.min(Math.max(offsetX, position.x), parent.clientWidth + offsetX);
|
|
343
|
+
position.y = Math.min(Math.max(offsetY, position.y), parent.clientHeight + offsetY);
|
|
344
|
+
|
|
345
|
+
return position;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interactionInit(event) {
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
|
|
351
|
+
const { pointerId } = event;
|
|
352
|
+
|
|
353
|
+
if (!this.pointers[pointerId]) this.pointers[pointerId] = {};
|
|
354
|
+
|
|
355
|
+
this.activePointerCount = Object.values(this.pointers).filter(_ => !!_).length;
|
|
356
|
+
|
|
357
|
+
if (this.pointerInteracting) return;
|
|
358
|
+
|
|
359
|
+
this.pointerInteracting = true;
|
|
360
|
+
this.pointers[pointerId].initiator = true;
|
|
361
|
+
|
|
362
|
+
const move = throttle(this[`${event.target === this.hueArea.elem ? 'hue' : 'picker'}Move`].bind(this), 60);
|
|
363
|
+
const removePointer = event => {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
|
|
366
|
+
if (!this.pointers[event.pointerId]) return;
|
|
367
|
+
|
|
368
|
+
move(event);
|
|
369
|
+
|
|
370
|
+
delete this.pointers[event.pointerId];
|
|
371
|
+
|
|
372
|
+
this.activePointerCount = Object.values(this.pointers).filter(_ => !!_).length;
|
|
373
|
+
|
|
374
|
+
if (this.activePointerCount === 0) {
|
|
375
|
+
this.pointerInteracting = false;
|
|
376
|
+
|
|
377
|
+
document.removeEventListener('pointermove', move);
|
|
378
|
+
document.removeEventListener('pointerup', removePointer);
|
|
379
|
+
document.removeEventListener('pointerleave', removePointer);
|
|
380
|
+
document.removeEventListener('pointercancel', removePointer);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
document.addEventListener('pointermove', move);
|
|
385
|
+
document.addEventListener('pointerup', removePointer);
|
|
386
|
+
document.addEventListener('pointerleave', removePointer);
|
|
387
|
+
document.addEventListener('pointercancel', removePointer);
|
|
388
|
+
|
|
389
|
+
this.replaceCleanup('gestureListeners', () => {
|
|
390
|
+
document.removeEventListener('pointermove', move);
|
|
391
|
+
document.removeEventListener('pointerup', removePointer);
|
|
392
|
+
document.removeEventListener('pointerleave', removePointer);
|
|
393
|
+
document.removeEventListener('pointercancel', removePointer);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
pickerMove(event) {
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
|
|
400
|
+
const { pointerId } = event;
|
|
401
|
+
|
|
402
|
+
if (!this.pointers[pointerId] || !this.pointers[pointerId].initiator || this.activePointerCount > 2) return;
|
|
403
|
+
|
|
404
|
+
const indicatorOffsetX = this.pickerIndicator.elem.clientWidth / 2;
|
|
405
|
+
const indicatorOffsetY = this.pickerIndicator.elem.clientHeight / 2;
|
|
406
|
+
const position = this.normalizePosition(event, this.pickerArea.elem, indicatorOffsetX, indicatorOffsetY);
|
|
407
|
+
const pickerAreaWidth = this.pickerArea.elem.clientWidth;
|
|
408
|
+
const pickerAreaHeight = this.pickerArea.elem.clientHeight;
|
|
409
|
+
|
|
410
|
+
position.x -= indicatorOffsetX;
|
|
411
|
+
position.y -= indicatorOffsetY;
|
|
412
|
+
|
|
413
|
+
const newSv = convertRange(position.x, [0, pickerAreaWidth], [0, 1]);
|
|
414
|
+
const newV = convertRange(position.y, [0, pickerAreaHeight], [1, 0]);
|
|
415
|
+
const { h, s, l } = ColorPicker.hsvToHsl(this.hue, newSv, newV);
|
|
416
|
+
|
|
417
|
+
this.pickerIndicator.elem.style.transform = `translate3d(${position.x}px, ${position.y}px, 0)`;
|
|
418
|
+
|
|
419
|
+
this.change({ h, s, l });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
hueMove(event) {
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
|
|
425
|
+
const { pointerId } = event;
|
|
426
|
+
|
|
427
|
+
if (!this.pointers[pointerId] || !this.pointers[pointerId].initiator || this.activePointerCount > 2) return;
|
|
428
|
+
|
|
429
|
+
const prevX = this.pointers[pointerId].prevX ?? event.clientX;
|
|
430
|
+
const delta = event.clientX - prevX;
|
|
431
|
+
this.pointers[pointerId].prevX = event.clientX;
|
|
432
|
+
|
|
433
|
+
const hueAreaWidth = this.hueArea.elem.clientWidth;
|
|
434
|
+
const newHue = (this.hue - (delta / hueAreaWidth) * 360 + 720) % 360;
|
|
435
|
+
|
|
436
|
+
this._setHueBackground(newHue, hueAreaWidth);
|
|
437
|
+
|
|
438
|
+
const sv = this.sv > 0 ? this.sv : 1;
|
|
439
|
+
const v = this.v > 0 ? this.v : 0.7;
|
|
440
|
+
const { s, l } = ColorPicker.hsvToHsl(newHue, sv, v);
|
|
441
|
+
this.change({ h: newHue, s, l });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export default ColorPicker;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ColorPicker
|
|
2
|
+
|
|
3
|
+
> ./ColorPicker.js
|
|
4
|
+
|
|
5
|
+
HSL-based color picker where hue and saturation/lightness are controlled by separate areas. The key design decision is that `value: 'random'` is a valid state; the picker starts in a selection mode rather than requiring a starting color.
|
|
6
|
+
|
|
7
|
+
## Multiple input formats are accepted without the caller normalizing first
|
|
8
|
+
|
|
9
|
+
- color strings, color objects, and the sentinel 'random' all produce a valid picker state without error
|
|
10
|
+
- does initializing with a color string produce a valid picker state?
|
|
11
|
+
- does initializing with value 'random' produce a valid picker state?
|
|
12
|
+
|
|
13
|
+
## Dragging outside the picker area does not produce out-of-range colors
|
|
14
|
+
|
|
15
|
+
- the position is clamped to the picker's bounds regardless of where the pointer goes; callers receive valid color values even during aggressive drag behavior
|
|
16
|
+
- does dragging well outside the saturation area still produce a valid color value?
|
|
17
|
+
|
|
18
|
+
## Hue and saturation areas have independent pointer tracking
|
|
19
|
+
|
|
20
|
+
- interaction with the hue bar does not affect the saturation/lightness position and vice versa
|
|
21
|
+
- does adjusting the hue leave the saturation value unchanged?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorPicker } from './ColorPicker';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const saturation = (id = '') =>
|
|
2
|
+
`<svg width="100%" height="100%"><defs><linearGradient id="gradient-black${id}" x1="0%" y1="105%" x2="0%" y2="0%"><stop offset="0%" stop-color="#000000" stop-opacity="1"></stop><stop offset="100%" stop-color="#000000" stop-opacity="0"></stop></linearGradient><linearGradient id="gradient-white${id}" x1="0%" y1="100%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"></stop><stop offset="100%" stop-color="#FFFFFF" stop-opacity="0"></stop></linearGradient></defs><rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-white${id})"></rect><rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-black${id})"></rect></svg>`;
|
|
3
|
+
|
|
4
|
+
export const hue = (id = '') =>
|
|
5
|
+
`<svg width="100%" height="100%"><defs><linearGradient id="gradient-hue${id}" x1="100%" y1="0%" x2="0%" y2="0%"><stop offset="0%" stop-color="#FF0000" stop-opacity="1"></stop><stop offset="13%" stop-color="#FF00FF" stop-opacity="1"></stop><stop offset="25%" stop-color="#8000FF" stop-opacity="1"></stop><stop offset="38%" stop-color="#0040FF" stop-opacity="1"></stop><stop offset="50%" stop-color="#00FFFF" stop-opacity="1"></stop><stop offset="63%" stop-color="#00FF40" stop-opacity="1"></stop><stop offset="75%" stop-color="#0BED00" stop-opacity="1"></stop><stop offset="88%" stop-color="#FFFF00" stop-opacity="1"></stop><stop offset="100%" stop-color="#FF0000" stop-opacity="1"></stop></linearGradient></defs><rect x="0" y="0" width="100%" height="100%" fill="url(#gradient-hue${id})"></rect></svg>`;
|