@dodlhuat/basix 1.3.4 → 1.4.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/README.md +160 -18
- package/css/checkbox.scss +15 -2
- package/css/color-picker.scss +278 -0
- package/css/datepicker.scss +5 -3
- package/css/push-menu.scss +7 -0
- package/css/stepper.scss +1 -1
- package/css/style.css +291 -6
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.min.css.map +1 -1
- package/css/style.scss +1 -0
- package/css/table.scss +33 -0
- package/js/bottom-sheet.d.ts +0 -2
- package/js/bottom-sheet.js +0 -2
- package/js/calendar.d.ts +9 -17
- package/js/calendar.js +4 -7
- package/js/carousel.d.ts +0 -2
- package/js/carousel.js +1 -3
- package/js/chart.d.ts +6 -14
- package/js/chart.js +3 -5
- package/js/code-viewer.d.ts +2 -1
- package/js/code-viewer.js +8 -2
- package/js/color-picker.d.ts +50 -0
- package/js/color-picker.js +291 -0
- package/js/context-menu.d.ts +0 -3
- package/js/context-menu.js +0 -3
- package/js/datepicker.d.ts +0 -4
- package/js/datepicker.js +0 -1
- package/js/docs-nav.js +1 -0
- package/js/dropdown.d.ts +1 -4
- package/js/dropdown.js +0 -1
- package/js/editor.d.ts +4 -3
- package/js/editor.js +50 -35
- package/js/file-uploader.d.ts +0 -4
- package/js/file-uploader.js +0 -1
- package/js/flyout-menu.d.ts +0 -2
- package/js/flyout-menu.js +0 -1
- package/js/gallery.d.ts +1 -4
- package/js/gallery.js +18 -23
- package/js/group-picker.d.ts +0 -5
- package/js/group-picker.js +6 -8
- package/js/lightbox.d.ts +1 -4
- package/js/lightbox.js +6 -10
- package/js/modal.d.ts +0 -2
- package/js/modal.js +0 -1
- package/js/popover.d.ts +0 -3
- package/js/popover.js +0 -2
- package/js/position.d.ts +0 -13
- package/js/position.js +0 -12
- package/js/push-menu.d.ts +0 -3
- package/js/push-menu.js +6 -11
- package/js/range-slider.d.ts +0 -1
- package/js/range-slider.js +2 -3
- package/js/scroll.d.ts +0 -2
- package/js/scroll.js +5 -6
- package/js/scrollbar.d.ts +2 -10
- package/js/scrollbar.js +25 -42
- package/js/select.d.ts +2 -3
- package/js/select.js +6 -9
- package/js/sidebar-nav.d.ts +4 -15
- package/js/sidebar-nav.js +21 -35
- package/js/stepper.d.ts +5 -2
- package/js/stepper.js +42 -6
- package/js/table.d.ts +1 -56
- package/js/table.js +4 -56
- package/js/tabs.d.ts +1 -32
- package/js/tabs.js +7 -39
- package/js/theme.d.ts +0 -44
- package/js/theme.js +0 -44
- package/js/timepicker.d.ts +1 -4
- package/js/timepicker.js +1 -2
- package/js/toast.d.ts +0 -2
- package/js/toast.js +0 -1
- package/js/tooltip.d.ts +0 -4
- package/js/tooltip.js +0 -1
- package/js/tree.d.ts +0 -3
- package/js/tree.js +9 -11
- package/js/utils.d.ts +0 -10
- package/js/utils.js +2 -42
- package/js/virtual-dropdown.d.ts +3 -5
- package/js/virtual-dropdown.js +17 -66
- package/package.json +1 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
class ColorPicker {
|
|
2
|
+
container;
|
|
3
|
+
onChange;
|
|
4
|
+
canvas;
|
|
5
|
+
ctx;
|
|
6
|
+
cursor;
|
|
7
|
+
hueSlider;
|
|
8
|
+
hexInput;
|
|
9
|
+
rInput;
|
|
10
|
+
gInput;
|
|
11
|
+
bInput;
|
|
12
|
+
preview;
|
|
13
|
+
hue = 0;
|
|
14
|
+
saturation = 100;
|
|
15
|
+
brightness = 100;
|
|
16
|
+
isDragging = false;
|
|
17
|
+
abortController = new AbortController();
|
|
18
|
+
ro;
|
|
19
|
+
constructor(elementOrSelector, options = {}) {
|
|
20
|
+
const el = typeof elementOrSelector === 'string'
|
|
21
|
+
? document.querySelector(elementOrSelector)
|
|
22
|
+
: elementOrSelector;
|
|
23
|
+
if (!el)
|
|
24
|
+
throw new Error(`ColorPicker: element not found for "${elementOrSelector}"`);
|
|
25
|
+
this.container = el;
|
|
26
|
+
this.onChange = options.onChange;
|
|
27
|
+
this.build();
|
|
28
|
+
this.bindEvents();
|
|
29
|
+
this.ro = new ResizeObserver(() => this.resizeCanvas());
|
|
30
|
+
this.ro.observe(this.canvas.parentElement);
|
|
31
|
+
this.resizeCanvas();
|
|
32
|
+
if (options.value) {
|
|
33
|
+
this.setValue(options.value);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.updateFromHSB();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
build() {
|
|
40
|
+
this.container.classList.add('color-picker');
|
|
41
|
+
this.container.innerHTML = `
|
|
42
|
+
<div class="color-picker__field">
|
|
43
|
+
<canvas class="color-picker__canvas"></canvas>
|
|
44
|
+
<div class="color-picker__cursor" aria-hidden="true"></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="color-picker__controls">
|
|
47
|
+
<div class="color-picker__hue-track">
|
|
48
|
+
<input type="range" class="color-picker__hue-slider" min="0" max="360" value="0" aria-label="Hue">
|
|
49
|
+
</div>
|
|
50
|
+
<div class="color-picker__preview" aria-hidden="true"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="color-picker__inputs">
|
|
53
|
+
<div class="color-picker__input-group color-picker__input-group--hex">
|
|
54
|
+
<label>HEX</label>
|
|
55
|
+
<input type="text" class="color-picker__hex" value="#FF0000" spellcheck="false" autocomplete="off" maxlength="7">
|
|
56
|
+
</div>
|
|
57
|
+
<div class="color-picker__input-group">
|
|
58
|
+
<label>R</label>
|
|
59
|
+
<input type="number" class="color-picker__r" min="0" max="255" value="255">
|
|
60
|
+
</div>
|
|
61
|
+
<div class="color-picker__input-group">
|
|
62
|
+
<label>G</label>
|
|
63
|
+
<input type="number" class="color-picker__g" min="0" max="255" value="0">
|
|
64
|
+
</div>
|
|
65
|
+
<div class="color-picker__input-group">
|
|
66
|
+
<label>B</label>
|
|
67
|
+
<input type="number" class="color-picker__b" min="0" max="255" value="0">
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
const canvas = this.container.querySelector('.color-picker__canvas');
|
|
72
|
+
const ctx = canvas?.getContext('2d');
|
|
73
|
+
if (!canvas || !ctx)
|
|
74
|
+
throw new Error('ColorPicker: canvas context unavailable');
|
|
75
|
+
this.canvas = canvas;
|
|
76
|
+
this.ctx = ctx;
|
|
77
|
+
this.cursor = this.container.querySelector('.color-picker__cursor');
|
|
78
|
+
this.hueSlider = this.container.querySelector('.color-picker__hue-slider');
|
|
79
|
+
this.hexInput = this.container.querySelector('.color-picker__hex');
|
|
80
|
+
this.rInput = this.container.querySelector('.color-picker__r');
|
|
81
|
+
this.gInput = this.container.querySelector('.color-picker__g');
|
|
82
|
+
this.bInput = this.container.querySelector('.color-picker__b');
|
|
83
|
+
this.preview = this.container.querySelector('.color-picker__preview');
|
|
84
|
+
}
|
|
85
|
+
bindEvents() {
|
|
86
|
+
const sig = { signal: this.abortController.signal };
|
|
87
|
+
this.canvas.addEventListener('pointerdown', (e) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
this.canvas.setPointerCapture(e.pointerId);
|
|
90
|
+
this.isDragging = true;
|
|
91
|
+
this.handleFieldInteraction(e);
|
|
92
|
+
}, sig);
|
|
93
|
+
this.canvas.addEventListener('pointermove', (e) => {
|
|
94
|
+
if (!this.isDragging)
|
|
95
|
+
return;
|
|
96
|
+
this.handleFieldInteraction(e);
|
|
97
|
+
}, sig);
|
|
98
|
+
this.canvas.addEventListener('pointerup', () => {
|
|
99
|
+
this.isDragging = false;
|
|
100
|
+
}, sig);
|
|
101
|
+
this.canvas.addEventListener('pointercancel', () => {
|
|
102
|
+
this.isDragging = false;
|
|
103
|
+
}, sig);
|
|
104
|
+
this.hueSlider.addEventListener('input', () => {
|
|
105
|
+
this.hue = +this.hueSlider.value;
|
|
106
|
+
this.drawField();
|
|
107
|
+
this.updateFromHSB();
|
|
108
|
+
}, sig);
|
|
109
|
+
this.hexInput.addEventListener('change', () => this.handleHexInput(), sig);
|
|
110
|
+
this.rInput.addEventListener('change', () => this.handleRGBInput(), sig);
|
|
111
|
+
this.gInput.addEventListener('change', () => this.handleRGBInput(), sig);
|
|
112
|
+
this.bInput.addEventListener('change', () => this.handleRGBInput(), sig);
|
|
113
|
+
}
|
|
114
|
+
resizeCanvas() {
|
|
115
|
+
const w = this.canvas.offsetWidth;
|
|
116
|
+
const h = this.canvas.offsetHeight;
|
|
117
|
+
if (w === 0 || h === 0)
|
|
118
|
+
return;
|
|
119
|
+
if (this.canvas.width === w && this.canvas.height === h)
|
|
120
|
+
return;
|
|
121
|
+
this.canvas.width = w;
|
|
122
|
+
this.canvas.height = h;
|
|
123
|
+
this.drawField();
|
|
124
|
+
this.updateCursor();
|
|
125
|
+
}
|
|
126
|
+
drawField() {
|
|
127
|
+
const { width, height } = this.canvas;
|
|
128
|
+
if (width === 0 || height === 0)
|
|
129
|
+
return;
|
|
130
|
+
const gradH = this.ctx.createLinearGradient(0, 0, width, 0);
|
|
131
|
+
gradH.addColorStop(0, '#fff');
|
|
132
|
+
gradH.addColorStop(1, `hsl(${this.hue}, 100%, 50%)`);
|
|
133
|
+
this.ctx.fillStyle = gradH;
|
|
134
|
+
this.ctx.fillRect(0, 0, width, height);
|
|
135
|
+
const gradV = this.ctx.createLinearGradient(0, 0, 0, height);
|
|
136
|
+
gradV.addColorStop(0, 'rgba(0,0,0,0)');
|
|
137
|
+
gradV.addColorStop(1, '#000');
|
|
138
|
+
this.ctx.fillStyle = gradV;
|
|
139
|
+
this.ctx.fillRect(0, 0, width, height);
|
|
140
|
+
}
|
|
141
|
+
handleFieldInteraction(e) {
|
|
142
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
143
|
+
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
144
|
+
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
145
|
+
this.saturation = (x / rect.width) * 100;
|
|
146
|
+
this.brightness = 100 - (y / rect.height) * 100;
|
|
147
|
+
this.updateCursor();
|
|
148
|
+
this.updateFromHSB();
|
|
149
|
+
}
|
|
150
|
+
updateCursor() {
|
|
151
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
152
|
+
const x = (this.saturation / 100) * rect.width;
|
|
153
|
+
const y = (1 - this.brightness / 100) * rect.height;
|
|
154
|
+
this.cursor.style.left = `${x}px`;
|
|
155
|
+
this.cursor.style.top = `${y}px`;
|
|
156
|
+
this.cursor.classList.toggle('color-picker__cursor--dark', this.brightness > 50 && this.saturation < 80);
|
|
157
|
+
}
|
|
158
|
+
updateFromHSB() {
|
|
159
|
+
const rgb = this.hsbToRgb(this.hue, this.saturation, this.brightness);
|
|
160
|
+
const hex = this.rgbToHex(rgb);
|
|
161
|
+
this.syncInputs(rgb, hex);
|
|
162
|
+
this.syncPreview(rgb);
|
|
163
|
+
this.onChange?.(hex, rgb);
|
|
164
|
+
}
|
|
165
|
+
syncInputs(rgb, hex) {
|
|
166
|
+
this.hexInput.value = hex;
|
|
167
|
+
this.rInput.value = String(rgb.r);
|
|
168
|
+
this.gInput.value = String(rgb.g);
|
|
169
|
+
this.bInput.value = String(rgb.b);
|
|
170
|
+
}
|
|
171
|
+
syncPreview(rgb) {
|
|
172
|
+
this.preview.style.backgroundColor = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
|
|
173
|
+
}
|
|
174
|
+
handleHexInput() {
|
|
175
|
+
const hex = this.hexInput.value.trim();
|
|
176
|
+
if (/^#[0-9a-f]{6}$/i.test(hex)) {
|
|
177
|
+
this.setFromRGB(this.hexToRgb(hex));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
this.hexInput.value = this.rgbToHex(this.hsbToRgb(this.hue, this.saturation, this.brightness));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
handleRGBInput() {
|
|
184
|
+
const clamp = (v) => Math.max(0, Math.min(255, isNaN(v) ? 0 : v));
|
|
185
|
+
this.setFromRGB({
|
|
186
|
+
r: clamp(+this.rInput.value),
|
|
187
|
+
g: clamp(+this.gInput.value),
|
|
188
|
+
b: clamp(+this.bInput.value),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
setFromRGB(rgb) {
|
|
192
|
+
const { h, s, v } = this.rgbToHsb(rgb);
|
|
193
|
+
this.hue = h;
|
|
194
|
+
this.saturation = s;
|
|
195
|
+
this.brightness = v;
|
|
196
|
+
this.hueSlider.value = String(this.hue);
|
|
197
|
+
this.drawField();
|
|
198
|
+
this.updateCursor();
|
|
199
|
+
this.updateFromHSB();
|
|
200
|
+
}
|
|
201
|
+
getValue() {
|
|
202
|
+
return this.rgbToHex(this.hsbToRgb(this.hue, this.saturation, this.brightness));
|
|
203
|
+
}
|
|
204
|
+
setValue(hex) {
|
|
205
|
+
if (!/^#[0-9a-f]{6}$/i.test(hex))
|
|
206
|
+
return;
|
|
207
|
+
this.setFromRGB(this.hexToRgb(hex));
|
|
208
|
+
}
|
|
209
|
+
destroy() {
|
|
210
|
+
this.abortController.abort();
|
|
211
|
+
this.ro.disconnect();
|
|
212
|
+
this.container.classList.remove('color-picker');
|
|
213
|
+
this.container.innerHTML = '';
|
|
214
|
+
}
|
|
215
|
+
hsbToRgb(h, s, v) {
|
|
216
|
+
s /= 100;
|
|
217
|
+
v /= 100;
|
|
218
|
+
const c = v * s;
|
|
219
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
220
|
+
const m = v - c;
|
|
221
|
+
let r = 0, g = 0, b = 0;
|
|
222
|
+
if (h < 60) {
|
|
223
|
+
r = c;
|
|
224
|
+
g = x;
|
|
225
|
+
b = 0;
|
|
226
|
+
}
|
|
227
|
+
else if (h < 120) {
|
|
228
|
+
r = x;
|
|
229
|
+
g = c;
|
|
230
|
+
b = 0;
|
|
231
|
+
}
|
|
232
|
+
else if (h < 180) {
|
|
233
|
+
r = 0;
|
|
234
|
+
g = c;
|
|
235
|
+
b = x;
|
|
236
|
+
}
|
|
237
|
+
else if (h < 240) {
|
|
238
|
+
r = 0;
|
|
239
|
+
g = x;
|
|
240
|
+
b = c;
|
|
241
|
+
}
|
|
242
|
+
else if (h < 300) {
|
|
243
|
+
r = x;
|
|
244
|
+
g = 0;
|
|
245
|
+
b = c;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
r = c;
|
|
249
|
+
g = 0;
|
|
250
|
+
b = x;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
r: Math.round((r + m) * 255),
|
|
254
|
+
g: Math.round((g + m) * 255),
|
|
255
|
+
b: Math.round((b + m) * 255),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
rgbToHsb({ r, g, b }) {
|
|
259
|
+
r /= 255;
|
|
260
|
+
g /= 255;
|
|
261
|
+
b /= 255;
|
|
262
|
+
const max = Math.max(r, g, b);
|
|
263
|
+
const min = Math.min(r, g, b);
|
|
264
|
+
const d = max - min;
|
|
265
|
+
let h = 0;
|
|
266
|
+
if (d !== 0) {
|
|
267
|
+
if (max === r)
|
|
268
|
+
h = ((g - b) / d) % 6;
|
|
269
|
+
else if (max === g)
|
|
270
|
+
h = (b - r) / d + 2;
|
|
271
|
+
else
|
|
272
|
+
h = (r - g) / d + 4;
|
|
273
|
+
}
|
|
274
|
+
h = Math.round(h * 60);
|
|
275
|
+
if (h < 0)
|
|
276
|
+
h += 360;
|
|
277
|
+
return {
|
|
278
|
+
h,
|
|
279
|
+
s: max === 0 ? 0 : Math.round((d / max) * 100),
|
|
280
|
+
v: Math.round(max * 100),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
rgbToHex({ r, g, b }) {
|
|
284
|
+
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
|
|
285
|
+
}
|
|
286
|
+
hexToRgb(hex) {
|
|
287
|
+
const m = hex.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
|
288
|
+
return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export { ColorPicker };
|
package/js/context-menu.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Definition for a single context menu item including optional submenu. */
|
|
2
1
|
interface ContextMenuItemDef {
|
|
3
2
|
label: string;
|
|
4
3
|
icon?: string;
|
|
@@ -12,10 +11,8 @@ type ContextMenuInput = ContextMenuItemDef | 'separator' | {
|
|
|
12
11
|
group: string;
|
|
13
12
|
};
|
|
14
13
|
interface ContextMenuOptions {
|
|
15
|
-
/** Path to the SVG sprite file, e.g. `'svg-icons/icons.svg'`. Required to render icons. */
|
|
16
14
|
spritePath?: string;
|
|
17
15
|
}
|
|
18
|
-
/** Right-click context menu with keyboard navigation and nested submenu support. */
|
|
19
16
|
declare class ContextMenu {
|
|
20
17
|
private items;
|
|
21
18
|
private targets;
|
package/js/context-menu.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Right-click context menu with keyboard navigation and nested submenu support. */
|
|
2
1
|
class ContextMenu {
|
|
3
2
|
items;
|
|
4
3
|
targets;
|
|
@@ -148,8 +147,6 @@ class ContextMenu {
|
|
|
148
147
|
const rect = li.getBoundingClientRect();
|
|
149
148
|
return rect.right + submenuEl.offsetWidth > window.innerWidth;
|
|
150
149
|
};
|
|
151
|
-
// Delay timer prevents the submenu closing when mouse travels from
|
|
152
|
-
// item → submenu (mouseleave fires before mouseenter on the submenu)
|
|
153
150
|
let closeTimer = null;
|
|
154
151
|
const openSub = () => {
|
|
155
152
|
if (closeTimer) {
|
package/js/datepicker.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/** Localised day and month names for the DatePicker. */
|
|
2
1
|
interface DatePickerLocales {
|
|
3
2
|
days: string[];
|
|
4
3
|
months: string[];
|
|
5
4
|
}
|
|
6
|
-
/** Configuration options for the DatePicker. */
|
|
7
5
|
interface DatePickerOptions {
|
|
8
6
|
mode?: 'single' | 'range';
|
|
9
7
|
startDay?: number;
|
|
@@ -12,12 +10,10 @@ interface DatePickerOptions {
|
|
|
12
10
|
format?: (date: Date) => string;
|
|
13
11
|
onSelect?: (date: Date | DateRange) => void;
|
|
14
12
|
}
|
|
15
|
-
/** A date range with optional start and end dates. */
|
|
16
13
|
interface DateRange {
|
|
17
14
|
start: Date | null;
|
|
18
15
|
end: Date | null;
|
|
19
16
|
}
|
|
20
|
-
/** Calendar-based date (or date-range) picker that attaches to an input element. */
|
|
21
17
|
declare class DatePicker {
|
|
22
18
|
private input;
|
|
23
19
|
private options;
|
package/js/datepicker.js
CHANGED
package/js/docs-nav.js
CHANGED
|
@@ -16,6 +16,7 @@ const NAV = [
|
|
|
16
16
|
items: [
|
|
17
17
|
{ label: 'Inputs & Forms', href: 'forms/inputs.html' },
|
|
18
18
|
{ label: 'Date Picker', href: 'forms/datepicker.html' },
|
|
19
|
+
{ label: 'Color Picker', href: 'forms/color-picker.html' },
|
|
19
20
|
{ label: 'Range Slider', href: 'forms/range-slider.html' },
|
|
20
21
|
{ label: 'File Uploader', href: 'forms/file-uploader.html' },
|
|
21
22
|
],
|
package/js/dropdown.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
/** Configuration options for a Dropdown instance. */
|
|
2
1
|
interface DropdownOptions {
|
|
3
2
|
closeOnSelect?: boolean;
|
|
4
3
|
allowMultipleOpen?: boolean;
|
|
5
4
|
}
|
|
6
|
-
/** Event detail payload for the `dropdown-select` custom event. */
|
|
7
5
|
interface DropdownSelectDetail {
|
|
8
6
|
text: string;
|
|
9
7
|
element: HTMLElement;
|
|
10
8
|
}
|
|
11
|
-
/** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
|
|
12
9
|
declare class Dropdown {
|
|
13
10
|
private container;
|
|
14
11
|
private trigger;
|
|
@@ -28,4 +25,4 @@ declare class Dropdown {
|
|
|
28
25
|
private handleSelection;
|
|
29
26
|
destroy(): void;
|
|
30
27
|
}
|
|
31
|
-
export { Dropdown, DropdownSelectDetail };
|
|
28
|
+
export { Dropdown, type DropdownSelectDetail };
|
package/js/dropdown.js
CHANGED
package/js/editor.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
interface EditorOptions {
|
|
2
|
-
/** Hides the entire side panel (code/preview) permanently. Safe to use
|
|
3
|
-
* without #code, #preview, or #sidePanel in the DOM. */
|
|
4
2
|
simple?: boolean;
|
|
3
|
+
root?: string | HTMLElement;
|
|
5
4
|
}
|
|
6
|
-
/** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
|
|
7
5
|
declare class Editor {
|
|
6
|
+
private readonly root;
|
|
8
7
|
private readonly editable;
|
|
9
8
|
private readonly code;
|
|
10
9
|
private readonly preview;
|
|
@@ -14,6 +13,8 @@ declare class Editor {
|
|
|
14
13
|
private redoStack;
|
|
15
14
|
private abortController;
|
|
16
15
|
constructor(options?: EditorOptions);
|
|
16
|
+
private q;
|
|
17
|
+
private qAll;
|
|
17
18
|
private bindToolbar;
|
|
18
19
|
private bindActions;
|
|
19
20
|
private bindKeyboard;
|
package/js/editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sanitizeHtml } from './utils.js';
|
|
2
|
-
/** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
|
|
3
2
|
class Editor {
|
|
3
|
+
root;
|
|
4
4
|
editable;
|
|
5
5
|
code;
|
|
6
6
|
preview;
|
|
@@ -10,24 +10,35 @@ class Editor {
|
|
|
10
10
|
redoStack = [];
|
|
11
11
|
abortController = new AbortController();
|
|
12
12
|
constructor(options = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw new Error('Editor: #editable element not found');
|
|
13
|
+
if (options.root instanceof HTMLElement) {
|
|
14
|
+
this.root = options.root;
|
|
16
15
|
}
|
|
16
|
+
else if (typeof options.root === 'string') {
|
|
17
|
+
const el = document.querySelector(options.root);
|
|
18
|
+
if (!el)
|
|
19
|
+
throw new Error(`Editor: root "${options.root}" not found`);
|
|
20
|
+
this.root = el;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
this.root = document.body;
|
|
24
|
+
}
|
|
25
|
+
const editable = this.q('[data-editor="editable"]');
|
|
26
|
+
if (!editable)
|
|
27
|
+
throw new Error('Editor: [data-editor="editable"] element not found');
|
|
17
28
|
this.editable = editable;
|
|
18
|
-
this.wordCount =
|
|
29
|
+
this.wordCount = this.q('[data-editor="wordcount"]');
|
|
19
30
|
if (options.simple) {
|
|
20
31
|
this.code = null;
|
|
21
32
|
this.preview = null;
|
|
22
|
-
this.sidePanel =
|
|
33
|
+
this.sidePanel = this.q('[data-editor="side-panel"]');
|
|
23
34
|
this.sidePanel?.classList.add('hidden');
|
|
24
35
|
}
|
|
25
36
|
else {
|
|
26
|
-
const code =
|
|
27
|
-
const preview =
|
|
28
|
-
const sidePanel =
|
|
37
|
+
const code = this.q('[data-editor="code"]');
|
|
38
|
+
const preview = this.q('[data-editor="preview"]');
|
|
39
|
+
const sidePanel = this.q('[data-editor="side-panel"]');
|
|
29
40
|
if (!code || !preview || !sidePanel) {
|
|
30
|
-
throw new Error('Editor:
|
|
41
|
+
throw new Error('Editor: [data-editor="code"], [data-editor="preview"] and [data-editor="side-panel"] are required unless simple: true');
|
|
31
42
|
}
|
|
32
43
|
this.code = code;
|
|
33
44
|
this.preview = preview;
|
|
@@ -42,9 +53,15 @@ class Editor {
|
|
|
42
53
|
this.syncViews();
|
|
43
54
|
this.saveState();
|
|
44
55
|
}
|
|
56
|
+
q(selector) {
|
|
57
|
+
return this.root.querySelector(selector);
|
|
58
|
+
}
|
|
59
|
+
qAll(selector) {
|
|
60
|
+
return this.root.querySelectorAll(selector);
|
|
61
|
+
}
|
|
45
62
|
bindToolbar() {
|
|
46
63
|
const sig = { signal: this.abortController.signal };
|
|
47
|
-
|
|
64
|
+
this.qAll('[data-cmd]').forEach(btn => {
|
|
48
65
|
btn.addEventListener('click', () => {
|
|
49
66
|
const cmd = btn.dataset.cmd;
|
|
50
67
|
const val = btn.dataset.value ?? null;
|
|
@@ -55,13 +72,13 @@ class Editor {
|
|
|
55
72
|
}
|
|
56
73
|
bindActions() {
|
|
57
74
|
const sig = { signal: this.abortController.signal };
|
|
58
|
-
|
|
75
|
+
this.q('[data-editor-action="link"]')?.addEventListener('click', () => {
|
|
59
76
|
const url = prompt('Enter URL:', 'https://');
|
|
60
77
|
if (url)
|
|
61
78
|
this.exec('createLink', url);
|
|
62
79
|
}, sig);
|
|
63
|
-
const imageFile =
|
|
64
|
-
|
|
80
|
+
const imageFile = this.q('[data-editor="image-file"]');
|
|
81
|
+
this.q('[data-editor-action="image"]')?.addEventListener('click', () => imageFile?.click(), sig);
|
|
65
82
|
imageFile?.addEventListener('change', () => {
|
|
66
83
|
const file = imageFile.files?.[0];
|
|
67
84
|
if (!file)
|
|
@@ -75,7 +92,7 @@ class Editor {
|
|
|
75
92
|
reader.readAsDataURL(file);
|
|
76
93
|
imageFile.value = '';
|
|
77
94
|
}, sig);
|
|
78
|
-
|
|
95
|
+
this.q('[data-editor-action="clean"]')?.addEventListener('click', () => {
|
|
79
96
|
const sel = window.getSelection();
|
|
80
97
|
if (!sel || sel.rangeCount === 0)
|
|
81
98
|
return;
|
|
@@ -85,35 +102,32 @@ class Editor {
|
|
|
85
102
|
range.insertNode(document.createTextNode(text));
|
|
86
103
|
this.onContentChange();
|
|
87
104
|
}, sig);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
this.q('[data-editor-action="undo"]')?.addEventListener('click', () => this.undo(), sig);
|
|
106
|
+
this.q('[data-editor-action="redo"]')?.addEventListener('click', () => this.redo(), sig);
|
|
107
|
+
this.q('[data-editor-action="toggle-code"]')?.addEventListener('click', () => {
|
|
91
108
|
this.sidePanel?.classList.toggle('hidden');
|
|
92
109
|
this.syncViews();
|
|
93
110
|
}, sig);
|
|
94
|
-
// Code action buttons — matched by position within .code-actions
|
|
95
111
|
if (this.code) {
|
|
96
112
|
const code = this.code;
|
|
97
|
-
|
|
98
|
-
codeActions[0]?.addEventListener('click', () => {
|
|
113
|
+
this.q('[data-editor-action="apply-code"]')?.addEventListener('click', () => {
|
|
99
114
|
this.editable.innerHTML = sanitizeHtml(code.value);
|
|
100
115
|
this.onContentChange();
|
|
101
116
|
}, sig);
|
|
102
|
-
|
|
117
|
+
this.q('[data-editor-action="sanitize-code"]')?.addEventListener('click', () => {
|
|
103
118
|
code.value = sanitizeHtml(code.value);
|
|
104
119
|
this.editable.innerHTML = code.value;
|
|
105
120
|
this.onContentChange();
|
|
106
121
|
}, sig);
|
|
107
|
-
|
|
122
|
+
this.q('[data-editor-action="minify-code"]')?.addEventListener('click', () => {
|
|
108
123
|
code.value = code.value
|
|
109
124
|
.replace(/\n/g, '')
|
|
110
125
|
.replace(/>\s+</g, '><')
|
|
111
126
|
.trim();
|
|
112
127
|
}, sig);
|
|
113
128
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
document.getElementById('clearBtn')?.addEventListener('click', () => {
|
|
129
|
+
this.q('[data-editor-action="save"]')?.addEventListener('click', () => this.downloadHTML(), sig);
|
|
130
|
+
this.q('[data-editor-action="clear"]')?.addEventListener('click', () => {
|
|
117
131
|
if (confirm('Clear all content?')) {
|
|
118
132
|
this.editable.innerHTML = '';
|
|
119
133
|
this.onContentChange();
|
|
@@ -121,8 +135,9 @@ class Editor {
|
|
|
121
135
|
}, sig);
|
|
122
136
|
}
|
|
123
137
|
bindKeyboard() {
|
|
124
|
-
const saveBtn = document.getElementById('saveBtn');
|
|
125
138
|
window.addEventListener('keydown', (e) => {
|
|
139
|
+
if (!this.root.contains(document.activeElement))
|
|
140
|
+
return;
|
|
126
141
|
const mod = e.ctrlKey || e.metaKey;
|
|
127
142
|
if (!mod)
|
|
128
143
|
return;
|
|
@@ -147,7 +162,7 @@ class Editor {
|
|
|
147
162
|
}
|
|
148
163
|
else if (key === 's') {
|
|
149
164
|
e.preventDefault();
|
|
150
|
-
|
|
165
|
+
this.q('[data-editor-action="save"]')?.click();
|
|
151
166
|
}
|
|
152
167
|
else if (key === 'z' && !e.shiftKey) {
|
|
153
168
|
e.preventDefault();
|
|
@@ -172,13 +187,13 @@ class Editor {
|
|
|
172
187
|
}
|
|
173
188
|
bindTabs() {
|
|
174
189
|
const sig = { signal: this.abortController.signal };
|
|
175
|
-
|
|
190
|
+
this.qAll('.side-tab[data-tab]').forEach(tab => {
|
|
176
191
|
tab.addEventListener('click', () => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
const target = tab.dataset.tab;
|
|
193
|
+
this.qAll('.side-tab').forEach(t => t.classList.remove('active'));
|
|
194
|
+
this.qAll('.side-panel').forEach(p => p.classList.remove('active'));
|
|
180
195
|
tab.classList.add('active');
|
|
181
|
-
|
|
196
|
+
this.q(`[data-editor="${target}"]`)?.classList.add('active');
|
|
182
197
|
}, sig);
|
|
183
198
|
});
|
|
184
199
|
}
|
|
@@ -365,7 +380,7 @@ class Editor {
|
|
|
365
380
|
const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
|
|
366
381
|
for (const line of lines) {
|
|
367
382
|
const li = document.createElement('li');
|
|
368
|
-
li.textContent = line.trim() || '
|
|
383
|
+
li.textContent = line.trim() || '';
|
|
369
384
|
list.appendChild(li);
|
|
370
385
|
}
|
|
371
386
|
range.deleteContents();
|
|
@@ -439,7 +454,7 @@ ${content}
|
|
|
439
454
|
const element = container.nodeType === Node.TEXT_NODE
|
|
440
455
|
? container.parentElement
|
|
441
456
|
: container;
|
|
442
|
-
|
|
457
|
+
this.qAll('[data-cmd]').forEach(btn => {
|
|
443
458
|
const cmd = btn.dataset.cmd;
|
|
444
459
|
let active = false;
|
|
445
460
|
let current = element;
|
package/js/file-uploader.d.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
/** Event detail payload for the `upload-completed` custom event. */
|
|
2
1
|
interface UploadCompletedDetail {
|
|
3
2
|
fileCount: number;
|
|
4
3
|
files: File[];
|
|
5
4
|
results: PromiseSettledResult<unknown>[];
|
|
6
5
|
}
|
|
7
|
-
/** Event detail payload for the `file-validation-error` custom event. */
|
|
8
6
|
interface FileValidationErrorDetail {
|
|
9
7
|
file: File;
|
|
10
8
|
reason: 'size' | 'type';
|
|
11
9
|
}
|
|
12
|
-
/** Configuration options for the FileUploader. */
|
|
13
10
|
interface FileUploaderConfig {
|
|
14
11
|
uploadUrl?: string;
|
|
15
12
|
maxFileSize?: number;
|
|
16
13
|
allowedTypes?: string[];
|
|
17
14
|
}
|
|
18
|
-
/** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
|
|
19
15
|
declare class FileUploader {
|
|
20
16
|
private container;
|
|
21
17
|
private dropZone;
|
package/js/file-uploader.js
CHANGED
package/js/flyout-menu.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Configuration options for the FlyoutMenu. */
|
|
2
1
|
interface FlyoutMenuOptions {
|
|
3
2
|
triggerSelector?: string;
|
|
4
3
|
menuSelector?: string;
|
|
@@ -12,7 +11,6 @@ interface FlyoutMenuOptions {
|
|
|
12
11
|
enableHeader?: boolean;
|
|
13
12
|
enableFooter?: boolean;
|
|
14
13
|
}
|
|
15
|
-
/** Off-canvas flyout navigation with nested submenu support. */
|
|
16
14
|
declare class FlyoutMenu {
|
|
17
15
|
private options;
|
|
18
16
|
private menuTrigger;
|
package/js/flyout-menu.js
CHANGED