@dodlhuat/basix 1.2.8 → 1.2.9

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/js/dropdown.ts DELETED
@@ -1,180 +0,0 @@
1
- interface DropdownOptions {
2
- closeOnSelect?: boolean;
3
- allowMultipleOpen?: boolean;
4
- }
5
-
6
- interface DropdownSelectDetail {
7
- text: string;
8
- element: HTMLElement;
9
- }
10
-
11
- class Dropdown {
12
- private container: HTMLElement;
13
- private trigger: HTMLElement;
14
- private menu: HTMLElement;
15
- private options: Required<DropdownOptions>;
16
- private abortController: AbortController;
17
-
18
- constructor(selector: string, options: DropdownOptions = {}) {
19
- const container = document.querySelector<HTMLElement>(selector);
20
-
21
- if (!container) {
22
- console.error(`Dropdown container not found: ${selector}`);
23
- throw new Error(`Dropdown container "${selector}" not found`);
24
- }
25
-
26
- this.container = container;
27
-
28
- const trigger = this.container.querySelector<HTMLElement>('.dropdown-trigger');
29
- const menu = this.container.querySelector<HTMLElement>('.dropdown-menu');
30
-
31
- if (!trigger || !menu) {
32
- throw new Error('Dropdown requires .dropdown-trigger and .dropdown-menu elements');
33
- }
34
-
35
- this.trigger = trigger;
36
- this.menu = menu;
37
-
38
- this.options = {
39
- closeOnSelect: options.closeOnSelect ?? true,
40
- allowMultipleOpen: options.allowMultipleOpen ?? false,
41
- };
42
-
43
- this.abortController = new AbortController();
44
- this.init();
45
- }
46
-
47
- private init(): void {
48
- this.setupItems();
49
- this.attachEventListeners();
50
- }
51
-
52
- private attachEventListeners(): void {
53
- const { signal } = this.abortController;
54
-
55
- // Toggle main dropdown
56
- this.trigger.addEventListener(
57
- 'click',
58
- (e: MouseEvent) => {
59
- e.stopPropagation();
60
- this.toggle();
61
- },
62
- { signal }
63
- );
64
-
65
- // Close when clicking outside
66
- document.addEventListener(
67
- 'click',
68
- (e: MouseEvent) => {
69
- if (!this.container.contains(e.target as Node)) {
70
- this.close();
71
- }
72
- },
73
- { signal }
74
- );
75
-
76
- // Handle item clicks using event delegation
77
- this.menu.addEventListener(
78
- 'click',
79
- (e: MouseEvent) => {
80
- e.stopPropagation();
81
-
82
- const target = e.target as HTMLElement;
83
- const item = target.closest<HTMLElement>('.dropdown-item');
84
-
85
- if (!item) return;
86
-
87
- const li = item.parentElement as HTMLLIElement;
88
- const submenu = li.querySelector<HTMLUListElement>('ul');
89
-
90
- if (submenu) {
91
- this.toggleSubmenu(li);
92
- } else {
93
- this.handleSelection(item);
94
- if (this.options.closeOnSelect) {
95
- this.close();
96
- }
97
- }
98
- },
99
- { signal }
100
- );
101
- }
102
-
103
- private setupItems(): void {
104
- const items = this.menu.querySelectorAll<HTMLElement>('.dropdown-item');
105
-
106
- items.forEach((item) => {
107
- const li = item.parentElement as HTMLLIElement;
108
- if (li.querySelector('ul')) {
109
- item.classList.add('has-children');
110
- }
111
- });
112
- }
113
-
114
- public toggle(): void {
115
- this.container.classList.toggle('active');
116
- }
117
-
118
- public close(): void {
119
- this.container.classList.remove('active');
120
- this.closeAllSubmenus();
121
- }
122
-
123
- public open(): void {
124
- this.container.classList.add('active');
125
- }
126
-
127
- private toggleSubmenu(li: HTMLLIElement): void {
128
- const isOpening = !li.classList.contains('open');
129
-
130
- // Close siblings if not allowing multiple open menus
131
- if (isOpening && !this.options.allowMultipleOpen) {
132
- const parent = li.parentElement;
133
- if (parent) {
134
- const siblings = Array.from(parent.children) as HTMLLIElement[];
135
-
136
- siblings.forEach((sibling) => {
137
- if (sibling !== li && sibling.classList.contains('open')) {
138
- sibling.classList.remove('open');
139
-
140
- // Close deeply nested open items
141
- const deepOpenItems = sibling.querySelectorAll<HTMLLIElement>('.open');
142
- deepOpenItems.forEach((el) => el.classList.remove('open'));
143
- }
144
- });
145
- }
146
- }
147
-
148
- li.classList.toggle('open');
149
- }
150
-
151
- private closeAllSubmenus(): void {
152
- const openItems = this.menu.querySelectorAll<HTMLLIElement>('li.open');
153
- openItems.forEach((item) => item.classList.remove('open'));
154
- }
155
-
156
- private handleSelection(item: HTMLElement): void {
157
- const text = item.textContent?.trim() ?? '';
158
-
159
- // Dispatch custom event with proper typing
160
- const event = new CustomEvent<DropdownSelectDetail>('dropdown-select', {
161
- detail: {
162
- text,
163
- element: item,
164
- },
165
- bubbles: true,
166
- });
167
-
168
- this.container.dispatchEvent(event);
169
- }
170
-
171
- /**
172
- * Cleanup method to remove event listeners
173
- */
174
- public destroy(): void {
175
- this.abortController.abort();
176
- this.close();
177
- }
178
- }
179
-
180
- export { Dropdown, DropdownSelectDetail };
package/js/editor.ts DELETED
@@ -1,492 +0,0 @@
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
- simple?: boolean;
5
- }
6
-
7
- class Editor {
8
- private readonly editable: HTMLElement;
9
- private readonly code: HTMLTextAreaElement | null;
10
- private readonly preview: HTMLElement | null;
11
- private readonly sidePanel: HTMLElement | null;
12
- private readonly wordCount: HTMLElement | null;
13
- private undoStack: string[] = [];
14
- private redoStack: string[] = [];
15
- private abortController = new AbortController();
16
-
17
- constructor(options: EditorOptions = {}) {
18
- const editable = document.getElementById('editable');
19
-
20
- if (!editable) {
21
- throw new Error('Editor: #editable element not found');
22
- }
23
-
24
- this.editable = editable;
25
- this.wordCount = document.getElementById('wordCount');
26
-
27
- if (options.simple) {
28
- this.code = null;
29
- this.preview = null;
30
- this.sidePanel = document.getElementById('sidePanel');
31
- this.sidePanel?.classList.add('hidden');
32
- } else {
33
- const code = document.getElementById('code') as HTMLTextAreaElement;
34
- const preview = document.getElementById('preview');
35
- const sidePanel = document.getElementById('sidePanel');
36
-
37
- if (!code || !preview || !sidePanel) {
38
- throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
39
- }
40
-
41
- this.code = code;
42
- this.preview = preview;
43
- this.sidePanel = sidePanel;
44
- this.sidePanel.classList.add('hidden');
45
- }
46
-
47
- this.bindToolbar();
48
- this.bindActions();
49
- this.bindKeyboard();
50
- this.bindEditable();
51
- this.bindTabs();
52
- this.syncViews();
53
- this.saveState();
54
- }
55
-
56
- private bindToolbar(): void {
57
- const sig = { signal: this.abortController.signal };
58
- document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
59
- btn.addEventListener('click', () => {
60
- const cmd = btn.dataset.cmd!;
61
- const val = btn.dataset.value ?? null;
62
- this.exec(cmd, val);
63
- this.editable.focus();
64
- }, sig);
65
- });
66
- }
67
-
68
- private bindActions(): void {
69
- const sig = { signal: this.abortController.signal };
70
- document.getElementById('linkBtn')?.addEventListener('click', () => {
71
- const url = prompt('Enter URL:', 'https://');
72
- if (url) this.exec('createLink', url);
73
- }, sig);
74
-
75
- const imageFile = document.getElementById('imageFile') as HTMLInputElement;
76
- document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click(), sig);
77
- imageFile?.addEventListener('change', () => {
78
- const file = imageFile.files?.[0];
79
- if (!file) return;
80
- const reader = new FileReader();
81
- reader.onload = () => {
82
- if (typeof reader.result === 'string') {
83
- this.insertImage(reader.result);
84
- }
85
- };
86
- reader.readAsDataURL(file);
87
- imageFile.value = '';
88
- }, sig);
89
-
90
- document.getElementById('cleanBtn')?.addEventListener('click', () => {
91
- const sel = window.getSelection();
92
- if (!sel || sel.rangeCount === 0) return;
93
- const range = sel.getRangeAt(0);
94
- const text = range.toString();
95
- range.deleteContents();
96
- range.insertNode(document.createTextNode(text));
97
- this.onContentChange();
98
- }, sig);
99
-
100
- document.getElementById('undoBtn')?.addEventListener('click', () => this.undo(), sig);
101
- document.getElementById('redoBtn')?.addEventListener('click', () => this.redo(), sig);
102
-
103
- document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
104
- this.sidePanel?.classList.toggle('hidden');
105
- this.syncViews();
106
- }, sig);
107
-
108
- // Code action buttons — matched by position within .code-actions
109
- if (this.code) {
110
- const code = this.code;
111
- const codeActions = document.querySelectorAll<HTMLButtonElement>('.code-actions button');
112
- codeActions[0]?.addEventListener('click', () => {
113
- this.editable.innerHTML = this.sanitizeHTML(code.value);
114
- this.onContentChange();
115
- }, sig);
116
- codeActions[1]?.addEventListener('click', () => {
117
- code.value = this.sanitizeHTML(code.value);
118
- this.editable.innerHTML = code.value;
119
- this.onContentChange();
120
- }, sig);
121
- codeActions[2]?.addEventListener('click', () => {
122
- code.value = code.value
123
- .replace(/\n/g, '')
124
- .replace(/>\s+</g, '><')
125
- .trim();
126
- }, sig);
127
- }
128
-
129
- const saveBtn = document.getElementById('saveBtn');
130
- saveBtn?.addEventListener('click', () => this.downloadHTML(), sig);
131
-
132
- document.getElementById('clearBtn')?.addEventListener('click', () => {
133
- if (confirm('Clear all content?')) {
134
- this.editable.innerHTML = '';
135
- this.onContentChange();
136
- }
137
- }, sig);
138
- }
139
-
140
- private bindKeyboard(): void {
141
- const saveBtn = document.getElementById('saveBtn');
142
-
143
- window.addEventListener('keydown', (e: KeyboardEvent) => {
144
- const mod = e.ctrlKey || e.metaKey;
145
- if (!mod) return;
146
-
147
- const key = e.key.toLowerCase();
148
-
149
- if (key === 'b') { e.preventDefault(); this.exec('bold'); }
150
- else if (key === 'i') { e.preventDefault(); this.exec('italic'); }
151
- else if (key === 'u') { e.preventDefault(); this.exec('underline'); }
152
- else if (key === 'k') {
153
- e.preventDefault();
154
- const url = prompt('Enter URL:', 'https://');
155
- if (url) this.exec('createLink', url);
156
- }
157
- else if (key === 's') { e.preventDefault(); saveBtn?.click(); }
158
- else if (key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); }
159
- else if (key === 'y' || (key === 'z' && e.shiftKey)) { e.preventDefault(); this.redo(); }
160
- }, { signal: this.abortController.signal });
161
- }
162
-
163
- private bindEditable(): void {
164
- const sig = { signal: this.abortController.signal };
165
- this.editable.addEventListener('input', () => this.onContentChange(), sig);
166
-
167
- this.editable.addEventListener('paste', (e: ClipboardEvent) => {
168
- e.preventDefault();
169
- const text = e.clipboardData?.getData('text/plain') ?? '';
170
- this.insertText(text);
171
- }, sig);
172
-
173
- this.editable.addEventListener('keyup', () => this.refreshActiveState(), sig);
174
- this.editable.addEventListener('mouseup', () => this.refreshActiveState(), sig);
175
- }
176
-
177
- private bindTabs(): void {
178
- const sig = { signal: this.abortController.signal };
179
- document.querySelectorAll<HTMLElement>('.side-tab[data-tab]').forEach(tab => {
180
- tab.addEventListener('click', () => {
181
- const targetId = tab.dataset.tab!;
182
-
183
- document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
184
- document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
185
-
186
- tab.classList.add('active');
187
- document.getElementById(targetId)?.classList.add('active');
188
- }, sig);
189
- });
190
- }
191
-
192
- private onContentChange(): void {
193
- this.saveState();
194
- this.syncViews();
195
- }
196
-
197
- private syncViews(): void {
198
- if (this.code) this.code.value = this.editable.innerHTML.trim();
199
- if (this.preview) this.preview.innerHTML = this.sanitizeHTML(this.editable.innerHTML);
200
- this.updateWordCount();
201
- }
202
-
203
- private updateWordCount(): void {
204
- if (!this.wordCount) return;
205
- const text = this.editable.innerText || '';
206
- const words = text.trim().split(/\s+/).filter(w => w.length > 0);
207
- const count = words.length;
208
- this.wordCount.textContent = `${count} word${count !== 1 ? 's' : ''}`;
209
- }
210
-
211
- private saveState(): void {
212
- this.undoStack.push(this.editable.innerHTML);
213
- if (this.undoStack.length > 100) this.undoStack.shift();
214
- this.redoStack = [];
215
- }
216
-
217
- private undo(): void {
218
- if (this.undoStack.length <= 1) return;
219
- this.redoStack.push(this.undoStack.pop()!);
220
- this.editable.innerHTML = this.undoStack[this.undoStack.length - 1];
221
- this.syncViews();
222
- }
223
-
224
- private redo(): void {
225
- if (this.redoStack.length === 0) return;
226
- const state = this.redoStack.pop()!;
227
- this.undoStack.push(state);
228
- this.editable.innerHTML = state;
229
- this.syncViews();
230
- }
231
-
232
- private exec(command: string, value: string | null = null): void {
233
- switch (command) {
234
- case 'bold': this.toggleInlineStyle('strong'); break;
235
- case 'italic': this.toggleInlineStyle('em'); break;
236
- case 'underline': this.toggleInlineStyle('u'); break;
237
- case 'strikeThrough': this.toggleInlineStyle('s'); break;
238
- case 'createLink': if (value) this.createLink(value); break;
239
- case 'formatBlock': if (value) this.formatBlock(value); break;
240
- case 'insertUnorderedList': this.insertList('ul'); break;
241
- case 'insertOrderedList': this.insertList('ol'); break;
242
- case 'justifyLeft':
243
- case 'justifyCenter':
244
- case 'justifyRight': this.setAlignment(command); break;
245
- case 'foreColor': if (value) this.setForeColor(value); break;
246
- }
247
- }
248
-
249
- private insertText(text: string): void {
250
- const sel = window.getSelection();
251
- if (!sel || sel.rangeCount === 0) return;
252
-
253
- const range = sel.getRangeAt(0);
254
- range.deleteContents();
255
- range.insertNode(document.createTextNode(text));
256
- range.collapse(false);
257
- sel.removeAllRanges();
258
- sel.addRange(range);
259
-
260
- this.onContentChange();
261
- }
262
-
263
- private insertImage(dataUrl: string): void {
264
- const sel = window.getSelection();
265
- if (!sel || sel.rangeCount === 0) return;
266
-
267
- const range = sel.getRangeAt(0);
268
- const img = document.createElement('img');
269
- img.src = dataUrl;
270
- img.style.maxWidth = '100%';
271
- range.deleteContents();
272
- range.insertNode(img);
273
-
274
- range.setStartAfter(img);
275
- range.collapse(true);
276
- sel.removeAllRanges();
277
- sel.addRange(range);
278
-
279
- this.onContentChange();
280
- }
281
-
282
- private toggleInlineStyle(tagName: string): void {
283
- const sel = window.getSelection();
284
- if (!sel || sel.rangeCount === 0) return;
285
-
286
- const range = sel.getRangeAt(0);
287
- const container = range.commonAncestorContainer;
288
- let current: HTMLElement | null = container.nodeType === Node.TEXT_NODE
289
- ? container.parentElement
290
- : container as HTMLElement;
291
-
292
- let wrapper: HTMLElement | null = null;
293
- while (current && current !== this.editable) {
294
- if (current.tagName === tagName.toUpperCase()) {
295
- wrapper = current;
296
- break;
297
- }
298
- current = current.parentElement;
299
- }
300
-
301
- if (wrapper) {
302
- const parent = wrapper.parentNode;
303
- while (wrapper.firstChild) {
304
- parent?.insertBefore(wrapper.firstChild, wrapper);
305
- }
306
- parent?.removeChild(wrapper);
307
- } else {
308
- const contents = range.extractContents();
309
- const el = document.createElement(tagName);
310
- el.appendChild(contents);
311
- range.insertNode(el);
312
- range.selectNodeContents(el);
313
- sel.removeAllRanges();
314
- sel.addRange(range);
315
- }
316
-
317
- this.onContentChange();
318
- }
319
-
320
- private createLink(url: string): void {
321
- const sel = window.getSelection();
322
- if (!sel || sel.rangeCount === 0) return;
323
-
324
- const range = sel.getRangeAt(0);
325
- const contents = range.extractContents();
326
- const link = document.createElement('a');
327
- link.href = url;
328
- link.appendChild(contents);
329
- range.insertNode(link);
330
-
331
- this.onContentChange();
332
- }
333
-
334
- private formatBlock(tag: string): void {
335
- const sel = window.getSelection();
336
- if (!sel || sel.rangeCount === 0) return;
337
-
338
- const range = sel.getRangeAt(0);
339
- const container = range.commonAncestorContainer;
340
- let blockElement: HTMLElement | null = container.nodeType === Node.TEXT_NODE
341
- ? container.parentElement
342
- : container as HTMLElement;
343
-
344
- while (blockElement && blockElement !== this.editable && blockElement.parentElement !== this.editable) {
345
- blockElement = blockElement.parentElement;
346
- }
347
-
348
- if (blockElement && blockElement !== this.editable) {
349
- const newBlock = document.createElement(tag);
350
- newBlock.innerHTML = blockElement.innerHTML;
351
- blockElement.parentNode?.replaceChild(newBlock, blockElement);
352
- this.onContentChange();
353
- }
354
- }
355
-
356
- private insertList(listTag: string): void {
357
- const sel = window.getSelection();
358
- if (!sel || sel.rangeCount === 0) return;
359
-
360
- const range = sel.getRangeAt(0);
361
- const text = range.toString();
362
-
363
- const list = document.createElement(listTag);
364
- const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
365
-
366
- for (const line of lines) {
367
- const li = document.createElement('li');
368
- li.textContent = line.trim() || '\u200B';
369
- list.appendChild(li);
370
- }
371
-
372
- range.deleteContents();
373
- range.insertNode(list);
374
-
375
- const lastLi = list.lastElementChild;
376
- if (lastLi) {
377
- range.setStart(lastLi, lastLi.childNodes.length);
378
- range.collapse(true);
379
- sel.removeAllRanges();
380
- sel.addRange(range);
381
- }
382
-
383
- this.onContentChange();
384
- }
385
-
386
- private setAlignment(cmd: string): void {
387
- const align: Record<string, string> = {
388
- justifyLeft: 'left', justifyCenter: 'center', justifyRight: 'right',
389
- };
390
- const sel = window.getSelection();
391
- if (!sel || sel.rangeCount === 0) return;
392
- const container = sel.getRangeAt(0).commonAncestorContainer;
393
- let block: HTMLElement | null = container.nodeType === Node.TEXT_NODE
394
- ? container.parentElement
395
- : container as HTMLElement;
396
- while (block && block !== this.editable && block.parentElement !== this.editable) {
397
- block = block.parentElement;
398
- }
399
- if (block && block !== this.editable) {
400
- block.style.textAlign = align[cmd];
401
- this.onContentChange();
402
- }
403
- }
404
-
405
- private setForeColor(color: string): void {
406
- const sel = window.getSelection();
407
- if (!sel || sel.rangeCount === 0) return;
408
- const range = sel.getRangeAt(0);
409
- if (range.collapsed) return;
410
- const span = document.createElement('span');
411
- span.style.color = color;
412
- span.appendChild(range.extractContents());
413
- range.insertNode(span);
414
- range.selectNodeContents(span);
415
- sel.removeAllRanges();
416
- sel.addRange(range);
417
- this.onContentChange();
418
- }
419
-
420
- private sanitizeHTML(html: string): string {
421
- const parser = new DOMParser();
422
- const doc = parser.parseFromString(html, 'text/html');
423
-
424
- doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
425
-
426
- doc.querySelectorAll('*').forEach(el => {
427
- for (const attr of Array.from(el.attributes)) {
428
- if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
429
- el.removeAttribute(attr.name);
430
- }
431
- }
432
- });
433
-
434
- return doc.body.innerHTML;
435
- }
436
-
437
- private downloadHTML(): void {
438
- const content = this.sanitizeHTML(this.editable.innerHTML);
439
- const html = `<!doctype html>
440
- <html lang="en">
441
- <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
442
- <body>
443
- ${content}
444
- </body>
445
- </html>`;
446
- const blob = new Blob([html], { type: 'text/html' });
447
- const a = document.createElement('a');
448
- a.href = URL.createObjectURL(blob);
449
- a.download = 'document.html';
450
- a.click();
451
- URL.revokeObjectURL(a.href);
452
- }
453
-
454
- private refreshActiveState(): void {
455
- const sel = window.getSelection();
456
- if (!sel || sel.rangeCount === 0) return;
457
-
458
- const range = sel.getRangeAt(0);
459
- const container = range.commonAncestorContainer;
460
- const element = container.nodeType === Node.TEXT_NODE
461
- ? container.parentElement
462
- : container as HTMLElement;
463
-
464
- document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
465
- const cmd = btn.dataset.cmd;
466
- let active = false;
467
-
468
- let current: HTMLElement | null = element;
469
- while (current && current !== this.editable) {
470
- const tag = current.tagName?.toLowerCase();
471
- if (
472
- (cmd === 'bold' && (tag === 'strong' || tag === 'b')) ||
473
- (cmd === 'italic' && (tag === 'em' || tag === 'i')) ||
474
- (cmd === 'underline' && tag === 'u') ||
475
- (cmd === 'strikeThrough' && tag === 's')
476
- ) {
477
- active = true;
478
- break;
479
- }
480
- current = current.parentElement;
481
- }
482
-
483
- btn.classList.toggle('active', active);
484
- });
485
- }
486
-
487
- public destroy(): void {
488
- this.abortController.abort();
489
- }
490
- }
491
-
492
- export { Editor };