editium 1.0.1

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.
@@ -0,0 +1,2453 @@
1
+ class Editium {
2
+ constructor(options = {}) {
3
+ this.container = options.container;
4
+ this.placeholder = options.placeholder || 'Start typing...';
5
+ this.toolbar = options.toolbar || ['bold', 'italic', 'underline', 'heading-one', 'heading-two', 'bulleted-list', 'numbered-list', 'link'];
6
+ this.onChange = options.onChange || (() => {});
7
+ this.readOnly = options.readOnly || false;
8
+ this.showWordCount = options.showWordCount || false;
9
+ this.className = options.className || '';
10
+ this.onImageUpload = options.onImageUpload || null;
11
+
12
+ this.height = options.height || '200px';
13
+ this.minHeight = options.minHeight || '150px';
14
+ this.maxHeight = options.maxHeight || '250px';
15
+
16
+ this.isFullscreen = false;
17
+ this.searchQuery = '';
18
+ this.searchMatches = [];
19
+ this.currentMatchIndex = 0;
20
+ this.findReplacePanel = null;
21
+ this.history = [];
22
+ this.historyIndex = -1;
23
+ this.maxHistory = 50;
24
+ this.openDropdown = null;
25
+ this.linkPopup = null;
26
+ this.selectedLink = null;
27
+
28
+ if (!this.container) {
29
+ throw new Error('Container element is required');
30
+ }
31
+
32
+ this.init();
33
+ }
34
+
35
+ init() {
36
+ this.createEditor();
37
+ this.attachEventListeners();
38
+
39
+ if (this.editor.innerHTML.trim() === '') this.editor.innerHTML = '<p><br></p>';
40
+
41
+ this.makeExistingImagesResizable();
42
+ this.makeExistingLinksNonEditable();
43
+ this.saveState();
44
+ }
45
+
46
+ createEditor() {
47
+ this.container.innerHTML = '';
48
+
49
+ this.wrapper = document.createElement('div');
50
+ this.wrapper.className = `editium-wrapper ${this.className}`;
51
+ if (this.isFullscreen) this.wrapper.classList.add('editium-fullscreen');
52
+
53
+ const toolbarItems = this.toolbar === 'all' ? this.getAllToolbarItems() : this.toolbar;
54
+
55
+ if (toolbarItems.length > 0) {
56
+ this.toolbarElement = this.createToolbar(toolbarItems);
57
+ this.wrapper.appendChild(this.toolbarElement);
58
+ }
59
+
60
+ this.editorContainer = document.createElement('div');
61
+ this.editorContainer.className = 'editium-editor-container';
62
+
63
+ this.editor = document.createElement('div');
64
+ this.editor.className = 'editium-editor';
65
+ this.editor.contentEditable = !this.readOnly;
66
+ this.editor.setAttribute('data-placeholder', this.placeholder);
67
+
68
+ if (!this.isFullscreen) {
69
+ this.editor.style.height = typeof this.height === 'number' ? `${this.height}px` : this.height;
70
+ this.editor.style.minHeight = typeof this.minHeight === 'number' ? `${this.minHeight}px` : this.minHeight;
71
+ this.editor.style.maxHeight = typeof this.maxHeight === 'number' ? `${this.maxHeight}px` : this.maxHeight;
72
+ } else {
73
+ this.editor.style.height = 'auto';
74
+ this.editor.style.minHeight = 'auto';
75
+ this.editor.style.maxHeight = 'none';
76
+ }
77
+
78
+ this.editorContainer.appendChild(this.editor);
79
+ this.wrapper.appendChild(this.editorContainer);
80
+
81
+ this.wordCountElement = document.createElement('div');
82
+ this.wordCountElement.className = 'editium-word-count';
83
+ this.wrapper.appendChild(this.wordCountElement);
84
+ this.updateWordCount();
85
+
86
+ this.container.appendChild(this.wrapper);
87
+ }
88
+
89
+ getAllToolbarItems() {
90
+ return [
91
+ 'paragraph', 'heading-one', 'heading-two', 'heading-three', 'heading-four',
92
+ 'heading-five', 'heading-six',
93
+ 'separator',
94
+ 'bold', 'italic', 'underline', 'strikethrough',
95
+ 'separator',
96
+ 'superscript', 'subscript', 'code',
97
+ 'separator',
98
+ 'left', 'center', 'right', 'justify',
99
+ 'separator',
100
+ 'text-color', 'bg-color',
101
+ 'separator',
102
+ 'blockquote', 'code-block',
103
+ 'separator',
104
+ 'bulleted-list', 'numbered-list', 'indent', 'outdent',
105
+ 'separator',
106
+ 'link', 'image', 'table', 'horizontal-rule', 'undo', 'redo',
107
+ 'separator',
108
+ 'find-replace', 'fullscreen', 'view-output'
109
+ ];
110
+ }
111
+
112
+ createToolbar(items) {
113
+ const toolbar = document.createElement('div');
114
+ toolbar.className = 'editium-toolbar';
115
+
116
+ const groups = {
117
+ paragraph: ['paragraph', 'heading-one', 'heading-two', 'heading-three', 'heading-four', 'heading-five', 'heading-six'],
118
+ format: ['bold', 'italic', 'underline', 'strikethrough', 'code', 'superscript', 'subscript'],
119
+ align: ['left', 'center', 'right', 'justify'],
120
+ color: ['text-color', 'bg-color'],
121
+ blocks: ['blockquote', 'code-block'],
122
+ lists: ['bulleted-list', 'numbered-list', 'indent', 'outdent'],
123
+ insert: ['link', 'image', 'table', 'horizontal-rule'],
124
+ edit: ['undo', 'redo'],
125
+ view: ['preview', 'view-html', 'view-json']
126
+ };
127
+
128
+ if (this.toolbar === 'all') {
129
+ toolbar.appendChild(this.createBlockFormatDropdown());
130
+ toolbar.appendChild(this.createGroupDropdown('Format', groups.format));
131
+ toolbar.appendChild(this.createAlignmentDropdown());
132
+ toolbar.appendChild(this.createGroupDropdown('Color', groups.color));
133
+ toolbar.appendChild(this.createGroupDropdown('Blocks', groups.blocks));
134
+ toolbar.appendChild(this.createGroupDropdown('Lists', groups.lists));
135
+ toolbar.appendChild(this.createGroupDropdown('Insert', groups.insert));
136
+ toolbar.appendChild(this.createGroupDropdown('Edit', groups.edit));
137
+ toolbar.appendChild(this.createGroupDropdown('View', groups.view));
138
+
139
+ const spacer = document.createElement('div');
140
+ spacer.style.flex = '1';
141
+ toolbar.appendChild(spacer);
142
+
143
+ const findButton = this.createToolbarButton('find-replace');
144
+ const fullscreenButton = this.createToolbarButton('fullscreen');
145
+ if (findButton) toolbar.appendChild(findButton);
146
+ if (fullscreenButton) toolbar.appendChild(fullscreenButton);
147
+ } else {
148
+ const blockFormats = groups.paragraph;
149
+ const alignments = groups.align;
150
+ let processedGroups = { block: false, align: false };
151
+
152
+ for (let i = 0; i < items.length; i++) {
153
+ const item = items[i];
154
+
155
+ if (item === 'separator') {
156
+ if (i > 0 && items[i-1] !== 'separator') {
157
+ const separator = document.createElement('div');
158
+ separator.className = 'editium-toolbar-separator';
159
+ toolbar.appendChild(separator);
160
+ }
161
+ } else if (blockFormats.includes(item) && !processedGroups.block) {
162
+ toolbar.appendChild(this.createBlockFormatDropdown());
163
+ processedGroups.block = true;
164
+ } else if (alignments.includes(item) && !processedGroups.align) {
165
+ toolbar.appendChild(this.createAlignmentDropdown());
166
+ processedGroups.align = true;
167
+ } else if (!blockFormats.includes(item) && !alignments.includes(item)) {
168
+ const button = this.createToolbarButton(item);
169
+ if (button) {
170
+ toolbar.appendChild(button);
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ return toolbar;
177
+ }
178
+
179
+ createGroupDropdown(label, items) {
180
+ const dropdown = document.createElement('div');
181
+ dropdown.className = 'editium-dropdown';
182
+
183
+ const trigger = document.createElement('button');
184
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
185
+ trigger.type = 'button';
186
+ trigger.textContent = label;
187
+ trigger.title = label;
188
+
189
+ const menu = document.createElement('div');
190
+ menu.className = 'editium-dropdown-menu';
191
+
192
+ items.forEach(itemType => {
193
+ const config = this.getButtonConfig(itemType);
194
+ if (!config) return;
195
+
196
+ const item = document.createElement('button');
197
+ item.type = 'button';
198
+ item.innerHTML = `${config.icon} <span>${config.title}</span>`;
199
+ item.onclick = (e) => {
200
+ e.preventDefault();
201
+ config.action();
202
+ this.closeDropdown();
203
+ };
204
+ menu.appendChild(item);
205
+ });
206
+
207
+ trigger.onclick = (e) => {
208
+ e.preventDefault();
209
+ this.toggleDropdown(menu);
210
+ };
211
+
212
+ dropdown.appendChild(trigger);
213
+ dropdown.appendChild(menu);
214
+
215
+ return dropdown;
216
+ }
217
+
218
+ createBlockFormatDropdown() {
219
+ const dropdown = document.createElement('div');
220
+ dropdown.className = 'editium-dropdown';
221
+
222
+ const trigger = document.createElement('button');
223
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
224
+ trigger.type = 'button';
225
+ trigger.textContent = 'Paragraph';
226
+ trigger.title = 'Block Format';
227
+
228
+ const menu = document.createElement('div');
229
+ menu.className = 'editium-dropdown-menu';
230
+
231
+ const formats = [
232
+ { label: 'Paragraph', value: 'p' },
233
+ { label: 'Heading 1', value: 'h1' },
234
+ { label: 'Heading 2', value: 'h2' },
235
+ { label: 'Heading 3', value: 'h3' },
236
+ { label: 'Heading 4', value: 'h4' },
237
+ { label: 'Heading 5', value: 'h5' },
238
+ { label: 'Heading 6', value: 'h6' },
239
+ ];
240
+
241
+ formats.forEach(format => {
242
+ const item = document.createElement('button');
243
+ item.type = 'button';
244
+ item.textContent = format.label;
245
+ item.onclick = (e) => {
246
+ e.preventDefault();
247
+ this.execCommand('formatBlock', `<${format.value}>`);
248
+ trigger.textContent = format.label;
249
+ this.closeDropdown();
250
+ };
251
+ menu.appendChild(item);
252
+ });
253
+
254
+ trigger.onclick = (e) => {
255
+ e.preventDefault();
256
+ this.toggleDropdown(menu);
257
+ };
258
+
259
+ dropdown.appendChild(trigger);
260
+ dropdown.appendChild(menu);
261
+
262
+ return dropdown;
263
+ }
264
+
265
+ createAlignmentDropdown() {
266
+ const dropdown = document.createElement('div');
267
+ dropdown.className = 'editium-dropdown';
268
+
269
+ const trigger = document.createElement('button');
270
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
271
+ trigger.type = 'button';
272
+ trigger.textContent = 'Align';
273
+ trigger.title = 'Text Alignment';
274
+
275
+ const menu = document.createElement('div');
276
+ menu.className = 'editium-dropdown-menu';
277
+
278
+ const alignments = [
279
+ { label: 'Align Left', icon: '<i class="fa-solid fa-align-left"></i>', command: 'justifyLeft' },
280
+ { label: 'Align Center', icon: '<i class="fa-solid fa-align-center"></i>', command: 'justifyCenter' },
281
+ { label: 'Align Right', icon: '<i class="fa-solid fa-align-right"></i>', command: 'justifyRight' },
282
+ { label: 'Justify', icon: '<i class="fa-solid fa-align-justify"></i>', command: 'justifyFull' },
283
+ ];
284
+
285
+ alignments.forEach(align => {
286
+ const item = document.createElement('button');
287
+ item.type = 'button';
288
+ item.innerHTML = `${align.icon} <span>${align.label}</span>`;
289
+ item.onclick = (e) => {
290
+ e.preventDefault();
291
+ this.execCommand(align.command);
292
+ this.closeDropdown();
293
+ };
294
+ menu.appendChild(item);
295
+ });
296
+
297
+ trigger.onclick = (e) => {
298
+ e.preventDefault();
299
+ this.toggleDropdown(menu);
300
+ };
301
+
302
+ dropdown.appendChild(trigger);
303
+ dropdown.appendChild(menu);
304
+
305
+ return dropdown;
306
+ }
307
+
308
+ toggleDropdown(menu) {
309
+ if (this.openDropdown === menu) {
310
+ this.closeDropdown();
311
+ } else {
312
+ this.closeDropdown();
313
+ menu.classList.add('show');
314
+ this.openDropdown = menu;
315
+ }
316
+ }
317
+
318
+ closeDropdown() {
319
+ if (this.openDropdown) {
320
+ this.openDropdown.classList.remove('show');
321
+ this.openDropdown = null;
322
+ }
323
+ }
324
+
325
+ createToolbarButton(type) {
326
+ const config = this.getButtonConfig(type);
327
+ if (!config) return null;
328
+
329
+ if (config.dropdown) {
330
+ return this.createDropdownButton(type, config);
331
+ }
332
+
333
+ const button = document.createElement('button');
334
+ button.className = 'editium-toolbar-button';
335
+ button.type = 'button';
336
+ button.setAttribute('data-command', type);
337
+ button.innerHTML = config.icon;
338
+ button.title = config.title;
339
+
340
+ button.onclick = (e) => {
341
+ e.preventDefault();
342
+ config.action();
343
+ this.closeDropdown();
344
+ };
345
+
346
+ return button;
347
+ }
348
+
349
+ createDropdownButton(type, config) {
350
+ const dropdown = document.createElement('div');
351
+ dropdown.className = 'editium-dropdown';
352
+
353
+ const trigger = document.createElement('button');
354
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
355
+ trigger.type = 'button';
356
+ trigger.innerHTML = config.icon;
357
+ trigger.title = config.title;
358
+
359
+ const menu = document.createElement('div');
360
+ menu.className = 'editium-dropdown-menu';
361
+
362
+ config.dropdown.forEach(item => {
363
+ const menuItem = document.createElement('button');
364
+ menuItem.type = 'button';
365
+ menuItem.textContent = item.label;
366
+ menuItem.onclick = (e) => {
367
+ e.preventDefault();
368
+ item.action();
369
+ this.closeDropdown();
370
+ };
371
+ menu.appendChild(menuItem);
372
+ });
373
+
374
+ trigger.onclick = (e) => {
375
+ e.preventDefault();
376
+ this.toggleDropdown(menu);
377
+ };
378
+
379
+ dropdown.appendChild(trigger);
380
+ dropdown.appendChild(menu);
381
+
382
+ return dropdown;
383
+ }
384
+
385
+ getButtonConfig(type) {
386
+ const configs = {
387
+ 'bold': { icon: '<i class="fa-solid fa-bold"></i>', title: 'Bold (Ctrl+B)', action: () => this.execCommand('bold') },
388
+ 'italic': { icon: '<i class="fa-solid fa-italic"></i>', title: 'Italic (Ctrl+I)', action: () => this.execCommand('italic') },
389
+ 'underline': { icon: '<i class="fa-solid fa-underline"></i>', title: 'Underline (Ctrl+U)', action: () => this.execCommand('underline') },
390
+ 'strikethrough': { icon: '<i class="fa-solid fa-strikethrough"></i>', title: 'Strikethrough', action: () => this.execCommand('strikeThrough') },
391
+ 'superscript': { icon: '<i class="fa-solid fa-superscript"></i>', title: 'Superscript', action: () => this.execCommand('superscript') },
392
+ 'subscript': { icon: '<i class="fa-solid fa-subscript"></i>', title: 'Subscript', action: () => this.execCommand('subscript') },
393
+ 'code': { icon: '<i class="fa-solid fa-code"></i>', title: 'Code', action: () => this.toggleInlineCode() },
394
+ 'left': { icon: '<i class="fa-solid fa-align-left"></i>', title: 'Align Left', action: () => this.execCommand('justifyLeft') },
395
+ 'center': { icon: '<i class="fa-solid fa-align-center"></i>', title: 'Align Center', action: () => this.execCommand('justifyCenter') },
396
+ 'right': { icon: '<i class="fa-solid fa-align-right"></i>', title: 'Align Right', action: () => this.execCommand('justifyRight') },
397
+ 'justify': { icon: '<i class="fa-solid fa-align-justify"></i>', title: 'Justify', action: () => this.execCommand('justifyFull') },
398
+ 'bulleted-list': { icon: '<i class="fa-solid fa-list-ul"></i>', title: 'Bulleted List', action: () => this.execCommand('insertUnorderedList') },
399
+ 'numbered-list': { icon: '<i class="fa-solid fa-list-ol"></i>', title: 'Numbered List', action: () => this.execCommand('insertOrderedList') },
400
+ 'indent': { icon: '<i class="fa-solid fa-indent"></i>', title: 'Indent', action: () => this.execCommand('indent') },
401
+ 'outdent': { icon: '<i class="fa-solid fa-outdent"></i>', title: 'Outdent', action: () => this.execCommand('outdent') },
402
+ 'link': { icon: '<i class="fa-solid fa-link"></i>', title: 'Insert Link', action: () => this.showLinkModal() },
403
+ 'image': { icon: '<i class="fa-solid fa-image"></i>', title: 'Insert Image', action: () => this.showImageModal() },
404
+ 'blockquote': { icon: '<i class="fa-solid fa-quote-left"></i>', title: 'Blockquote', action: () => this.execCommand('formatBlock', '<blockquote>') },
405
+ 'code-block': { icon: '<i class="fa-solid fa-file-code"></i>', title: 'Code Block', action: () => this.insertCodeBlock() },
406
+ 'horizontal-rule': { icon: '<i class="fa-solid fa-minus"></i>', title: 'Horizontal Rule', action: () => this.execCommand('insertHorizontalRule') },
407
+ 'table': { icon: '<i class="fa-solid fa-table"></i>', title: 'Insert Table', action: () => this.showTableModal() },
408
+ 'text-color': { icon: '<i class="fa-solid fa-palette"></i>', title: 'Text Color', action: () => this.showColorPicker('foreColor') },
409
+ 'bg-color': { icon: '<i class="fa-solid fa-fill-drip"></i>', title: 'Background Color', action: () => this.showColorPicker('hiliteColor') },
410
+ 'undo': { icon: '<i class="fa-solid fa-rotate-left"></i>', title: 'Undo (Ctrl+Z)', action: () => this.undo() },
411
+ 'redo': { icon: '<i class="fa-solid fa-rotate-right"></i>', title: 'Redo (Ctrl+Y)', action: () => this.redo() },
412
+ 'preview': { icon: '<i class="fa-solid fa-eye"></i>', title: 'Preview', action: () => this.viewOutput('preview') },
413
+ 'view-html': { icon: '<i class="fa-solid fa-code"></i>', title: 'View HTML', action: () => this.viewOutput('html') },
414
+ 'view-json': { icon: '<i class="fa-solid fa-brackets-curly"></i>', title: 'View JSON', action: () => this.viewOutput('json') },
415
+ 'find-replace': { icon: '<i class="fa-solid fa-magnifying-glass"></i>', title: 'Find & Replace', action: () => this.toggleFindReplace() },
416
+ 'fullscreen': { icon: '<i class="fa-solid fa-expand"></i>', title: 'Toggle Fullscreen (F11)', action: () => this.toggleFullscreen() }
417
+ };
418
+
419
+ return configs[type];
420
+ }
421
+
422
+ execCommand(command, value = null) {
423
+ document.execCommand(command, false, value);
424
+ this.editor.focus();
425
+ this.saveState();
426
+ this.triggerChange();
427
+ }
428
+
429
+ toggleInlineCode() {
430
+ const selection = window.getSelection();
431
+ if (!selection.rangeCount) return;
432
+
433
+ const range = selection.getRangeAt(0);
434
+ const selectedText = range.toString();
435
+
436
+ if (selectedText) {
437
+ const code = document.createElement('code');
438
+ code.style.backgroundColor = '#f4f4f4';
439
+ code.style.padding = '2px 4px';
440
+ code.style.borderRadius = '3px';
441
+ code.style.fontFamily = 'monospace';
442
+ code.textContent = selectedText;
443
+
444
+ range.deleteContents();
445
+ range.insertNode(code);
446
+
447
+ this.saveState();
448
+ this.triggerChange();
449
+ }
450
+ }
451
+
452
+ showLinkModal() {
453
+ this.editor.focus();
454
+ const selection = window.getSelection();
455
+ const selectedText = selection.toString();
456
+ let savedRange = null;
457
+
458
+ if (selection.rangeCount > 0) savedRange = selection.getRangeAt(0).cloneRange();
459
+
460
+ const modal = this.createModal('Insert Link', `
461
+ <div style="margin-bottom: 16px;">
462
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Link Text:</label>
463
+ <input type="text" id="link-text" value="${this.escapeHtml(selectedText)}" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
464
+ </div>
465
+ <div style="margin-bottom: 16px;">
466
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">URL: *</label>
467
+ <input type="text" id="link-url" placeholder="https://example.com" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
468
+ </div>
469
+ <div style="margin-bottom: 16px;">
470
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Title (optional):</label>
471
+ <input type="text" id="link-title" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
472
+ </div>
473
+ <div style="margin-bottom: 16px;">
474
+ <label style="display: inline-flex; align-items: center; font-size: 14px; color: #333; cursor: pointer;">
475
+ <input type="checkbox" id="link-target" style="margin-right: 8px;"> Open in new tab
476
+ </label>
477
+ </div>
478
+ `, () => {
479
+ const url = document.getElementById('link-url').value.trim();
480
+ const text = document.getElementById('link-text').value.trim();
481
+ const title = document.getElementById('link-title').value.trim();
482
+ const target = document.getElementById('link-target').checked;
483
+
484
+ if (!url) {
485
+ alert('URL is required');
486
+ return false;
487
+ }
488
+
489
+ try {
490
+ new URL(url);
491
+ } catch {
492
+ alert('Please enter a valid URL');
493
+ return false;
494
+ }
495
+
496
+ if (savedRange) {
497
+ this.editor.focus();
498
+ const sel = window.getSelection();
499
+ sel.removeAllRanges();
500
+ sel.addRange(savedRange);
501
+ }
502
+
503
+ const link = document.createElement('a');
504
+ link.href = url;
505
+ link.textContent = text || url;
506
+ link.contentEditable = 'false';
507
+ if (title) link.title = title;
508
+ if (target) link.target = '_blank';
509
+
510
+ const sel = window.getSelection();
511
+ if (sel.rangeCount) {
512
+ const range = sel.getRangeAt(0);
513
+ range.deleteContents();
514
+ range.insertNode(link);
515
+
516
+ const space = document.createTextNode('\u00A0');
517
+ range.setStartAfter(link);
518
+ range.insertNode(space);
519
+
520
+ range.setStartAfter(space);
521
+ range.setEndAfter(space);
522
+ sel.removeAllRanges();
523
+ sel.addRange(range);
524
+ }
525
+
526
+ this.saveState();
527
+ this.triggerChange();
528
+ return true;
529
+ });
530
+
531
+ document.body.appendChild(modal);
532
+ document.getElementById('link-url').focus();
533
+ }
534
+
535
+ showLinkPopup(linkElement) {
536
+ this.selectedLink = linkElement;
537
+
538
+ this.closeLinkPopup();
539
+
540
+ const rect = linkElement.getBoundingClientRect();
541
+
542
+ this.linkPopup = document.createElement('div');
543
+ this.linkPopup.className = 'editium-link-popup';
544
+ this.linkPopup.style.cssText = `
545
+ position: fixed;
546
+ top: ${rect.bottom + window.scrollY + 5}px;
547
+ left: ${rect.left + window.scrollX}px;
548
+ background-color: #ffffff;
549
+ border: 1px solid #d1d5db;
550
+ border-radius: 8px;
551
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
552
+ min-width: 200px;
553
+ overflow: hidden;
554
+ z-index: 10000;
555
+ `;
556
+
557
+ this.linkPopup.innerHTML = `
558
+ <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; background-color: #f9fafb;">
559
+ <div style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">Link URL:</div>
560
+ <div style="font-size: 13px; color: #111827; word-break: break-all; font-family: monospace;">
561
+ ${this.escapeHtml(linkElement.href)}
562
+ </div>
563
+ </div>
564
+ <button class="editium-link-popup-btn editium-link-open" style="
565
+ width: 100%;
566
+ padding: 12px 16px;
567
+ border: none;
568
+ background-color: transparent;
569
+ color: #374151;
570
+ font-size: 14px;
571
+ text-align: left;
572
+ cursor: pointer;
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 10px;
576
+ font-weight: 500;
577
+ ">
578
+ <svg style="width: 16px; height: 16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
579
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
580
+ </svg>
581
+ Open Link
582
+ </button>
583
+ <button class="editium-link-popup-btn editium-link-edit" style="
584
+ width: 100%;
585
+ padding: 12px 16px;
586
+ border: none;
587
+ background-color: transparent;
588
+ color: #374151;
589
+ font-size: 14px;
590
+ text-align: left;
591
+ cursor: pointer;
592
+ display: flex;
593
+ align-items: center;
594
+ gap: 10px;
595
+ font-weight: 500;
596
+ ">
597
+ <svg style="width: 16px; height: 16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
598
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
599
+ </svg>
600
+ Edit Link
601
+ </button>
602
+ <button class="editium-link-popup-btn editium-link-remove" style="
603
+ width: 100%;
604
+ padding: 12px 16px;
605
+ border: none;
606
+ border-top: 1px solid #e5e7eb;
607
+ background-color: transparent;
608
+ color: #ef4444;
609
+ font-size: 14px;
610
+ text-align: left;
611
+ cursor: pointer;
612
+ display: flex;
613
+ align-items: center;
614
+ gap: 10px;
615
+ font-weight: 500;
616
+ ">
617
+ <svg style="width: 16px; height: 16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
618
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
619
+ </svg>
620
+ Remove Link
621
+ </button>
622
+ `;
623
+
624
+ const buttons = this.linkPopup.querySelectorAll('.editium-link-popup-btn');
625
+ buttons.forEach(btn => {
626
+ btn.addEventListener('mouseenter', () => {
627
+ if (btn.classList.contains('editium-link-remove')) {
628
+ btn.style.backgroundColor = '#fef2f2';
629
+ } else {
630
+ btn.style.backgroundColor = '#f3f4f6';
631
+ }
632
+ });
633
+ btn.addEventListener('mouseleave', () => {
634
+ btn.style.backgroundColor = 'transparent';
635
+ });
636
+ });
637
+
638
+ this.linkPopup.querySelector('.editium-link-open').addEventListener('click', () => {
639
+ window.open(linkElement.href, linkElement.target || '_self');
640
+ this.closeLinkPopup();
641
+ });
642
+
643
+ this.linkPopup.querySelector('.editium-link-edit').addEventListener('click', () => {
644
+ this.closeLinkPopup();
645
+ this.editLink(linkElement);
646
+ });
647
+
648
+ this.linkPopup.querySelector('.editium-link-remove').addEventListener('click', () => {
649
+ this.removeLink(linkElement);
650
+ this.closeLinkPopup();
651
+ });
652
+
653
+ document.body.appendChild(this.linkPopup);
654
+ }
655
+
656
+ closeLinkPopup() {
657
+ if (this.linkPopup) {
658
+ this.linkPopup.remove();
659
+ this.linkPopup = null;
660
+ }
661
+ this.selectedLink = null;
662
+ }
663
+
664
+ editLink(linkElement) {
665
+
666
+ const savedLinkElement = linkElement;
667
+
668
+ const modal = this.createModal('Edit Link', `
669
+ <div style="margin-bottom: 16px;">
670
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Link Text:</label>
671
+ <input type="text" id="link-text" value="${this.escapeHtml(linkElement.textContent)}" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
672
+ </div>
673
+ <div style="margin-bottom: 16px;">
674
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">URL: *</label>
675
+ <input type="text" id="link-url" value="${this.escapeHtml(linkElement.href)}" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
676
+ </div>
677
+ <div style="margin-bottom: 16px;">
678
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Title (optional):</label>
679
+ <input type="text" id="link-title" value="${this.escapeHtml(linkElement.title || '')}" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
680
+ </div>
681
+ <div style="margin-bottom: 16px;">
682
+ <label style="display: inline-flex; align-items: center; font-size: 14px; color: #333; cursor: pointer;">
683
+ <input type="checkbox" id="link-target" ${linkElement.target === '_blank' ? 'checked' : ''} style="margin-right: 8px;"> Open in new tab
684
+ </label>
685
+ </div>
686
+ `, () => {
687
+ const url = document.getElementById('link-url').value.trim();
688
+ const text = document.getElementById('link-text').value.trim();
689
+ const title = document.getElementById('link-title').value.trim();
690
+ const target = document.getElementById('link-target').checked;
691
+
692
+ if (!url) {
693
+ alert('URL is required');
694
+ return false;
695
+ }
696
+
697
+ try {
698
+ new URL(url);
699
+ } catch {
700
+ alert('Please enter a valid URL');
701
+ return false;
702
+ }
703
+
704
+ savedLinkElement.href = url;
705
+ savedLinkElement.textContent = text || url;
706
+ savedLinkElement.title = title;
707
+ savedLinkElement.target = target ? '_blank' : '';
708
+ savedLinkElement.contentEditable = 'false';
709
+
710
+ this.saveState();
711
+ this.triggerChange();
712
+ return true;
713
+ });
714
+
715
+ document.body.appendChild(modal);
716
+ document.getElementById('link-url').focus();
717
+ }
718
+
719
+ removeLink(linkElement) {
720
+
721
+ const textNode = document.createTextNode(linkElement.textContent);
722
+ linkElement.parentNode.replaceChild(textNode, linkElement);
723
+
724
+ this.saveState();
725
+ this.triggerChange();
726
+ }
727
+
728
+ showImageModal() {
729
+
730
+ this.editor.focus();
731
+ const selection = window.getSelection();
732
+ let savedRange = null;
733
+
734
+ if (selection.rangeCount > 0) {
735
+ savedRange = selection.getRangeAt(0).cloneRange();
736
+ }
737
+
738
+ const modal = this.createModal('Insert Image', `
739
+ <div style="margin-bottom: 16px;">
740
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Image URL:</label>
741
+ <input type="text" id="image-url" placeholder="https://example.com/image.jpg" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
742
+ </div>
743
+ ${this.onImageUpload ? `
744
+ <div style="margin-bottom: 16px; text-align: center;">
745
+ <div style="color: #666; margin-bottom: 8px;">- OR -</div>
746
+ <input type="file" id="image-file" accept="image/*" style="display: block; margin: 0 auto;">
747
+ </div>
748
+ ` : ''}
749
+ <div style="margin-bottom: 16px;">
750
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Alt Text:</label>
751
+ <input type="text" id="image-alt" placeholder="Image description" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
752
+ </div>
753
+ <div style="margin-bottom: 16px;">
754
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Width (optional):</label>
755
+ <input type="number" id="image-width" placeholder="e.g., 400" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
756
+ </div>
757
+ `, async () => {
758
+ let url = document.getElementById('image-url').value.trim();
759
+ const alt = document.getElementById('image-alt').value.trim();
760
+ const width = document.getElementById('image-width').value.trim();
761
+ const fileInput = document.getElementById('image-file');
762
+
763
+ if (fileInput && fileInput.files.length > 0) {
764
+ if (this.onImageUpload) {
765
+ try {
766
+ url = await this.onImageUpload(fileInput.files[0]);
767
+ } catch (error) {
768
+ alert('Failed to upload image');
769
+ return false;
770
+ }
771
+ }
772
+ }
773
+
774
+ if (!url) {
775
+ alert('Image URL is required');
776
+ return false;
777
+ }
778
+
779
+ this.insertImage(url, alt || 'Image', width ? parseInt(width) : null, savedRange);
780
+
781
+ return true;
782
+ });
783
+
784
+ document.body.appendChild(modal);
785
+ document.getElementById('image-url').focus();
786
+ }
787
+
788
+ insertImage(url, alt = 'Image', width = null, savedRange = null) {
789
+
790
+ if (savedRange) {
791
+ this.editor.focus();
792
+ const selection = window.getSelection();
793
+ selection.removeAllRanges();
794
+ selection.addRange(savedRange);
795
+ } else {
796
+
797
+ this.editor.focus();
798
+ }
799
+
800
+ const imageWrapper = document.createElement('div');
801
+ imageWrapper.className = 'editium-image-wrapper align-left';
802
+ imageWrapper.contentEditable = 'false';
803
+ imageWrapper.style.textAlign = 'left';
804
+
805
+ const imageContainer = document.createElement('div');
806
+ imageContainer.style.position = 'relative';
807
+ imageContainer.style.display = 'inline-block';
808
+
809
+ const img = document.createElement('img');
810
+ img.src = url;
811
+ img.alt = alt;
812
+ img.style.maxWidth = '100%';
813
+ img.style.height = 'auto';
814
+ img.style.display = 'block';
815
+ img.style.marginLeft = '0';
816
+ img.style.marginRight = 'auto';
817
+ img.className = 'resizable';
818
+ img.draggable = false;
819
+
820
+ if (width) {
821
+ img.style.width = width + 'px';
822
+ }
823
+
824
+ const toolbar = this.createImageToolbar(imageWrapper, img);
825
+
826
+ imageContainer.appendChild(img);
827
+ imageContainer.appendChild(toolbar);
828
+ imageWrapper.appendChild(imageContainer);
829
+
830
+ this.makeImageResizable(img);
831
+
832
+ const selection = window.getSelection();
833
+ let inserted = false;
834
+
835
+ if (selection.rangeCount > 0) {
836
+ const range = selection.getRangeAt(0);
837
+
838
+ range.deleteContents();
839
+
840
+ try {
841
+ range.insertNode(imageWrapper);
842
+
843
+ const newPara = document.createElement('p');
844
+ newPara.innerHTML = '<br>';
845
+
846
+ if (imageWrapper.nextSibling) {
847
+ imageWrapper.parentNode.insertBefore(newPara, imageWrapper.nextSibling);
848
+ } else {
849
+ imageWrapper.parentNode.appendChild(newPara);
850
+ }
851
+
852
+ range.setStart(newPara, 0);
853
+ range.setEnd(newPara, 0);
854
+ selection.removeAllRanges();
855
+ selection.addRange(range);
856
+
857
+ inserted = true;
858
+ } catch (e) {
859
+ console.error('Error inserting image at cursor:', e);
860
+ }
861
+ }
862
+
863
+ if (!inserted) {
864
+ this.editor.appendChild(imageWrapper);
865
+ const newPara = document.createElement('p');
866
+ newPara.innerHTML = '<br>';
867
+ this.editor.appendChild(newPara);
868
+
869
+ const range = document.createRange();
870
+ range.setStart(newPara, 0);
871
+ range.setEnd(newPara, 0);
872
+ selection.removeAllRanges();
873
+ selection.addRange(range);
874
+ }
875
+
876
+ this.saveState();
877
+ this.triggerChange();
878
+ }
879
+
880
+ createImageToolbar(wrapper, img) {
881
+ const toolbar = document.createElement('div');
882
+ toolbar.className = 'editium-image-toolbar';
883
+
884
+ const alignmentGroup = document.createElement('div');
885
+ alignmentGroup.className = 'editium-image-toolbar-group';
886
+
887
+ const alignments = [
888
+ { value: 'left', label: '⬅', title: 'Align left' },
889
+ { value: 'center', label: '↔', title: 'Align center' },
890
+ { value: 'right', label: '➡', title: 'Align right' }
891
+ ];
892
+
893
+ alignments.forEach(align => {
894
+ const btn = document.createElement('button');
895
+ btn.textContent = align.label;
896
+ btn.title = align.title;
897
+ btn.className = align.value === 'left' ? 'active' : '';
898
+ btn.onclick = (e) => {
899
+ e.preventDefault();
900
+ e.stopPropagation();
901
+ this.changeImageAlignment(wrapper, align.value);
902
+
903
+ alignmentGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));
904
+ btn.classList.add('active');
905
+ };
906
+ alignmentGroup.appendChild(btn);
907
+ });
908
+
909
+ toolbar.appendChild(alignmentGroup);
910
+
911
+ const actionGroup = document.createElement('div');
912
+ actionGroup.className = 'editium-image-toolbar-group';
913
+
914
+ const removeBtn = document.createElement('button');
915
+ removeBtn.innerHTML = '<i class="fa-solid fa-trash"></i>';
916
+ removeBtn.title = 'Remove Image';
917
+ removeBtn.style.color = '#dc3545';
918
+ removeBtn.onclick = (e) => {
919
+ e.preventDefault();
920
+ e.stopPropagation();
921
+ if (confirm('Remove this image?')) {
922
+ wrapper.remove();
923
+ this.saveState();
924
+ this.triggerChange();
925
+ }
926
+ };
927
+ actionGroup.appendChild(removeBtn);
928
+
929
+ toolbar.appendChild(actionGroup);
930
+
931
+ return toolbar;
932
+ }
933
+
934
+ changeImageAlignment(wrapper, alignment) {
935
+
936
+ wrapper.classList.remove('align-left', 'align-center', 'align-right');
937
+
938
+ wrapper.classList.add(`align-${alignment}`);
939
+
940
+ const container = wrapper.querySelector('div[style*="position: relative"]');
941
+ const img = wrapper.querySelector('img');
942
+
943
+ if (container && img) {
944
+
945
+ if (alignment === 'left') {
946
+ wrapper.style.textAlign = 'left';
947
+ img.style.marginLeft = '0';
948
+ img.style.marginRight = 'auto';
949
+ } else if (alignment === 'center') {
950
+ wrapper.style.textAlign = 'center';
951
+ img.style.marginLeft = 'auto';
952
+ img.style.marginRight = 'auto';
953
+ } else if (alignment === 'right') {
954
+ wrapper.style.textAlign = 'right';
955
+ img.style.marginLeft = 'auto';
956
+ img.style.marginRight = '0';
957
+ }
958
+ }
959
+
960
+ this.saveState();
961
+ this.triggerChange();
962
+ }
963
+
964
+ makeImageResizable(img) {
965
+ let isResizing = false;
966
+ let startX, startWidth;
967
+
968
+ const startResize = (e) => {
969
+ e.preventDefault();
970
+ e.stopPropagation();
971
+
972
+ isResizing = true;
973
+ startX = e.clientX || e.touches[0].clientX;
974
+ startWidth = img.offsetWidth;
975
+
976
+ img.classList.add('resizing');
977
+
978
+ document.addEventListener('mousemove', resize);
979
+ document.addEventListener('mouseup', stopResize);
980
+ document.addEventListener('touchmove', resize);
981
+ document.addEventListener('touchend', stopResize);
982
+ };
983
+
984
+ const resize = (e) => {
985
+ if (!isResizing) return;
986
+
987
+ e.preventDefault();
988
+ const currentX = e.clientX || e.touches[0].clientX;
989
+ const diff = currentX - startX;
990
+ const newWidth = startWidth + diff;
991
+
992
+ if (newWidth > 50 && newWidth <= this.editor.offsetWidth) {
993
+ img.style.width = newWidth + 'px';
994
+ }
995
+ };
996
+
997
+ const stopResize = () => {
998
+ if (!isResizing) return;
999
+
1000
+ isResizing = false;
1001
+ img.classList.remove('resizing');
1002
+
1003
+ document.removeEventListener('mousemove', resize);
1004
+ document.removeEventListener('mouseup', stopResize);
1005
+ document.removeEventListener('touchmove', resize);
1006
+ document.removeEventListener('touchend', stopResize);
1007
+
1008
+ this.saveState();
1009
+ this.triggerChange();
1010
+ };
1011
+
1012
+ img.addEventListener('mousedown', (e) => {
1013
+ const rect = img.getBoundingClientRect();
1014
+ const offsetX = e.clientX - rect.left;
1015
+
1016
+ if (offsetX > rect.width - 20) {
1017
+ startResize(e);
1018
+ }
1019
+ });
1020
+
1021
+ img.addEventListener('touchstart', (e) => {
1022
+ const rect = img.getBoundingClientRect();
1023
+ const touch = e.touches[0];
1024
+ const offsetX = touch.clientX - rect.left;
1025
+
1026
+ if (offsetX > rect.width - 20) {
1027
+ startResize(e);
1028
+ }
1029
+ });
1030
+ }
1031
+
1032
+ showTableModal() {
1033
+ const modal = this.createModal('Insert Table', `
1034
+ <div style="margin-bottom: 16px;">
1035
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Rows:</label>
1036
+ <input type="number" id="table-rows" value="3" min="1" max="20" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
1037
+ </div>
1038
+ <div style="margin-bottom: 16px;">
1039
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Columns:</label>
1040
+ <input type="number" id="table-cols" value="3" min="1" max="10" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
1041
+ </div>
1042
+ `, () => {
1043
+ const rows = parseInt(document.getElementById('table-rows').value) || 3;
1044
+ const cols = parseInt(document.getElementById('table-cols').value) || 3;
1045
+
1046
+ const table = document.createElement('table');
1047
+ table.style.cssText = 'border-collapse: collapse; width: 100%; margin-bottom: 1em; border: 1px solid #ccc;';
1048
+
1049
+ for (let i = 0; i < rows; i++) {
1050
+ const tr = document.createElement('tr');
1051
+ for (let j = 0; j < cols; j++) {
1052
+ const cell = i === 0 ? document.createElement('th') : document.createElement('td');
1053
+ cell.style.cssText = 'border: 1px solid #ccc; padding: 8px; text-align: left;';
1054
+ if (i === 0) {
1055
+ cell.style.backgroundColor = '#f0f0f0';
1056
+ cell.style.fontWeight = 'bold';
1057
+ cell.textContent = `Header ${j + 1}`;
1058
+ } else {
1059
+ cell.innerHTML = '<br>';
1060
+ }
1061
+ cell.contentEditable = 'true';
1062
+ tr.appendChild(cell);
1063
+ }
1064
+ table.appendChild(tr);
1065
+ }
1066
+
1067
+ this.insertNodeAtCursor(table);
1068
+ this.saveState();
1069
+ this.triggerChange();
1070
+ return true;
1071
+ });
1072
+
1073
+ document.body.appendChild(modal);
1074
+ }
1075
+
1076
+ showColorPicker(command) {
1077
+ const colors = [
1078
+ '#000000', '#495057', '#6c757d', '#adb5bd',
1079
+ '#dc3545', '#fd7e14', '#ffc107', '#28a745',
1080
+ '#20c997', '#007bff', '#6610f2', '#6f42c1',
1081
+ '#e83e8c', '#ffffff'
1082
+ ];
1083
+
1084
+ const colorHTML = colors.map(color =>
1085
+ `<button type="button" style="width:30px;height:30px;border:1px solid #ccc;background:${color};cursor:pointer;margin:4px;border-radius:3px;" data-color="${color}"></button>`
1086
+ ).join('');
1087
+
1088
+ const modal = this.createModal('Choose Color', `
1089
+ <div style="display: flex; flex-wrap: wrap; justify-content: center; margin-bottom: 16px;">
1090
+ ${colorHTML}
1091
+ </div>
1092
+ <div>
1093
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Or enter custom color:</label>
1094
+ <input type="text" id="custom-color" placeholder="#000000 or rgb(0,0,0)" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
1095
+ </div>
1096
+ `, () => {
1097
+ const customColor = document.getElementById('custom-color').value.trim();
1098
+ if (customColor) {
1099
+ this.execCommand(command, customColor);
1100
+ }
1101
+ return true;
1102
+ });
1103
+
1104
+ modal.querySelectorAll('[data-color]').forEach(btn => {
1105
+ btn.onclick = () => {
1106
+ this.execCommand(command, btn.getAttribute('data-color'));
1107
+ modal.remove();
1108
+ };
1109
+ });
1110
+
1111
+ document.body.appendChild(modal);
1112
+ }
1113
+
1114
+ createModal(title, content, onSubmit) {
1115
+ const overlay = document.createElement('div');
1116
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px;';
1117
+
1118
+ const modal = document.createElement('div');
1119
+ modal.style.cssText = 'background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,0.2);max-width:500px;width:100%;max-height:90vh;overflow:auto;';
1120
+
1121
+ modal.innerHTML = `
1122
+ <div style="padding:16px 20px;border-bottom:1px solid #ccc;display:flex;justify-content:space-between;align-items:center;">
1123
+ <h3 style="margin:0;font-size:18px;font-weight:600;color:#222f3e;">${title}</h3>
1124
+ <button type="button" class="modal-close" style="background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0;width:32px;height:32px;line-height:24px;">×</button>
1125
+ </div>
1126
+ <div style="padding:20px;">
1127
+ ${content}
1128
+ </div>
1129
+ <div style="padding:16px 20px;border-top:1px solid #ccc;display:flex;justify-content:flex-end;gap:10px;">
1130
+ <button type="button" class="modal-cancel" style="padding:8px 16px;border:1px solid #ccc;border-radius:4px;background:#fff;color:#333;cursor:pointer;font-size:14px;">Cancel</button>
1131
+ <button type="button" class="modal-submit" style="padding:8px 16px;border:1px solid #007bff;border-radius:4px;background:#007bff;color:#fff;cursor:pointer;font-size:14px;font-weight:500;">Insert</button>
1132
+ </div>
1133
+ `;
1134
+
1135
+ overlay.appendChild(modal);
1136
+
1137
+ const close = () => overlay.remove();
1138
+
1139
+ modal.querySelector('.modal-close').onclick = close;
1140
+ modal.querySelector('.modal-cancel').onclick = close;
1141
+ modal.querySelector('.modal-submit').onclick = async () => {
1142
+ const result = await onSubmit();
1143
+ if (result !== false) {
1144
+ close();
1145
+ }
1146
+ };
1147
+
1148
+ overlay.onclick = (e) => {
1149
+ if (e.target === overlay) close();
1150
+ };
1151
+
1152
+ return overlay;
1153
+ }
1154
+
1155
+ insertCodeBlock() {
1156
+ const pre = document.createElement('pre');
1157
+ const code = document.createElement('code');
1158
+ code.textContent = 'Code here...';
1159
+ pre.appendChild(code);
1160
+ pre.style.cssText = 'background:#f4f4f4;padding:10px;border-radius:3px;font-family:monospace;overflow:auto;margin:1em 0;';
1161
+
1162
+ this.insertNodeAtCursor(pre);
1163
+ this.saveState();
1164
+ this.triggerChange();
1165
+ }
1166
+
1167
+ insertNodeAtCursor(node) {
1168
+ const selection = window.getSelection();
1169
+ if (selection.rangeCount) {
1170
+ const range = selection.getRangeAt(0);
1171
+ range.deleteContents();
1172
+ range.insertNode(node);
1173
+
1174
+ range.setStartAfter(node);
1175
+ range.setEndAfter(node);
1176
+ selection.removeAllRanges();
1177
+ selection.addRange(range);
1178
+ } else {
1179
+ this.editor.appendChild(node);
1180
+ }
1181
+ }
1182
+
1183
+ toggleFullscreen() {
1184
+ this.isFullscreen = !this.isFullscreen;
1185
+ if (this.isFullscreen) {
1186
+ this.wrapper.classList.add('editium-fullscreen');
1187
+
1188
+ document.body.classList.add('editium-fullscreen-active');
1189
+
1190
+ this.editor.style.height = 'auto';
1191
+ this.editor.style.minHeight = 'auto';
1192
+ this.editor.style.maxHeight = 'none';
1193
+ } else {
1194
+ this.wrapper.classList.remove('editium-fullscreen');
1195
+
1196
+ document.body.classList.remove('editium-fullscreen-active');
1197
+
1198
+ this.editor.style.height = typeof this.height === 'number' ? `${this.height}px` : this.height;
1199
+ this.editor.style.minHeight = typeof this.minHeight === 'number' ? `${this.minHeight}px` : this.minHeight;
1200
+ this.editor.style.maxHeight = typeof this.maxHeight === 'number' ? `${this.maxHeight}px` : this.maxHeight;
1201
+ }
1202
+ }
1203
+
1204
+ toggleFindReplace() {
1205
+ if (this.findReplacePanel) {
1206
+ this.findReplacePanel.remove();
1207
+ this.findReplacePanel = null;
1208
+ this.clearSearch();
1209
+ return;
1210
+ }
1211
+
1212
+ const panel = document.createElement('div');
1213
+ panel.className = 'editium-find-replace';
1214
+ panel.innerHTML = `
1215
+ <div style="display: flex; align-items: flex-start; gap: 12px;">
1216
+ <!-- Search Input Container -->
1217
+ <div style="flex: 1; min-width: 200px;">
1218
+ <div style="position: relative;">
1219
+ <i class="fa-solid fa-magnifying-glass" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: #9ca3af; pointer-events: none; font-size: 14px;"></i>
1220
+ <input type="text" placeholder="Find..." class="editium-find-input" autocomplete="off" style="width: 100%; padding: 8px 10px 8px 32px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px; outline: none; transition: all 0.2s; background-color: white; box-sizing: border-box;">
1221
+ </div>
1222
+ <div class="editium-match-info" style="margin-top: 4px; font-size: 11px; color: #6b7280; min-height: 14px;"></div>
1223
+ </div>
1224
+
1225
+ <!-- Navigation Buttons -->
1226
+ <div class="editium-nav-buttons" style="display: none; gap: 4px;">
1227
+ <button class="editium-btn-prev" title="Previous match (Shift+Enter)" style="padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; background-color: white; cursor: pointer; display: flex; align-items: center; color: #6b7280; transition: all 0.2s;">
1228
+ <i class="fa-solid fa-chevron-left" style="font-size: 14px;"></i>
1229
+ </button>
1230
+ <button class="editium-btn-next" title="Next match (Enter)" style="padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; background-color: white; cursor: pointer; display: flex; align-items: center; color: #6b7280; transition: all 0.2s;">
1231
+ <i class="fa-solid fa-chevron-right" style="font-size: 14px;"></i>
1232
+ </button>
1233
+ </div>
1234
+
1235
+ <!-- Replace Input -->
1236
+ <div style="flex: 1; min-width: 200px;">
1237
+ <input type="text" placeholder="Replace..." class="editium-replace-input" autocomplete="off" style="width: 100%; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px; outline: none; transition: all 0.2s; background-color: white; box-sizing: border-box;">
1238
+ </div>
1239
+
1240
+ <!-- Action Buttons -->
1241
+ <div style="display: flex; gap: 6px;">
1242
+ <button class="editium-btn-replace" title="Replace current match" style="padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; background-color: white; color: #374151; cursor: pointer; font-size: 13px; font-weight: 500; white-space: nowrap; transition: all 0.2s;">
1243
+ Replace
1244
+ </button>
1245
+ <button class="editium-btn-replace-all" title="Replace all matches" style="padding: 8px 12px; border: none; border-radius: 6px; background-color: #3b82f6; color: white; cursor: pointer; font-size: 13px; font-weight: 500; white-space: nowrap; transition: all 0.2s;">
1246
+ Replace All
1247
+ </button>
1248
+ </div>
1249
+
1250
+ <!-- Close Button -->
1251
+ <button class="editium-btn-close" title="Close (Esc)" style="padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; background-color: white; cursor: pointer; display: flex; align-items: center; color: #6b7280; transition: all 0.2s;">
1252
+ <i class="fa-solid fa-xmark" style="font-size: 16px;"></i>
1253
+ </button>
1254
+ </div>
1255
+ `;
1256
+
1257
+ this.editorContainer.insertBefore(panel, this.editor);
1258
+ this.findReplacePanel = panel;
1259
+
1260
+ const findInput = panel.querySelector('.editium-find-input');
1261
+ const replaceInput = panel.querySelector('.editium-replace-input');
1262
+ const matchInfo = panel.querySelector('.editium-match-info');
1263
+ const navButtons = panel.querySelector('.editium-nav-buttons');
1264
+ const prevBtn = panel.querySelector('.editium-btn-prev');
1265
+ const nextBtn = panel.querySelector('.editium-btn-next');
1266
+ const replaceBtn = panel.querySelector('.editium-btn-replace');
1267
+ const replaceAllBtn = panel.querySelector('.editium-btn-replace-all');
1268
+ const closeBtn = panel.querySelector('.editium-btn-close');
1269
+
1270
+ findInput.addEventListener('focus', () => {
1271
+ if (!this.searchQuery || this.searchMatches.length > 0) {
1272
+ findInput.style.borderColor = '#3b82f6';
1273
+ }
1274
+ });
1275
+
1276
+ findInput.addEventListener('blur', () => {
1277
+ if (this.searchQuery && this.searchMatches.length === 0) {
1278
+ findInput.style.borderColor = '#ef4444';
1279
+ } else {
1280
+ findInput.style.borderColor = '#d1d5db';
1281
+ }
1282
+ });
1283
+
1284
+ replaceInput.addEventListener('focus', () => {
1285
+ replaceInput.style.borderColor = '#3b82f6';
1286
+ });
1287
+
1288
+ replaceInput.addEventListener('blur', () => {
1289
+ replaceInput.style.borderColor = '#d1d5db';
1290
+ });
1291
+
1292
+ findInput.addEventListener('input', () => {
1293
+ this.searchQuery = findInput.value;
1294
+ this.performSearch();
1295
+
1296
+ if (this.searchQuery) {
1297
+ if (this.searchMatches.length === 0) {
1298
+ matchInfo.textContent = 'No matches';
1299
+ matchInfo.style.color = '#ef4444';
1300
+ findInput.style.borderColor = '#ef4444';
1301
+ navButtons.style.display = 'none';
1302
+ replaceBtn.disabled = true;
1303
+ replaceAllBtn.disabled = true;
1304
+ replaceBtn.style.backgroundColor = '#f3f4f6';
1305
+ replaceBtn.style.color = '#9ca3af';
1306
+ replaceBtn.style.cursor = 'not-allowed';
1307
+ replaceAllBtn.style.backgroundColor = '#cbd5e1';
1308
+ replaceAllBtn.style.cursor = 'not-allowed';
1309
+ } else {
1310
+ matchInfo.textContent = `${this.currentMatchIndex + 1} of ${this.searchMatches.length}`;
1311
+ matchInfo.style.color = '#6b7280';
1312
+ findInput.style.borderColor = '#d1d5db';
1313
+ navButtons.style.display = 'flex';
1314
+ replaceBtn.disabled = false;
1315
+ replaceAllBtn.disabled = false;
1316
+ replaceBtn.style.backgroundColor = 'white';
1317
+ replaceBtn.style.color = '#374151';
1318
+ replaceBtn.style.cursor = 'pointer';
1319
+ replaceAllBtn.style.backgroundColor = '#3b82f6';
1320
+ replaceAllBtn.style.cursor = 'pointer';
1321
+ }
1322
+ } else {
1323
+ matchInfo.textContent = '';
1324
+ navButtons.style.display = 'none';
1325
+ replaceBtn.disabled = true;
1326
+ replaceAllBtn.disabled = true;
1327
+ }
1328
+ });
1329
+
1330
+ prevBtn.onclick = () => {
1331
+ this.navigateSearch(-1, matchInfo);
1332
+ };
1333
+
1334
+ nextBtn.onclick = () => {
1335
+ this.navigateSearch(1, matchInfo);
1336
+ };
1337
+
1338
+ [prevBtn, nextBtn].forEach(btn => {
1339
+ btn.addEventListener('mouseenter', () => {
1340
+ if (this.searchMatches.length > 0) {
1341
+ btn.style.backgroundColor = '#f3f4f6';
1342
+ btn.style.borderColor = '#9ca3af';
1343
+ }
1344
+ });
1345
+ btn.addEventListener('mouseleave', () => {
1346
+ if (this.searchMatches.length > 0) {
1347
+ btn.style.backgroundColor = 'white';
1348
+ btn.style.borderColor = '#d1d5db';
1349
+ }
1350
+ });
1351
+ });
1352
+
1353
+ replaceBtn.addEventListener('mouseenter', () => {
1354
+ if (!replaceBtn.disabled) {
1355
+ replaceBtn.style.backgroundColor = '#f3f4f6';
1356
+ replaceBtn.style.borderColor = '#9ca3af';
1357
+ }
1358
+ });
1359
+ replaceBtn.addEventListener('mouseleave', () => {
1360
+ if (!replaceBtn.disabled) {
1361
+ replaceBtn.style.backgroundColor = 'white';
1362
+ replaceBtn.style.borderColor = '#d1d5db';
1363
+ }
1364
+ });
1365
+
1366
+ replaceAllBtn.addEventListener('mouseenter', () => {
1367
+ if (!replaceAllBtn.disabled) {
1368
+ replaceAllBtn.style.backgroundColor = '#2563eb';
1369
+ }
1370
+ });
1371
+ replaceAllBtn.addEventListener('mouseleave', () => {
1372
+ if (!replaceAllBtn.disabled) {
1373
+ replaceAllBtn.style.backgroundColor = '#3b82f6';
1374
+ }
1375
+ });
1376
+
1377
+ closeBtn.addEventListener('mouseenter', () => {
1378
+ closeBtn.style.backgroundColor = '#fee2e2';
1379
+ closeBtn.style.borderColor = '#ef4444';
1380
+ closeBtn.style.color = '#dc2626';
1381
+ });
1382
+ closeBtn.addEventListener('mouseleave', () => {
1383
+ closeBtn.style.backgroundColor = 'white';
1384
+ closeBtn.style.borderColor = '#d1d5db';
1385
+ closeBtn.style.color = '#6b7280';
1386
+ });
1387
+
1388
+ replaceBtn.onclick = () => {
1389
+ this.replaceCurrentMatch(replaceInput.value, matchInfo);
1390
+ };
1391
+
1392
+ replaceAllBtn.onclick = () => {
1393
+ this.replaceAllMatches(replaceInput.value);
1394
+ this.clearSearch();
1395
+ findInput.value = '';
1396
+ replaceInput.value = '';
1397
+ matchInfo.textContent = '';
1398
+ navButtons.style.display = 'none';
1399
+ };
1400
+
1401
+ closeBtn.onclick = () => {
1402
+ panel.remove();
1403
+ this.findReplacePanel = null;
1404
+ this.clearSearch();
1405
+ };
1406
+
1407
+ findInput.focus();
1408
+ }
1409
+
1410
+ performSearch() {
1411
+ this.clearHighlights();
1412
+ if (!this.searchQuery || this.searchQuery.trim() === '') {
1413
+ this.searchMatches = [];
1414
+ return;
1415
+ }
1416
+
1417
+ const searchLower = this.searchQuery.toLowerCase();
1418
+ const matches = [];
1419
+
1420
+ const walker = document.createTreeWalker(
1421
+ this.editor,
1422
+ NodeFilter.SHOW_TEXT,
1423
+ {
1424
+ acceptNode: (node) => {
1425
+
1426
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-match')) {
1427
+ return NodeFilter.FILTER_REJECT;
1428
+ }
1429
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-current')) {
1430
+ return NodeFilter.FILTER_REJECT;
1431
+ }
1432
+ return NodeFilter.FILTER_ACCEPT;
1433
+ }
1434
+ }
1435
+ );
1436
+
1437
+ let currentNode;
1438
+ const nodesToProcess = [];
1439
+
1440
+ while (currentNode = walker.nextNode()) {
1441
+ nodesToProcess.push(currentNode);
1442
+ }
1443
+
1444
+ nodesToProcess.forEach(node => {
1445
+ const text = node.textContent;
1446
+ const textLower = text.toLowerCase();
1447
+ let index = 0;
1448
+
1449
+ while ((index = textLower.indexOf(searchLower, index)) !== -1) {
1450
+ matches.push({
1451
+ node: node,
1452
+ offset: index,
1453
+ length: this.searchQuery.length
1454
+ });
1455
+ index += 1;
1456
+ }
1457
+ });
1458
+
1459
+ this.searchMatches = matches;
1460
+ this.currentMatchIndex = 0;
1461
+
1462
+ if (this.searchMatches.length > 0) {
1463
+ this.highlightAllMatches();
1464
+ }
1465
+ }
1466
+
1467
+ highlightAllMatches() {
1468
+
1469
+ const sortedMatches = [...this.searchMatches].reverse();
1470
+
1471
+ sortedMatches.forEach((match, reverseIdx) => {
1472
+ const actualIdx = this.searchMatches.length - 1 - reverseIdx;
1473
+ const { node, offset, length } = match;
1474
+
1475
+ if (!node.parentNode) return;
1476
+
1477
+ try {
1478
+
1479
+ const text = node.textContent;
1480
+ const before = text.substring(0, offset);
1481
+ const matchText = text.substring(offset, offset + length);
1482
+ const after = text.substring(offset + length);
1483
+
1484
+ const mark = document.createElement('mark');
1485
+ const isCurrent = actualIdx === this.currentMatchIndex;
1486
+ mark.className = isCurrent ? 'editium-search-current' : 'editium-search-match';
1487
+ mark.textContent = matchText;
1488
+
1489
+ if (isCurrent) {
1490
+ mark.style.backgroundColor = '#ff9800';
1491
+ mark.style.color = '#ffffff';
1492
+ mark.style.fontWeight = '600';
1493
+ mark.setAttribute('data-current-match', 'true');
1494
+ } else {
1495
+ mark.style.backgroundColor = '#ffeb3b';
1496
+ mark.style.color = '#000000';
1497
+ }
1498
+ mark.style.padding = '2px 4px';
1499
+ mark.style.borderRadius = '2px';
1500
+
1501
+ const parent = node.parentNode;
1502
+ const fragment = document.createDocumentFragment();
1503
+
1504
+ if (before) {
1505
+ fragment.appendChild(document.createTextNode(before));
1506
+ }
1507
+ fragment.appendChild(mark);
1508
+ if (after) {
1509
+ fragment.appendChild(document.createTextNode(after));
1510
+ }
1511
+
1512
+ parent.replaceChild(fragment, node);
1513
+
1514
+ } catch (e) {
1515
+ console.warn('Failed to highlight match:', e);
1516
+ }
1517
+ });
1518
+
1519
+ if (this.searchMatches.length > 0) {
1520
+ setTimeout(() => {
1521
+ const currentMark = this.editor.querySelector('[data-current-match="true"]');
1522
+ if (currentMark) {
1523
+ currentMark.scrollIntoView({ behavior: 'smooth', block: 'center' });
1524
+ }
1525
+ }, 10);
1526
+ }
1527
+ }
1528
+
1529
+ clearHighlights() {
1530
+ this.editor.querySelectorAll('.editium-search-match, .editium-search-current').forEach(mark => {
1531
+ const parent = mark.parentNode;
1532
+ while (mark.firstChild) {
1533
+ parent.insertBefore(mark.firstChild, mark);
1534
+ }
1535
+ parent.removeChild(mark);
1536
+ parent.normalize();
1537
+ });
1538
+ }
1539
+
1540
+ navigateSearch(direction, matchInfoElement) {
1541
+ if (this.searchMatches.length === 0) return;
1542
+
1543
+ this.currentMatchIndex += direction;
1544
+ if (this.currentMatchIndex < 0) {
1545
+ this.currentMatchIndex = this.searchMatches.length - 1;
1546
+ } else if (this.currentMatchIndex >= this.searchMatches.length) {
1547
+ this.currentMatchIndex = 0;
1548
+ }
1549
+
1550
+ this.clearHighlights();
1551
+
1552
+ const searchLower = this.searchQuery.toLowerCase();
1553
+ const matches = [];
1554
+
1555
+ const walker = document.createTreeWalker(
1556
+ this.editor,
1557
+ NodeFilter.SHOW_TEXT,
1558
+ {
1559
+ acceptNode: (node) => {
1560
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-match')) {
1561
+ return NodeFilter.FILTER_REJECT;
1562
+ }
1563
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-current')) {
1564
+ return NodeFilter.FILTER_REJECT;
1565
+ }
1566
+ return NodeFilter.FILTER_ACCEPT;
1567
+ }
1568
+ }
1569
+ );
1570
+
1571
+ let currentNode;
1572
+ const nodesToProcess = [];
1573
+
1574
+ while (currentNode = walker.nextNode()) {
1575
+ nodesToProcess.push(currentNode);
1576
+ }
1577
+
1578
+ nodesToProcess.forEach(node => {
1579
+ const text = node.textContent;
1580
+ const textLower = text.toLowerCase();
1581
+ let index = 0;
1582
+
1583
+ while ((index = textLower.indexOf(searchLower, index)) !== -1) {
1584
+ matches.push({
1585
+ node: node,
1586
+ offset: index,
1587
+ length: this.searchQuery.length
1588
+ });
1589
+ index += 1;
1590
+ }
1591
+ });
1592
+
1593
+ this.searchMatches = matches;
1594
+
1595
+ if (this.searchMatches.length > 0) {
1596
+ this.highlightAllMatches();
1597
+ }
1598
+
1599
+ if (matchInfoElement) {
1600
+ matchInfoElement.textContent = this.searchMatches.length > 0
1601
+ ? `${this.currentMatchIndex + 1} of ${this.searchMatches.length}`
1602
+ : 'No matches';
1603
+ matchInfoElement.style.color = this.searchMatches.length > 0 ? '#6b7280' : '#ef4444';
1604
+ }
1605
+ }
1606
+
1607
+ updateMatchCount(element) {
1608
+ if (element) {
1609
+ element.textContent = this.searchMatches.length > 0
1610
+ ? `${this.currentMatchIndex + 1}/${this.searchMatches.length}`
1611
+ : '0/0';
1612
+ }
1613
+ }
1614
+
1615
+ replaceCurrentMatch(replacement, matchCountElement) {
1616
+ if (this.searchMatches.length === 0) return;
1617
+
1618
+ const currentMark = this.editor.querySelectorAll('.editium-search-match, .editium-search-current')[this.currentMatchIndex];
1619
+ if (currentMark) {
1620
+ currentMark.textContent = replacement;
1621
+ const parent = currentMark.parentNode;
1622
+ while (currentMark.firstChild) {
1623
+ parent.insertBefore(currentMark.firstChild, currentMark);
1624
+ }
1625
+ parent.removeChild(currentMark);
1626
+ parent.normalize();
1627
+
1628
+ this.performSearch();
1629
+ this.updateMatchCount(matchCountElement);
1630
+ this.saveState();
1631
+ this.triggerChange();
1632
+ }
1633
+ }
1634
+
1635
+ replaceAllMatches(replacement) {
1636
+ this.editor.querySelectorAll('.editium-search-match, .editium-search-current').forEach(mark => {
1637
+ mark.textContent = replacement;
1638
+ const parent = mark.parentNode;
1639
+ while (mark.firstChild) {
1640
+ parent.insertBefore(mark.firstChild, mark);
1641
+ }
1642
+ parent.removeChild(mark);
1643
+ parent.normalize();
1644
+ });
1645
+
1646
+ this.saveState();
1647
+ this.triggerChange();
1648
+ }
1649
+
1650
+ clearSearch() {
1651
+ this.clearHighlights();
1652
+ this.searchMatches = [];
1653
+ this.currentMatchIndex = 0;
1654
+ this.searchQuery = '';
1655
+ }
1656
+
1657
+ viewOutput(type) {
1658
+ const modal = document.createElement('div');
1659
+ modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px;';
1660
+
1661
+ let content = '';
1662
+ let title = '';
1663
+
1664
+ if (type === 'html') {
1665
+ content = this.formatHTML(this.getHTML());
1666
+ title = 'HTML Output';
1667
+
1668
+ if (!content || content.trim() === '') {
1669
+ content = '<!-- No content -->';
1670
+ }
1671
+ } else if (type === 'json') {
1672
+ content = JSON.stringify(this.getJSON(), null, 2);
1673
+ title = 'JSON Output';
1674
+ } else if (type === 'preview') {
1675
+ const htmlContent = this.getHTML();
1676
+ const previewContent = htmlContent && htmlContent.trim() !== ''
1677
+ ? htmlContent
1678
+ : '<p style="color:#999;text-align:center;padding:40px;">No content to preview</p>';
1679
+
1680
+ modal.innerHTML = `
1681
+ <div style="background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,0.2);max-width:1200px;width:100%;max-height:90vh;display:flex;flex-direction:column;">
1682
+ <div style="padding:16px 20px;border-bottom:1px solid #ccc;display:flex;justify-content:space-between;align-items:center;">
1683
+ <h3 style="margin:0;font-size:18px;font-weight:600;color:#222f3e;">Preview</h3>
1684
+ <button class="modal-close" style="background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0;">×</button>
1685
+ </div>
1686
+ <div style="padding:20px;flex:1;overflow:auto;">${previewContent}</div>
1687
+ <div style="padding:16px 20px;border-top:1px solid #ccc;display:flex;justify-content:flex-end;gap:10px;">
1688
+ <button class="btn-copy" style="padding:8px 16px;border:1px solid #007bff;border-radius:4px;background:#007bff;color:#fff;cursor:pointer;font-size:14px;font-weight:500;">Copy HTML</button>
1689
+ </div>
1690
+ </div>
1691
+ `;
1692
+ document.body.appendChild(modal);
1693
+
1694
+ const closeBtn = modal.querySelector('.modal-close');
1695
+ const copyBtn = modal.querySelector('.btn-copy');
1696
+
1697
+ closeBtn.onclick = () => modal.remove();
1698
+ modal.onclick = (e) => {
1699
+ if (e.target === modal) modal.remove();
1700
+ };
1701
+
1702
+ copyBtn.onclick = () => {
1703
+ const html = this.getHTML();
1704
+ if (html && html.trim() !== '') {
1705
+ navigator.clipboard.writeText(html).then(() => {
1706
+ copyBtn.textContent = 'Copied!';
1707
+ copyBtn.style.backgroundColor = '#28a745';
1708
+ copyBtn.style.borderColor = '#28a745';
1709
+ setTimeout(() => {
1710
+ copyBtn.textContent = 'Copy HTML';
1711
+ copyBtn.style.backgroundColor = '#007bff';
1712
+ copyBtn.style.borderColor = '#007bff';
1713
+ }, 2000);
1714
+ });
1715
+ }
1716
+ };
1717
+ return;
1718
+ }
1719
+
1720
+ modal.innerHTML = `
1721
+ <div style="background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,0.2);max-width:900px;width:100%;max-height:90vh;display:flex;flex-direction:column;">
1722
+ <div style="padding:16px 20px;border-bottom:1px solid #ccc;display:flex;justify-content:space-between;align-items:center;">
1723
+ <h3 style="margin:0;font-size:18px;font-weight:600;color:#222f3e;">${title}</h3>
1724
+ <button class="modal-close" style="background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0;">×</button>
1725
+ </div>
1726
+ <div style="padding:0;flex:1;overflow:auto;background:#282c34;">
1727
+ <pre style="margin:0;padding:20px;overflow-x:auto;background:#282c34;color:#abb2bf;font-family:'Courier New',Courier,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;">${this.highlightCode(content, type)}</pre>
1728
+ </div>
1729
+ <div style="padding:16px 20px;border-top:1px solid #ccc;display:flex;justify-content:flex-end;gap:10px;">
1730
+ <button class="btn-download" style="padding:8px 16px;border:1px solid #6c757d;border-radius:4px;background:#6c757d;color:#fff;cursor:pointer;font-size:14px;font-weight:500;">Download</button>
1731
+ <button class="btn-copy" style="padding:8px 16px;border:1px solid #007bff;border-radius:4px;background:#007bff;color:#fff;cursor:pointer;font-size:14px;font-weight:500;">Copy to Clipboard</button>
1732
+ </div>
1733
+ </div>
1734
+ `;
1735
+
1736
+ document.body.appendChild(modal);
1737
+
1738
+ const closeBtn = modal.querySelector('.modal-close');
1739
+ const copyBtn = modal.querySelector('.btn-copy');
1740
+ const downloadBtn = modal.querySelector('.btn-download');
1741
+
1742
+ closeBtn.onclick = () => modal.remove();
1743
+ modal.onclick = (e) => {
1744
+ if (e.target === modal) modal.remove();
1745
+ };
1746
+
1747
+ copyBtn.onclick = () => {
1748
+ navigator.clipboard.writeText(content).then(() => {
1749
+ copyBtn.textContent = 'Copied!';
1750
+ copyBtn.style.backgroundColor = '#28a745';
1751
+ copyBtn.style.borderColor = '#28a745';
1752
+ setTimeout(() => {
1753
+ copyBtn.textContent = 'Copy to Clipboard';
1754
+ copyBtn.style.backgroundColor = '#007bff';
1755
+ copyBtn.style.borderColor = '#007bff';
1756
+ }, 2000);
1757
+ });
1758
+ };
1759
+
1760
+ downloadBtn.onclick = () => {
1761
+ const blob = new Blob([content], { type: type === 'html' ? 'text/html' : 'application/json' });
1762
+ const url = URL.createObjectURL(blob);
1763
+ const a = document.createElement('a');
1764
+ a.href = url;
1765
+ a.download = `editium-output.${type === 'html' ? 'html' : 'json'}`;
1766
+ document.body.appendChild(a);
1767
+ a.click();
1768
+ document.body.removeChild(a);
1769
+ URL.revokeObjectURL(url);
1770
+ };
1771
+ }
1772
+
1773
+ formatHTML(html) {
1774
+ if (!html || html.trim() === '') return '';
1775
+
1776
+ let indentLevel = 0;
1777
+ const tab = ' ';
1778
+ let formattedHTML = '';
1779
+
1780
+ const rawTokens = html.split(/(<[^>]+>)/);
1781
+
1782
+ const tokens = [];
1783
+ for (let i = 0; i < rawTokens.length; i++) {
1784
+ const token = rawTokens[i];
1785
+ if (token.startsWith('<') && token.endsWith('>')) {
1786
+
1787
+ tokens.push({ type: 'tag', content: token });
1788
+ } else if (token.trim()) {
1789
+
1790
+ tokens.push({ type: 'text', content: token.trim() });
1791
+ }
1792
+ }
1793
+
1794
+ const isBlockTag = (tag) => {
1795
+ const tagNameMatch = tag.match(/<\/?([a-zA-Z0-9]+)/);
1796
+ if (!tagNameMatch) return false;
1797
+ const blockTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'ul', 'ol', 'li', 'blockquote', 'pre', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'header', 'footer', 'section', 'article', 'aside', 'nav', 'main'];
1798
+ return blockTags.includes(tagNameMatch[1].toLowerCase());
1799
+ };
1800
+
1801
+ const isInlineTag = (tag) => {
1802
+ const tagNameMatch = tag.match(/<\/?([a-zA-Z0-9]+)/);
1803
+ if (!tagNameMatch) return false;
1804
+ const inlineTags = ['strong', 'em', 'u', 's', 'a', 'span', 'code', 'b', 'i', 'sub', 'sup', 'img', 'br'];
1805
+ return inlineTags.includes(tagNameMatch[1].toLowerCase());
1806
+ };
1807
+
1808
+ const isSelfClosing = (tag) => {
1809
+ return tag.endsWith('/>') || /<br|<hr|<img/.test(tag);
1810
+ };
1811
+
1812
+ let onNewLine = true;
1813
+
1814
+ for (let i = 0; i < tokens.length; i++) {
1815
+ const token = tokens[i];
1816
+ const prevToken = i > 0 ? tokens[i - 1] : null;
1817
+ const nextToken = i < tokens.length - 1 ? tokens[i + 1] : null;
1818
+
1819
+ if (token.type === 'tag') {
1820
+ const tag = token.content;
1821
+ const isClosing = tag.startsWith('</');
1822
+ const isBlock = isBlockTag(tag);
1823
+ const isInline = isInlineTag(tag);
1824
+
1825
+ if (isBlock) {
1826
+ if (isClosing) {
1827
+ indentLevel = Math.max(0, indentLevel - 1);
1828
+ }
1829
+
1830
+ if (!onNewLine) {
1831
+ formattedHTML += '\n';
1832
+ }
1833
+
1834
+ formattedHTML += tab.repeat(indentLevel) + tag;
1835
+ onNewLine = true;
1836
+
1837
+ if (!isClosing && !isSelfClosing(tag)) {
1838
+ indentLevel++;
1839
+ }
1840
+ } else if (isInline) {
1841
+
1842
+ if (onNewLine) {
1843
+ formattedHTML += tab.repeat(indentLevel);
1844
+ onNewLine = false;
1845
+ }
1846
+
1847
+ if (!isClosing && prevToken && prevToken.type === 'text') {
1848
+ formattedHTML += ' ';
1849
+ }
1850
+
1851
+ formattedHTML += tag;
1852
+
1853
+ if (isClosing && nextToken && nextToken.type === 'text') {
1854
+ formattedHTML += ' ';
1855
+ }
1856
+ } else {
1857
+
1858
+ formattedHTML += tag;
1859
+ }
1860
+ } else if (token.type === 'text') {
1861
+
1862
+ if (onNewLine) {
1863
+ formattedHTML += tab.repeat(indentLevel);
1864
+ onNewLine = false;
1865
+ }
1866
+
1867
+ formattedHTML += token.content;
1868
+ }
1869
+ }
1870
+
1871
+ return formattedHTML.trim();
1872
+ }
1873
+
1874
+ highlightCode(code, type) {
1875
+
1876
+ const escaped = this.escapeHtml(code);
1877
+
1878
+ if (type === 'html') {
1879
+
1880
+ return escaped
1881
+
1882
+ .replace(/(&lt;\/?)([a-z][a-z0-9]*)(\s[^&]*?)?(&gt;)/gi, (match, open, tagName, attrs, close) => {
1883
+ let result = open + '<span style="color:#e06c75;">' + tagName + '</span>';
1884
+ if (attrs) {
1885
+
1886
+ result += attrs.replace(/\s([a-z-]+)(=)(&quot;[^&quot;]*&quot;)/gi, ' <span style="color:#d19a66;">$1</span>=<span style="color:#98c379;">$3</span>');
1887
+ }
1888
+ result += close;
1889
+ return result;
1890
+ });
1891
+ } else if (type === 'json') {
1892
+
1893
+ return escaped
1894
+
1895
+ .replace(/(&quot;)(.*?)(&quot;)(\s*:)/g, '<span style="color:#e06c75;">$1$2$3</span><span style="color:#abb2bf;">$4</span>')
1896
+
1897
+ .replace(/(:)(\s*)(&quot;)((?:[^&]|&(?!quot;))*?)(&quot;)/g, '$1$2<span style="color:#98c379;">$3$4$5</span>')
1898
+
1899
+ .replace(/:\s*(-?\d+\.?\d*)/g, ': <span style="color:#d19a66;">$1</span>')
1900
+
1901
+ .replace(/:\s*(true|false|null)/g, ': <span style="color:#56b6c2;">$1</span>')
1902
+
1903
+ .replace(/([{}\[\]])/g, '<span style="color:#abb2bf;">$1</span>')
1904
+
1905
+ .replace(/(,)/g, '<span style="color:#abb2bf;">$1</span>');
1906
+ }
1907
+
1908
+ return escaped;
1909
+ }
1910
+
1911
+ escapeHtml(text) {
1912
+ const div = document.createElement('div');
1913
+ div.textContent = text;
1914
+ return div.innerHTML;
1915
+ }
1916
+
1917
+ saveState() {
1918
+ const state = this.editor.innerHTML;
1919
+
1920
+ if (this.historyIndex < this.history.length - 1) {
1921
+ this.history = this.history.slice(0, this.historyIndex + 1);
1922
+ }
1923
+
1924
+ if (this.history[this.historyIndex] === state) {
1925
+ return;
1926
+ }
1927
+
1928
+ this.history.push(state);
1929
+ this.historyIndex++;
1930
+
1931
+ if (this.history.length > this.maxHistory) {
1932
+ this.history.shift();
1933
+ this.historyIndex--;
1934
+ }
1935
+ }
1936
+
1937
+ undo() {
1938
+ if (this.historyIndex > 0) {
1939
+ this.historyIndex--;
1940
+ this.editor.innerHTML = this.history[this.historyIndex];
1941
+ this.triggerChange();
1942
+ }
1943
+ }
1944
+
1945
+ redo() {
1946
+ if (this.historyIndex < this.history.length - 1) {
1947
+ this.historyIndex++;
1948
+ this.editor.innerHTML = this.history[this.historyIndex];
1949
+ this.triggerChange();
1950
+ }
1951
+ }
1952
+
1953
+ attachEventListeners() {
1954
+ this.editor.addEventListener('input', () => {
1955
+ this.makeExistingLinksNonEditable();
1956
+ this.saveState();
1957
+ this.triggerChange();
1958
+ this.updateWordCount();
1959
+ });
1960
+
1961
+ this.editor.addEventListener('keydown', (e) => {
1962
+ if (e.ctrlKey && e.key === 'b') {
1963
+ e.preventDefault();
1964
+ this.execCommand('bold');
1965
+ } else if (e.ctrlKey && e.key === 'i') {
1966
+ e.preventDefault();
1967
+ this.execCommand('italic');
1968
+ } else if (e.ctrlKey && e.key === 'u') {
1969
+ e.preventDefault();
1970
+ this.execCommand('underline');
1971
+ } else if (e.ctrlKey && e.key === 'k') {
1972
+
1973
+ e.preventDefault();
1974
+ const selection = window.getSelection();
1975
+ if (selection.rangeCount > 0) {
1976
+ let node = selection.anchorNode;
1977
+
1978
+ while (node && node !== this.editor) {
1979
+ if (node.nodeType === 1 && node.tagName === 'A') {
1980
+ this.editLink(node);
1981
+ return;
1982
+ }
1983
+ node = node.parentNode;
1984
+ }
1985
+ }
1986
+
1987
+ this.showLinkModal();
1988
+ } else if (e.ctrlKey && e.key === 'z' && !e.shiftKey) {
1989
+ e.preventDefault();
1990
+ this.undo();
1991
+ } else if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) {
1992
+ e.preventDefault();
1993
+ this.redo();
1994
+ } else if (e.key === 'F11') {
1995
+ e.preventDefault();
1996
+ this.toggleFullscreen();
1997
+ } else if (e.key === 'Escape' && this.isFullscreen) {
1998
+ e.preventDefault();
1999
+ this.toggleFullscreen();
2000
+ } else if (e.key === 'Escape' && this.linkPopup) {
2001
+
2002
+ e.preventDefault();
2003
+ this.closeLinkPopup();
2004
+ }
2005
+ });
2006
+
2007
+ this.editor.addEventListener('mouseup', () => this.updateToolbarStates());
2008
+ this.editor.addEventListener('keyup', () => this.updateToolbarStates());
2009
+
2010
+ this.editor.addEventListener('beforeinput', (e) => {
2011
+ const selection = window.getSelection();
2012
+ if (selection.rangeCount > 0) {
2013
+ let node = selection.anchorNode;
2014
+
2015
+ while (node && node !== this.editor) {
2016
+ if (node.nodeType === 1 && node.tagName === 'A') {
2017
+
2018
+ if (e.inputType.startsWith('delete') ||
2019
+ e.inputType.startsWith('insert') ||
2020
+ e.inputType.startsWith('format') ||
2021
+ e.inputType === 'historyUndo' ||
2022
+ e.inputType === 'historyRedo') {
2023
+ e.preventDefault();
2024
+
2025
+ node.style.backgroundColor = 'rgba(255, 152, 0, 0.2)';
2026
+ setTimeout(() => {
2027
+ node.style.backgroundColor = '';
2028
+ }, 200);
2029
+ return;
2030
+ }
2031
+ }
2032
+ node = node.parentNode;
2033
+ }
2034
+ }
2035
+ });
2036
+
2037
+ this.editor.addEventListener('click', (e) => {
2038
+ if (e.target.tagName === 'A') {
2039
+ e.preventDefault();
2040
+ this.showLinkPopup(e.target);
2041
+ }
2042
+ });
2043
+
2044
+ this.editor.addEventListener('mousedown', (e) => {
2045
+ if (e.target.tagName === 'A') {
2046
+ e.preventDefault();
2047
+ }
2048
+ });
2049
+
2050
+ document.addEventListener('click', (e) => {
2051
+ if (!e.target.closest('.editium-dropdown')) {
2052
+ this.closeDropdown();
2053
+ }
2054
+
2055
+ if (this.linkPopup && !e.target.closest('.editium-link-popup') && !e.target.closest('a')) {
2056
+ this.closeLinkPopup();
2057
+ }
2058
+ });
2059
+ }
2060
+
2061
+ updateToolbarStates() {
2062
+ if (!this.toolbarElement) return;
2063
+
2064
+ const commands = {
2065
+ 'bold': 'bold',
2066
+ 'italic': 'italic',
2067
+ 'underline': 'underline',
2068
+ 'strikethrough': 'strikeThrough'
2069
+ };
2070
+
2071
+ Object.entries(commands).forEach(([type, cmd]) => {
2072
+ const isActive = document.queryCommandState(cmd);
2073
+ const button = this.toolbarElement.querySelector(`[data-command="${type}"]`);
2074
+ if (button) {
2075
+ button.classList.toggle('active', isActive);
2076
+ }
2077
+ });
2078
+ }
2079
+
2080
+ updateWordCount() {
2081
+ if (!this.wordCountElement) return;
2082
+
2083
+ let statsHTML = '';
2084
+
2085
+ if (this.showWordCount) {
2086
+ const text = this.editor.textContent || '';
2087
+ const words = text.trim().split(/\s+/).filter(w => w.length > 0).length;
2088
+ const chars = text.length;
2089
+ const charsNoSpaces = text.replace(/\s/g, '').length;
2090
+
2091
+ statsHTML = `
2092
+ <div class="editium-word-count-stats">
2093
+ <span>Words: ${words}</span>
2094
+ <span>Characters: ${chars}</span>
2095
+ <span>Characters (no spaces): ${charsNoSpaces}</span>
2096
+ </div>
2097
+ `;
2098
+ }
2099
+
2100
+ this.wordCountElement.innerHTML = `
2101
+ ${statsHTML}
2102
+ <div class="editium-word-count-branding">
2103
+ Built with <a href="https://www.npmjs.com/package/editium" target="_blank" rel="noopener noreferrer" class="editium-brand">Editium</a>
2104
+ </div>
2105
+ `;
2106
+ }
2107
+
2108
+ triggerChange() {
2109
+ this.onChange({
2110
+ html: this.getHTML(),
2111
+ json: this.getJSON(),
2112
+ text: this.getText()
2113
+ });
2114
+ }
2115
+
2116
+ getHTML() {
2117
+
2118
+ const clone = this.editor.cloneNode(true);
2119
+
2120
+ const toolbars = clone.querySelectorAll('.editium-image-toolbar');
2121
+ toolbars.forEach(toolbar => toolbar.remove());
2122
+
2123
+ const wrappers = clone.querySelectorAll('.editium-image-wrapper');
2124
+ wrappers.forEach(wrapper => {
2125
+
2126
+ const alignment = wrapper.classList.contains('align-center') ? 'center' :
2127
+ wrapper.classList.contains('align-right') ? 'right' : 'left';
2128
+
2129
+ wrapper.className = '';
2130
+ wrapper.removeAttribute('contenteditable');
2131
+
2132
+ wrapper.style.textAlign = alignment;
2133
+ wrapper.style.margin = '10px 0';
2134
+ wrapper.style.display = 'block';
2135
+ });
2136
+
2137
+ const containers = clone.querySelectorAll('div[style*="position: relative"]');
2138
+ containers.forEach(container => {
2139
+ if (container.querySelector('img')) {
2140
+
2141
+ container.style.position = '';
2142
+ container.style.display = '';
2143
+ }
2144
+ });
2145
+
2146
+ const images = clone.querySelectorAll('img');
2147
+ images.forEach(img => {
2148
+ img.classList.remove('resizable', 'resizing');
2149
+ img.removeAttribute('draggable');
2150
+ });
2151
+
2152
+ let html = clone.innerHTML;
2153
+
2154
+ if (html === '<p><br></p>' || html === '<h1><br></h1>' || html.match(/^<[^>]+><br><\/[^>]+>$/)) {
2155
+ return '';
2156
+ }
2157
+
2158
+ return html;
2159
+ }
2160
+
2161
+ getText() {
2162
+ return this.editor.textContent || '';
2163
+ }
2164
+
2165
+ getJSON() {
2166
+ const nodes = [];
2167
+ const editorContent = this.editor.cloneNode(true);
2168
+
2169
+ editorContent.querySelectorAll('.editium-image-toolbar').forEach(el => el.remove());
2170
+
2171
+ Array.from(editorContent.childNodes).forEach(node => {
2172
+ const parsed = this.parseNodeToJSON(node);
2173
+ if (parsed) {
2174
+ nodes.push(parsed);
2175
+ }
2176
+ });
2177
+
2178
+ return nodes;
2179
+ }
2180
+
2181
+ parseNodeToJSON(node) {
2182
+
2183
+ if (node.nodeType === Node.TEXT_NODE) {
2184
+ const text = node.textContent;
2185
+ if (text.trim() === '') return null;
2186
+ return { text: text };
2187
+ }
2188
+
2189
+ if (node.nodeType === Node.ELEMENT_NODE) {
2190
+ const tagName = node.tagName.toLowerCase();
2191
+
2192
+ const typeMap = {
2193
+ 'p': 'paragraph',
2194
+ 'h1': 'heading-one',
2195
+ 'h2': 'heading-two',
2196
+ 'h3': 'heading-three',
2197
+ 'h4': 'heading-four',
2198
+ 'h5': 'heading-five',
2199
+ 'h6': 'heading-six',
2200
+ 'ul': 'bulleted-list',
2201
+ 'ol': 'numbered-list',
2202
+ 'li': 'list-item',
2203
+ 'blockquote': 'quote',
2204
+ 'pre': 'code-block',
2205
+ 'a': 'link',
2206
+ 'img': 'image',
2207
+ 'table': 'table',
2208
+ 'tr': 'table-row',
2209
+ 'td': 'table-cell',
2210
+ 'th': 'table-header',
2211
+ 'hr': 'horizontal-rule'
2212
+ };
2213
+
2214
+ const type = typeMap[tagName] || 'paragraph';
2215
+ const result = { type };
2216
+
2217
+ if (tagName === 'a') {
2218
+ result.url = node.getAttribute('href') || '';
2219
+ } else if (tagName === 'img') {
2220
+ const wrapper = node.closest('.editium-image-wrapper');
2221
+ return {
2222
+ type: 'image',
2223
+ url: node.getAttribute('src') || '',
2224
+ alt: node.getAttribute('alt') || '',
2225
+ width: node.style.width || node.getAttribute('width') || null,
2226
+ alignment: wrapper ? (wrapper.classList.contains('align-left') ? 'left' : wrapper.classList.contains('align-right') ? 'right' : 'center') : 'left'
2227
+ };
2228
+ } else if (tagName === 'hr') {
2229
+ return { type: 'horizontal-rule' };
2230
+ } else if (tagName === 'br') {
2231
+ return null;
2232
+ } else if (tagName === 'div' && node.classList.contains('editium-image-wrapper')) {
2233
+
2234
+ const img = node.querySelector('img');
2235
+ if (img) {
2236
+ return this.parseNodeToJSON(img);
2237
+ }
2238
+ return null;
2239
+ }
2240
+
2241
+ const children = [];
2242
+ Array.from(node.childNodes).forEach(child => {
2243
+ const parsed = this.parseInlineNode(child);
2244
+ if (parsed) {
2245
+ children.push(parsed);
2246
+ }
2247
+ });
2248
+
2249
+ if (children.length === 0) {
2250
+ children.push({ text: '' });
2251
+ }
2252
+
2253
+ result.children = children;
2254
+ return result;
2255
+ }
2256
+
2257
+ return null;
2258
+ }
2259
+
2260
+ parseInlineNode(node) {
2261
+
2262
+ if (node.nodeType === Node.TEXT_NODE) {
2263
+ const text = node.textContent;
2264
+ if (text === '') return null;
2265
+ return { text: text };
2266
+ }
2267
+
2268
+ if (node.nodeType === Node.ELEMENT_NODE) {
2269
+ const tagName = node.tagName.toLowerCase();
2270
+
2271
+ const marks = {};
2272
+
2273
+ if (tagName === 'strong' || tagName === 'b') {
2274
+ marks.bold = true;
2275
+ } else if (tagName === 'em' || tagName === 'i') {
2276
+ marks.italic = true;
2277
+ } else if (tagName === 'u') {
2278
+ marks.underline = true;
2279
+ } else if (tagName === 's' || tagName === 'strike') {
2280
+ marks.strikethrough = true;
2281
+ } else if (tagName === 'code') {
2282
+ marks.code = true;
2283
+ } else if (tagName === 'sub') {
2284
+ marks.subscript = true;
2285
+ } else if (tagName === 'sup') {
2286
+ marks.superscript = true;
2287
+ } else if (tagName === 'a') {
2288
+ return {
2289
+ type: 'link',
2290
+ url: node.getAttribute('href') || '',
2291
+ children: this.parseInlineChildren(node)
2292
+ };
2293
+ } else if (tagName === 'span') {
2294
+
2295
+ const style = node.getAttribute('style') || '';
2296
+ if (style.includes('color:')) {
2297
+ const colorMatch = style.match(/color:\s*([^;]+)/);
2298
+ if (colorMatch) {
2299
+ marks.color = colorMatch[1].trim();
2300
+ }
2301
+ }
2302
+ if (style.includes('background-color:')) {
2303
+ const bgMatch = style.match(/background-color:\s*([^;]+)/);
2304
+ if (bgMatch) {
2305
+ marks.backgroundColor = bgMatch[1].trim();
2306
+ }
2307
+ }
2308
+ } else if (tagName === 'br') {
2309
+ return { text: '\n' };
2310
+ } else if (tagName === 'img') {
2311
+
2312
+ return {
2313
+ type: 'image',
2314
+ url: node.getAttribute('src') || '',
2315
+ alt: node.getAttribute('alt') || '',
2316
+ children: [{ text: '' }]
2317
+ };
2318
+ }
2319
+
2320
+ const blockElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'table', 'tr', 'td', 'th'];
2321
+ if (blockElements.includes(tagName)) {
2322
+ return this.parseNodeToJSON(node);
2323
+ }
2324
+
2325
+ const children = this.parseInlineChildren(node);
2326
+
2327
+ return children.map(child => {
2328
+ if (child.text !== undefined) {
2329
+ return { ...child, ...marks };
2330
+ }
2331
+ return child;
2332
+ });
2333
+ }
2334
+
2335
+ return null;
2336
+ }
2337
+
2338
+ parseInlineChildren(node) {
2339
+ const children = [];
2340
+ Array.from(node.childNodes).forEach(child => {
2341
+ const parsed = this.parseInlineNode(child);
2342
+ if (parsed) {
2343
+ if (Array.isArray(parsed)) {
2344
+ children.push(...parsed);
2345
+ } else {
2346
+ children.push(parsed);
2347
+ }
2348
+ }
2349
+ });
2350
+
2351
+ if (children.length === 0) {
2352
+ return [{ text: '' }];
2353
+ }
2354
+
2355
+ return children;
2356
+ }
2357
+
2358
+ setContent(content) {
2359
+ if (typeof content === 'string') {
2360
+ this.editor.innerHTML = content;
2361
+ } else if (typeof content === 'object' && content.content) {
2362
+ this.editor.innerHTML = content.content;
2363
+ }
2364
+
2365
+ this.makeExistingImagesResizable();
2366
+
2367
+ this.saveState();
2368
+ this.triggerChange();
2369
+ }
2370
+
2371
+ makeExistingImagesResizable() {
2372
+ const images = this.editor.querySelectorAll('img');
2373
+ images.forEach(img => {
2374
+
2375
+ const parent = img.parentElement;
2376
+
2377
+ if (parent && parent.classList.contains('editium-image-wrapper')) {
2378
+
2379
+ if (!img.classList.contains('resizable')) {
2380
+ img.classList.add('resizable');
2381
+ img.draggable = false;
2382
+ this.makeImageResizable(img);
2383
+ }
2384
+
2385
+ if (!parent.querySelector('.editium-image-toolbar')) {
2386
+ const toolbar = this.createImageToolbar(parent, img);
2387
+ const container = img.parentElement;
2388
+ if (container) {
2389
+ container.appendChild(toolbar);
2390
+ }
2391
+ }
2392
+ } else {
2393
+
2394
+ const wrapper = document.createElement('div');
2395
+ wrapper.className = 'editium-image-wrapper align-left';
2396
+ wrapper.contentEditable = 'false';
2397
+ wrapper.style.textAlign = 'left';
2398
+
2399
+ const container = document.createElement('div');
2400
+ container.style.position = 'relative';
2401
+ container.style.display = 'inline-block';
2402
+
2403
+ img.parentNode.insertBefore(wrapper, img);
2404
+ img.classList.add('resizable');
2405
+ img.draggable = false;
2406
+
2407
+ if (!img.style.marginLeft && !img.style.marginRight) {
2408
+ img.style.marginLeft = '0';
2409
+ img.style.marginRight = 'auto';
2410
+ }
2411
+
2412
+ container.appendChild(img);
2413
+ const toolbar = this.createImageToolbar(wrapper, img);
2414
+ container.appendChild(toolbar);
2415
+ wrapper.appendChild(container);
2416
+
2417
+ this.makeImageResizable(img);
2418
+ }
2419
+ });
2420
+ }
2421
+
2422
+ makeExistingLinksNonEditable() {
2423
+ const links = this.editor.querySelectorAll('a');
2424
+ links.forEach(link => {
2425
+ link.contentEditable = 'false';
2426
+ });
2427
+ }
2428
+
2429
+ clear() {
2430
+ this.editor.innerHTML = '<p><br></p>';
2431
+ this.saveState();
2432
+ this.triggerChange();
2433
+ }
2434
+
2435
+ focus() {
2436
+ this.editor.focus();
2437
+ }
2438
+
2439
+ destroy() {
2440
+
2441
+ if (this.isFullscreen) {
2442
+ document.body.classList.remove('editium-fullscreen-active');
2443
+ }
2444
+ this.container.innerHTML = '';
2445
+ }
2446
+ }
2447
+
2448
+ if (typeof module !== 'undefined' && module.exports) {
2449
+ module.exports = Editium;
2450
+ }
2451
+ if (typeof window !== 'undefined') {
2452
+ window.Editium = Editium;
2453
+ }