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,3257 @@
1
+ /**
2
+ * Editium - Vanilla JavaScript Rich Text Editor (Bundled Version)
3
+ * Version: 1.0.0 | License: MIT
4
+ * Single file bundle - includes CSS and Font Awesome icons
5
+ */
6
+
7
+ (function() {
8
+ 'use strict';
9
+
10
+ function injectStyles() {
11
+ if (typeof document === 'undefined' || document.getElementById('editium-styles')) return;
12
+
13
+ const styleElement = document.createElement('style');
14
+ styleElement.id = 'editium-styles';
15
+ styleElement.textContent = `@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css");
16
+
17
+ /**
18
+ * Editium - Vanilla JavaScript Rich Text Editor Styles
19
+ * Matches the React version UI
20
+ */
21
+
22
+ /* Main container */
23
+ .editium-wrapper {
24
+ border: 1px solid #ccc;
25
+ border-radius: 4px;
26
+ background-color: #ffffff;
27
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
28
+ display: flex;
29
+ flex-direction: column;
30
+ overflow: hidden;
31
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
32
+ }
33
+
34
+ /* Fullscreen mode */
35
+ .editium-fullscreen {
36
+ position: fixed;
37
+ top: 0;
38
+ left: 0;
39
+ right: 0;
40
+ bottom: 0;
41
+ width: 100vw;
42
+ height: 100vh;
43
+ z-index: 9999;
44
+ border-radius: 0;
45
+ margin: 0;
46
+ }
47
+
48
+ /* Block body scroll when in fullscreen mode */
49
+ body.editium-fullscreen-active {
50
+ overflow: hidden;
51
+ }
52
+
53
+ /* Toolbar */
54
+ .editium-toolbar {
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ gap: 4px;
58
+ padding: 12px;
59
+ background-color: #f8f9fa;
60
+ border-bottom: 1px solid #ccc;
61
+ border-radius: 4px 4px 0 0;
62
+ align-items: center;
63
+ }
64
+
65
+ .editium-toolbar-button {
66
+ background-color: transparent;
67
+ border: none;
68
+ border-radius: 3px;
69
+ padding: 5px 8px;
70
+ cursor: pointer;
71
+ font-size: 14px;
72
+ font-weight: 400;
73
+ color: #222f3e;
74
+ transition: background-color 0.1s ease;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ gap: 4px;
79
+ min-width: 28px;
80
+ min-height: 28px;
81
+ line-height: 1;
82
+ white-space: nowrap;
83
+ }
84
+
85
+ .editium-toolbar-button i {
86
+ font-size: 14px;
87
+ width: 14px;
88
+ height: 14px;
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ }
93
+
94
+ .editium-toolbar-button:hover {
95
+ background-color: #e9ecef;
96
+ }
97
+
98
+ .editium-toolbar-button:active,
99
+ .editium-toolbar-button.active {
100
+ background-color: #dee2e6;
101
+ }
102
+
103
+ .editium-toolbar-button strong,
104
+ .editium-toolbar-button em,
105
+ .editium-toolbar-button u,
106
+ .editium-toolbar-button s {
107
+ pointer-events: none;
108
+ }
109
+
110
+ .editium-toolbar-separator {
111
+ width: 1px;
112
+ height: 24px;
113
+ background-color: #ccc;
114
+ margin: 0 4px;
115
+ align-self: center;
116
+ }
117
+
118
+ /* Dropdown */
119
+ .editium-dropdown {
120
+ position: relative;
121
+ display: inline-block;
122
+ }
123
+
124
+ .editium-dropdown-trigger {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ gap: 4px;
128
+ }
129
+
130
+ .editium-dropdown-trigger::after {
131
+ content: '▼';
132
+ font-size: 8px;
133
+ margin-left: 2px;
134
+ opacity: 0.6;
135
+ }
136
+
137
+ .editium-dropdown-menu {
138
+ display: none;
139
+ position: absolute;
140
+ top: 100%;
141
+ left: 0;
142
+ background-color: #ffffff;
143
+ border: 1px solid #ccc;
144
+ border-radius: 3px;
145
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
146
+ margin-top: 4px;
147
+ min-width: 180px;
148
+ z-index: 9999;
149
+ padding: 4px 0;
150
+ overflow: hidden;
151
+ }
152
+
153
+ .editium-dropdown-menu.show {
154
+ display: block;
155
+ }
156
+
157
+ .editium-dropdown-menu button {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 10px;
161
+ width: 100%;
162
+ padding: 6px 16px;
163
+ border: none;
164
+ background: none;
165
+ text-align: left;
166
+ cursor: pointer;
167
+ font-size: 14px;
168
+ font-weight: 400;
169
+ color: #222f3e;
170
+ transition: background-color 0.1s ease;
171
+ border-radius: 0;
172
+ }
173
+
174
+ .editium-dropdown-menu button i {
175
+ font-size: 14px;
176
+ width: 16px;
177
+ display: inline-flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+
182
+ .editium-dropdown-menu button:hover {
183
+ background-color: #e7f4ff;
184
+ }
185
+
186
+ .editium-dropdown-menu button.active {
187
+ background-color: #e7f4ff;
188
+ }
189
+
190
+ .editium-dropdown-menu button i {
191
+ width: 16px;
192
+ text-align: center;
193
+ margin-right: 4px;
194
+ }
195
+
196
+ .editium-dropdown-menu button span {
197
+ flex: 1;
198
+ text-align: left;
199
+ }
200
+
201
+ /* Editor container */
202
+ .editium-editor-container {
203
+ position: relative;
204
+ flex: 1;
205
+ display: flex;
206
+ flex-direction: column;
207
+ overflow: hidden;
208
+ }
209
+
210
+ /* Fullscreen editor container should allow scrolling */
211
+ .editium-fullscreen .editium-editor-container {
212
+ overflow: auto;
213
+ }
214
+
215
+ /* Editor area */
216
+ .editium-editor {
217
+ flex: 1;
218
+ padding: 16px;
219
+ /* Height, min-height, and max-height are set via inline styles from options */
220
+ outline: none;
221
+ font-size: 14px;
222
+ line-height: 1.6;
223
+ color: #000;
224
+ overflow-y: auto;
225
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
226
+ }
227
+
228
+ /* In fullscreen mode, ensure editor takes full available space */
229
+ .editium-fullscreen .editium-editor {
230
+ height: 100% !important;
231
+ min-height: unset !important;
232
+ max-height: unset !important;
233
+ }
234
+
235
+ .editium-editor:empty:before {
236
+ content: attr(data-placeholder);
237
+ color: #999;
238
+ pointer-events: none;
239
+ position: absolute;
240
+ }
241
+
242
+ /* Content styles - Match React version exactly */
243
+ .editium-editor h1,
244
+ .editium-editor h2,
245
+ .editium-editor h3,
246
+ .editium-editor h4,
247
+ .editium-editor h5,
248
+ .editium-editor h6 {
249
+ margin: 0;
250
+ font-weight: normal;
251
+ }
252
+
253
+ .editium-editor h1 {
254
+ font-size: 2em;
255
+ font-weight: bold;
256
+ }
257
+
258
+ .editium-editor h2 {
259
+ font-size: 1.5em;
260
+ font-weight: bold;
261
+ }
262
+
263
+ .editium-editor h3 {
264
+ font-size: 1.25em;
265
+ font-weight: bold;
266
+ }
267
+
268
+ .editium-editor h4 {
269
+ font-size: 1.1em;
270
+ font-weight: bold;
271
+ }
272
+
273
+ .editium-editor h5 {
274
+ font-size: 1em;
275
+ font-weight: bold;
276
+ }
277
+
278
+ .editium-editor h6 {
279
+ font-size: 0.9em;
280
+ font-weight: bold;
281
+ }
282
+
283
+ .editium-editor p {
284
+ margin: 0;
285
+ font-weight: normal;
286
+ }
287
+
288
+ .editium-editor blockquote {
289
+ margin: 1em 0;
290
+ padding-left: 1em;
291
+ border-left: 4px solid #dee2e6;
292
+ color: #6c757d;
293
+ font-style: italic;
294
+ }
295
+
296
+ .editium-editor code {
297
+ background-color: #f4f4f4;
298
+ padding: 2px 4px;
299
+ border-radius: 3px;
300
+ font-family: 'Courier New', Courier, monospace;
301
+ font-size: 0.9em;
302
+ }
303
+
304
+ .editium-editor pre {
305
+ background-color: #f4f4f4;
306
+ padding: 10px;
307
+ border-radius: 5px;
308
+ overflow-x: auto;
309
+ margin: 1em 0;
310
+ }
311
+
312
+ .editium-editor pre code {
313
+ background: none;
314
+ padding: 0;
315
+ }
316
+
317
+ .editium-editor ul,
318
+ .editium-editor ol {
319
+ margin: 1em 0;
320
+ padding-left: 2em;
321
+ }
322
+
323
+ .editium-editor li {
324
+ margin: 0.5em 0;
325
+ }
326
+
327
+ .editium-editor a {
328
+ color: #007bff;
329
+ text-decoration: underline;
330
+ cursor: pointer;
331
+ user-select: none;
332
+ -webkit-user-select: none;
333
+ -moz-user-select: none;
334
+ -ms-user-select: none;
335
+ position: relative;
336
+ padding: 2px 4px;
337
+ border-radius: 3px;
338
+ transition: all 0.15s ease;
339
+ display: inline-block;
340
+ }
341
+
342
+ .editium-editor a:hover {
343
+ color: #0056b3;
344
+ background-color: rgba(0, 123, 255, 0.1);
345
+ }
346
+
347
+ /* Visual hint for clickable links */
348
+ .editium-editor a::after {
349
+ content: '';
350
+ position: absolute;
351
+ bottom: 0px;
352
+ left: 0;
353
+ right: 0;
354
+ height: 2px;
355
+ background-color: transparent;
356
+ transition: background-color 0.2s ease;
357
+ }
358
+
359
+ .editium-editor a:hover::after {
360
+ background-color: rgba(0, 123, 255, 0.4);
361
+ }
362
+
363
+ .editium-editor img {
364
+ max-width: 100%;
365
+ height: auto;
366
+ display: block;
367
+ margin: 10px 0;
368
+ }
369
+
370
+ .editium-editor img.resizable {
371
+ cursor: nwse-resize;
372
+ border: 2px solid transparent;
373
+ transition: border-color 0.2s ease;
374
+ position: relative;
375
+ }
376
+
377
+ .editium-editor img.resizable:hover,
378
+ .editium-editor img.resizable:focus {
379
+ border-color: #007bff;
380
+ outline: none;
381
+ }
382
+
383
+ .editium-editor img.resizing {
384
+ border-color: #007bff;
385
+ opacity: 0.8;
386
+ }
387
+
388
+ /* Image wrapper for alignment */
389
+ .editium-image-wrapper {
390
+ margin: 10px 0;
391
+ display: flex;
392
+ position: relative;
393
+ }
394
+
395
+ .editium-image-wrapper.align-left {
396
+ justify-content: flex-start;
397
+ }
398
+
399
+ .editium-image-wrapper.align-center {
400
+ justify-content: center;
401
+ }
402
+
403
+ .editium-image-wrapper.align-right {
404
+ justify-content: flex-end;
405
+ }
406
+
407
+ /* Image toolbar that appears on hover/selection */
408
+ .editium-image-toolbar {
409
+ position: absolute;
410
+ top: 8px;
411
+ right: 8px;
412
+ display: none;
413
+ gap: 4px;
414
+ z-index: 10;
415
+ }
416
+
417
+ .editium-image-wrapper:hover .editium-image-toolbar,
418
+ .editium-image-wrapper.selected .editium-image-toolbar {
419
+ display: flex;
420
+ }
421
+
422
+ .editium-image-toolbar-group {
423
+ display: flex;
424
+ gap: 4px;
425
+ background-color: #ffffff;
426
+ border: 1px solid #d1d5db;
427
+ border-radius: 4px;
428
+ padding: 4px;
429
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
430
+ }
431
+
432
+ .editium-image-toolbar button {
433
+ padding: 4px 8px;
434
+ background-color: transparent;
435
+ border: none;
436
+ border-radius: 2px;
437
+ color: #374151;
438
+ font-size: 12px;
439
+ font-weight: 500;
440
+ cursor: pointer;
441
+ transition: all 0.15s ease;
442
+ min-width: 28px;
443
+ height: 28px;
444
+ display: flex;
445
+ align-items: center;
446
+ justify-content: center;
447
+ }
448
+
449
+ .editium-image-toolbar button:hover {
450
+ background-color: #f9fafb;
451
+ }
452
+
453
+ .editium-image-toolbar button.active {
454
+ background-color: #e0f2fe;
455
+ }
456
+
457
+ .editium-editor hr {
458
+ border: none;
459
+ border-top: 2px solid #dee2e6;
460
+ margin: 2em 0;
461
+ }
462
+
463
+ .editium-editor table {
464
+ border-collapse: collapse;
465
+ width: 100%;
466
+ margin: 1em 0;
467
+ }
468
+
469
+ .editium-editor table th,
470
+ .editium-editor table td {
471
+ border: 1px solid #dee2e6;
472
+ padding: 8px 12px;
473
+ text-align: left;
474
+ }
475
+
476
+ .editium-editor table th {
477
+ background-color: #f8f9fa;
478
+ font-weight: 600;
479
+ }
480
+
481
+ .editium-editor table tr:nth-child(even) {
482
+ background-color: #f8f9fa;
483
+ }
484
+
485
+ /* Search highlighting */
486
+ .editium-search-match {
487
+ background-color: #ffeb3b;
488
+ color: #000000;
489
+ padding: 2px 4px;
490
+ border-radius: 2px;
491
+ }
492
+
493
+ .editium-search-current {
494
+ background-color: #ff9800;
495
+ color: #ffffff;
496
+ padding: 2px 4px;
497
+ border-radius: 2px;
498
+ font-weight: 600;
499
+ }
500
+
501
+ /* Find & Replace Panel */
502
+ .editium-find-replace {
503
+ background-color: #f9fafb;
504
+ border: 1px solid #ccc;
505
+ border-top: none;
506
+ border-radius: 0 0 4px 4px;
507
+ padding: 16px;
508
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
509
+ }
510
+
511
+ .editium-find-replace-row {
512
+ display: flex;
513
+ gap: 8px;
514
+ margin-bottom: 8px;
515
+ align-items: center;
516
+ }
517
+
518
+ .editium-find-replace-row:last-child {
519
+ margin-bottom: 0;
520
+ }
521
+
522
+ .editium-find-input,
523
+ .editium-replace-input {
524
+ flex: 1;
525
+ padding: 6px 10px;
526
+ border: 1px solid #ced4da;
527
+ border-radius: 4px;
528
+ font-size: 14px;
529
+ outline: none;
530
+ }
531
+
532
+ .editium-find-input:focus,
533
+ .editium-replace-input:focus {
534
+ border-color: #80bdff;
535
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
536
+ }
537
+
538
+ .editium-find-replace button {
539
+ padding: 6px 12px;
540
+ border: 1px solid #ced4da;
541
+ border-radius: 4px;
542
+ background-color: #ffffff;
543
+ cursor: pointer;
544
+ font-size: 14px;
545
+ transition: background-color 0.15s ease;
546
+ }
547
+
548
+ .editium-find-replace button:hover {
549
+ background-color: #e9ecef;
550
+ }
551
+
552
+ .editium-match-count {
553
+ font-size: 14px;
554
+ color: #6c757d;
555
+ white-space: nowrap;
556
+ }
557
+
558
+ .editium-btn-prev,
559
+ .editium-btn-next {
560
+ min-width: 32px;
561
+ }
562
+
563
+ .editium-btn-close {
564
+ background-color: transparent;
565
+ border: none;
566
+ font-size: 18px;
567
+ color: #6c757d;
568
+ cursor: pointer;
569
+ padding: 0 8px;
570
+ }
571
+
572
+ .editium-btn-close:hover {
573
+ color: #dc3545;
574
+ }
575
+
576
+ /* Word count */
577
+ .editium-word-count {
578
+ padding: 8px 16px;
579
+ background-color: #f8f9fa;
580
+ border-top: 1px solid #ccc;
581
+ border-radius: 0 0 4px 4px;
582
+ font-size: 12px;
583
+ color: #666;
584
+ text-align: right;
585
+ display: flex;
586
+ justify-content: flex-end;
587
+ gap: 16px;
588
+ }
589
+
590
+ .editium-word-count strong {
591
+ color: #000;
592
+ font-weight: 500;
593
+ }
594
+
595
+ /* Modal */
596
+ .editium-modal {
597
+ position: fixed;
598
+ top: 0;
599
+ left: 0;
600
+ right: 0;
601
+ bottom: 0;
602
+ background-color: rgba(0, 0, 0, 0.5);
603
+ display: flex;
604
+ align-items: center;
605
+ justify-content: center;
606
+ z-index: 10000;
607
+ padding: 20px;
608
+ }
609
+
610
+ .editium-modal-content {
611
+ background-color: #ffffff;
612
+ border-radius: 8px;
613
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
614
+ max-width: 800px;
615
+ max-height: 90vh;
616
+ width: 100%;
617
+ display: flex;
618
+ flex-direction: column;
619
+ overflow: hidden;
620
+ }
621
+
622
+ .editium-modal-content.editium-preview {
623
+ max-width: 1200px;
624
+ }
625
+
626
+ .editium-modal-header {
627
+ padding: 16px 20px;
628
+ border-bottom: 1px solid #e0e0e0;
629
+ display: flex;
630
+ justify-content: space-between;
631
+ align-items: center;
632
+ }
633
+
634
+ .editium-modal-header h3 {
635
+ margin: 0;
636
+ font-size: 18px;
637
+ font-weight: 600;
638
+ color: #222f3e;
639
+ }
640
+
641
+ .editium-modal-close {
642
+ background: none;
643
+ border: none;
644
+ font-size: 24px;
645
+ color: #6c757d;
646
+ cursor: pointer;
647
+ padding: 0;
648
+ width: 32px;
649
+ height: 32px;
650
+ display: flex;
651
+ align-items: center;
652
+ justify-content: center;
653
+ border-radius: 4px;
654
+ transition: background-color 0.15s ease;
655
+ }
656
+
657
+ .editium-modal-close:hover {
658
+ background-color: #f8f9fa;
659
+ color: #dc3545;
660
+ }
661
+
662
+ .editium-modal-body {
663
+ flex: 1;
664
+ padding: 20px;
665
+ overflow: auto;
666
+ }
667
+
668
+ .editium-modal-body pre {
669
+ background-color: #f8f9fa;
670
+ padding: 15px;
671
+ border-radius: 4px;
672
+ overflow-x: auto;
673
+ margin: 0;
674
+ }
675
+
676
+ .editium-modal-body code {
677
+ font-family: 'Courier New', Courier, monospace;
678
+ font-size: 13px;
679
+ line-height: 1.5;
680
+ color: #222f3e;
681
+ }
682
+
683
+ .editium-modal-footer {
684
+ padding: 16px 20px;
685
+ border-top: 1px solid #e0e0e0;
686
+ display: flex;
687
+ justify-content: flex-end;
688
+ gap: 10px;
689
+ }
690
+
691
+ .editium-btn-copy {
692
+ padding: 8px 16px;
693
+ border: 1px solid #007bff;
694
+ border-radius: 4px;
695
+ background-color: #007bff;
696
+ color: #ffffff;
697
+ cursor: pointer;
698
+ font-size: 14px;
699
+ font-weight: 500;
700
+ transition: background-color 0.15s ease;
701
+ }
702
+
703
+ .editium-btn-copy:hover {
704
+ background-color: #0056b3;
705
+ border-color: #0056b3;
706
+ }
707
+
708
+ /* Word count */
709
+ .editium-word-count {
710
+ display: flex;
711
+ justify-content: space-between;
712
+ align-items: center;
713
+ padding: 8px 12px;
714
+ background-color: #f8f9fa;
715
+ border-top: 1px solid #ccc;
716
+ font-size: 12px;
717
+ color: #6c757d;
718
+ border-radius: 0 0 4px 4px;
719
+ }
720
+
721
+ /* When only branding is shown (no stats) */
722
+ .editium-word-count:has(.editium-word-count-branding:only-child) {
723
+ justify-content: flex-end;
724
+ }
725
+
726
+ .editium-word-count-stats {
727
+ text-align: left;
728
+ display: flex;
729
+ gap: 16px;
730
+ }
731
+
732
+ .editium-word-count-branding {
733
+ text-align: right;
734
+ color: #6c757d;
735
+ }
736
+
737
+ .editium-word-count-branding .editium-brand {
738
+ color: #4f88f7;
739
+ font-weight: 500;
740
+ text-decoration: none;
741
+ cursor: pointer;
742
+ transition: color 0.2s ease;
743
+ }
744
+
745
+ .editium-word-count-branding .editium-brand:hover {
746
+ color: #3b6fd9;
747
+ }
748
+
749
+ /* Responsive */
750
+ @media (max-width: 768px) {
751
+ .editium-toolbar {
752
+ padding: 8px;
753
+ gap: 2px;
754
+ }
755
+
756
+ .editium-toolbar-button {
757
+ padding: 5px 8px;
758
+ min-width: 28px;
759
+ min-height: 28px;
760
+ font-size: 13px;
761
+ }
762
+
763
+ .editium-editor {
764
+ padding: 15px;
765
+ font-size: 15px;
766
+ }
767
+
768
+ .editium-modal-content {
769
+ max-width: 95%;
770
+ }
771
+ }
772
+
773
+ /* Print styles */
774
+ @media print {
775
+ .editium-toolbar,
776
+ .editium-word-count,
777
+ .editium-find-replace {
778
+ display: none;
779
+ }
780
+
781
+ .editium-wrapper {
782
+ border: none;
783
+ }
784
+
785
+ .editium-editor {
786
+ padding: 0;
787
+ }
788
+ }
789
+ `;
790
+ document.head.appendChild(styleElement);
791
+ }
792
+
793
+ if (document.readyState === 'loading') {
794
+ document.addEventListener('DOMContentLoaded', injectStyles);
795
+ } else {
796
+ injectStyles();
797
+ }
798
+
799
+ class Editium {
800
+ constructor(options = {}) {
801
+ this.container = options.container;
802
+ this.placeholder = options.placeholder || 'Start typing...';
803
+ this.toolbar = options.toolbar || ['bold', 'italic', 'underline', 'heading-one', 'heading-two', 'bulleted-list', 'numbered-list', 'link'];
804
+ this.onChange = options.onChange || (() => {});
805
+ this.readOnly = options.readOnly || false;
806
+ this.showWordCount = options.showWordCount || false;
807
+ this.className = options.className || '';
808
+ this.onImageUpload = options.onImageUpload || null;
809
+
810
+ this.height = options.height || '200px';
811
+ this.minHeight = options.minHeight || '150px';
812
+ this.maxHeight = options.maxHeight || '250px';
813
+
814
+ this.isFullscreen = false;
815
+ this.searchQuery = '';
816
+ this.searchMatches = [];
817
+ this.currentMatchIndex = 0;
818
+ this.findReplacePanel = null;
819
+ this.history = [];
820
+ this.historyIndex = -1;
821
+ this.maxHistory = 50;
822
+ this.openDropdown = null;
823
+ this.linkPopup = null;
824
+ this.selectedLink = null;
825
+
826
+ if (!this.container) {
827
+ throw new Error('Container element is required');
828
+ }
829
+
830
+ this.init();
831
+ }
832
+
833
+ init() {
834
+ this.createEditor();
835
+ this.attachEventListeners();
836
+
837
+ if (this.editor.innerHTML.trim() === '') this.editor.innerHTML = '<p><br></p>';
838
+
839
+ this.makeExistingImagesResizable();
840
+ this.makeExistingLinksNonEditable();
841
+ this.saveState();
842
+ }
843
+
844
+ createEditor() {
845
+ this.container.innerHTML = '';
846
+
847
+ this.wrapper = document.createElement('div');
848
+ this.wrapper.className = `editium-wrapper ${this.className}`;
849
+ if (this.isFullscreen) this.wrapper.classList.add('editium-fullscreen');
850
+
851
+ const toolbarItems = this.toolbar === 'all' ? this.getAllToolbarItems() : this.toolbar;
852
+
853
+ if (toolbarItems.length > 0) {
854
+ this.toolbarElement = this.createToolbar(toolbarItems);
855
+ this.wrapper.appendChild(this.toolbarElement);
856
+ }
857
+
858
+ this.editorContainer = document.createElement('div');
859
+ this.editorContainer.className = 'editium-editor-container';
860
+
861
+ this.editor = document.createElement('div');
862
+ this.editor.className = 'editium-editor';
863
+ this.editor.contentEditable = !this.readOnly;
864
+ this.editor.setAttribute('data-placeholder', this.placeholder);
865
+
866
+ if (!this.isFullscreen) {
867
+ this.editor.style.height = typeof this.height === 'number' ? `${this.height}px` : this.height;
868
+ this.editor.style.minHeight = typeof this.minHeight === 'number' ? `${this.minHeight}px` : this.minHeight;
869
+ this.editor.style.maxHeight = typeof this.maxHeight === 'number' ? `${this.maxHeight}px` : this.maxHeight;
870
+ } else {
871
+ this.editor.style.height = 'auto';
872
+ this.editor.style.minHeight = 'auto';
873
+ this.editor.style.maxHeight = 'none';
874
+ }
875
+
876
+ this.editorContainer.appendChild(this.editor);
877
+ this.wrapper.appendChild(this.editorContainer);
878
+
879
+ this.wordCountElement = document.createElement('div');
880
+ this.wordCountElement.className = 'editium-word-count';
881
+ this.wrapper.appendChild(this.wordCountElement);
882
+ this.updateWordCount();
883
+
884
+ this.container.appendChild(this.wrapper);
885
+ }
886
+
887
+ getAllToolbarItems() {
888
+ return [
889
+ 'paragraph', 'heading-one', 'heading-two', 'heading-three', 'heading-four',
890
+ 'heading-five', 'heading-six',
891
+ 'separator',
892
+ 'bold', 'italic', 'underline', 'strikethrough',
893
+ 'separator',
894
+ 'superscript', 'subscript', 'code',
895
+ 'separator',
896
+ 'left', 'center', 'right', 'justify',
897
+ 'separator',
898
+ 'text-color', 'bg-color',
899
+ 'separator',
900
+ 'blockquote', 'code-block',
901
+ 'separator',
902
+ 'bulleted-list', 'numbered-list', 'indent', 'outdent',
903
+ 'separator',
904
+ 'link', 'image', 'table', 'horizontal-rule', 'undo', 'redo',
905
+ 'separator',
906
+ 'find-replace', 'fullscreen', 'view-output'
907
+ ];
908
+ }
909
+
910
+ createToolbar(items) {
911
+ const toolbar = document.createElement('div');
912
+ toolbar.className = 'editium-toolbar';
913
+
914
+ const groups = {
915
+ paragraph: ['paragraph', 'heading-one', 'heading-two', 'heading-three', 'heading-four', 'heading-five', 'heading-six'],
916
+ format: ['bold', 'italic', 'underline', 'strikethrough', 'code', 'superscript', 'subscript'],
917
+ align: ['left', 'center', 'right', 'justify'],
918
+ color: ['text-color', 'bg-color'],
919
+ blocks: ['blockquote', 'code-block'],
920
+ lists: ['bulleted-list', 'numbered-list', 'indent', 'outdent'],
921
+ insert: ['link', 'image', 'table', 'horizontal-rule'],
922
+ edit: ['undo', 'redo'],
923
+ view: ['preview', 'view-html', 'view-json']
924
+ };
925
+
926
+ if (this.toolbar === 'all') {
927
+ toolbar.appendChild(this.createBlockFormatDropdown());
928
+ toolbar.appendChild(this.createGroupDropdown('Format', groups.format));
929
+ toolbar.appendChild(this.createAlignmentDropdown());
930
+ toolbar.appendChild(this.createGroupDropdown('Color', groups.color));
931
+ toolbar.appendChild(this.createGroupDropdown('Blocks', groups.blocks));
932
+ toolbar.appendChild(this.createGroupDropdown('Lists', groups.lists));
933
+ toolbar.appendChild(this.createGroupDropdown('Insert', groups.insert));
934
+ toolbar.appendChild(this.createGroupDropdown('Edit', groups.edit));
935
+ toolbar.appendChild(this.createGroupDropdown('View', groups.view));
936
+
937
+ const spacer = document.createElement('div');
938
+ spacer.style.flex = '1';
939
+ toolbar.appendChild(spacer);
940
+
941
+ const findButton = this.createToolbarButton('find-replace');
942
+ const fullscreenButton = this.createToolbarButton('fullscreen');
943
+ if (findButton) toolbar.appendChild(findButton);
944
+ if (fullscreenButton) toolbar.appendChild(fullscreenButton);
945
+ } else {
946
+ const blockFormats = groups.paragraph;
947
+ const alignments = groups.align;
948
+ let processedGroups = { block: false, align: false };
949
+
950
+ for (let i = 0; i < items.length; i++) {
951
+ const item = items[i];
952
+
953
+ if (item === 'separator') {
954
+ if (i > 0 && items[i-1] !== 'separator') {
955
+ const separator = document.createElement('div');
956
+ separator.className = 'editium-toolbar-separator';
957
+ toolbar.appendChild(separator);
958
+ }
959
+ } else if (blockFormats.includes(item) && !processedGroups.block) {
960
+ toolbar.appendChild(this.createBlockFormatDropdown());
961
+ processedGroups.block = true;
962
+ } else if (alignments.includes(item) && !processedGroups.align) {
963
+ toolbar.appendChild(this.createAlignmentDropdown());
964
+ processedGroups.align = true;
965
+ } else if (!blockFormats.includes(item) && !alignments.includes(item)) {
966
+ const button = this.createToolbarButton(item);
967
+ if (button) {
968
+ toolbar.appendChild(button);
969
+ }
970
+ }
971
+ }
972
+ }
973
+
974
+ return toolbar;
975
+ }
976
+
977
+ createGroupDropdown(label, items) {
978
+ const dropdown = document.createElement('div');
979
+ dropdown.className = 'editium-dropdown';
980
+
981
+ const trigger = document.createElement('button');
982
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
983
+ trigger.type = 'button';
984
+ trigger.textContent = label;
985
+ trigger.title = label;
986
+
987
+ const menu = document.createElement('div');
988
+ menu.className = 'editium-dropdown-menu';
989
+
990
+ items.forEach(itemType => {
991
+ const config = this.getButtonConfig(itemType);
992
+ if (!config) return;
993
+
994
+ const item = document.createElement('button');
995
+ item.type = 'button';
996
+ item.innerHTML = `${config.icon} <span>${config.title}</span>`;
997
+ item.onclick = (e) => {
998
+ e.preventDefault();
999
+ config.action();
1000
+ this.closeDropdown();
1001
+ };
1002
+ menu.appendChild(item);
1003
+ });
1004
+
1005
+ trigger.onclick = (e) => {
1006
+ e.preventDefault();
1007
+ this.toggleDropdown(menu);
1008
+ };
1009
+
1010
+ dropdown.appendChild(trigger);
1011
+ dropdown.appendChild(menu);
1012
+
1013
+ return dropdown;
1014
+ }
1015
+
1016
+ createBlockFormatDropdown() {
1017
+ const dropdown = document.createElement('div');
1018
+ dropdown.className = 'editium-dropdown';
1019
+
1020
+ const trigger = document.createElement('button');
1021
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
1022
+ trigger.type = 'button';
1023
+ trigger.textContent = 'Paragraph';
1024
+ trigger.title = 'Block Format';
1025
+
1026
+ const menu = document.createElement('div');
1027
+ menu.className = 'editium-dropdown-menu';
1028
+
1029
+ const formats = [
1030
+ { label: 'Paragraph', value: 'p' },
1031
+ { label: 'Heading 1', value: 'h1' },
1032
+ { label: 'Heading 2', value: 'h2' },
1033
+ { label: 'Heading 3', value: 'h3' },
1034
+ { label: 'Heading 4', value: 'h4' },
1035
+ { label: 'Heading 5', value: 'h5' },
1036
+ { label: 'Heading 6', value: 'h6' },
1037
+ ];
1038
+
1039
+ formats.forEach(format => {
1040
+ const item = document.createElement('button');
1041
+ item.type = 'button';
1042
+ item.textContent = format.label;
1043
+ item.onclick = (e) => {
1044
+ e.preventDefault();
1045
+ this.execCommand('formatBlock', `<${format.value}>`);
1046
+ trigger.textContent = format.label;
1047
+ this.closeDropdown();
1048
+ };
1049
+ menu.appendChild(item);
1050
+ });
1051
+
1052
+ trigger.onclick = (e) => {
1053
+ e.preventDefault();
1054
+ this.toggleDropdown(menu);
1055
+ };
1056
+
1057
+ dropdown.appendChild(trigger);
1058
+ dropdown.appendChild(menu);
1059
+
1060
+ return dropdown;
1061
+ }
1062
+
1063
+ createAlignmentDropdown() {
1064
+ const dropdown = document.createElement('div');
1065
+ dropdown.className = 'editium-dropdown';
1066
+
1067
+ const trigger = document.createElement('button');
1068
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
1069
+ trigger.type = 'button';
1070
+ trigger.textContent = 'Align';
1071
+ trigger.title = 'Text Alignment';
1072
+
1073
+ const menu = document.createElement('div');
1074
+ menu.className = 'editium-dropdown-menu';
1075
+
1076
+ const alignments = [
1077
+ { label: 'Align Left', icon: '<i class="fa-solid fa-align-left"></i>', command: 'justifyLeft' },
1078
+ { label: 'Align Center', icon: '<i class="fa-solid fa-align-center"></i>', command: 'justifyCenter' },
1079
+ { label: 'Align Right', icon: '<i class="fa-solid fa-align-right"></i>', command: 'justifyRight' },
1080
+ { label: 'Justify', icon: '<i class="fa-solid fa-align-justify"></i>', command: 'justifyFull' },
1081
+ ];
1082
+
1083
+ alignments.forEach(align => {
1084
+ const item = document.createElement('button');
1085
+ item.type = 'button';
1086
+ item.innerHTML = `${align.icon} <span>${align.label}</span>`;
1087
+ item.onclick = (e) => {
1088
+ e.preventDefault();
1089
+ this.execCommand(align.command);
1090
+ this.closeDropdown();
1091
+ };
1092
+ menu.appendChild(item);
1093
+ });
1094
+
1095
+ trigger.onclick = (e) => {
1096
+ e.preventDefault();
1097
+ this.toggleDropdown(menu);
1098
+ };
1099
+
1100
+ dropdown.appendChild(trigger);
1101
+ dropdown.appendChild(menu);
1102
+
1103
+ return dropdown;
1104
+ }
1105
+
1106
+ toggleDropdown(menu) {
1107
+ if (this.openDropdown === menu) {
1108
+ this.closeDropdown();
1109
+ } else {
1110
+ this.closeDropdown();
1111
+ menu.classList.add('show');
1112
+ this.openDropdown = menu;
1113
+ }
1114
+ }
1115
+
1116
+ closeDropdown() {
1117
+ if (this.openDropdown) {
1118
+ this.openDropdown.classList.remove('show');
1119
+ this.openDropdown = null;
1120
+ }
1121
+ }
1122
+
1123
+ createToolbarButton(type) {
1124
+ const config = this.getButtonConfig(type);
1125
+ if (!config) return null;
1126
+
1127
+ if (config.dropdown) {
1128
+ return this.createDropdownButton(type, config);
1129
+ }
1130
+
1131
+ const button = document.createElement('button');
1132
+ button.className = 'editium-toolbar-button';
1133
+ button.type = 'button';
1134
+ button.setAttribute('data-command', type);
1135
+ button.innerHTML = config.icon;
1136
+ button.title = config.title;
1137
+
1138
+ button.onclick = (e) => {
1139
+ e.preventDefault();
1140
+ config.action();
1141
+ this.closeDropdown();
1142
+ };
1143
+
1144
+ return button;
1145
+ }
1146
+
1147
+ createDropdownButton(type, config) {
1148
+ const dropdown = document.createElement('div');
1149
+ dropdown.className = 'editium-dropdown';
1150
+
1151
+ const trigger = document.createElement('button');
1152
+ trigger.className = 'editium-toolbar-button editium-dropdown-trigger';
1153
+ trigger.type = 'button';
1154
+ trigger.innerHTML = config.icon;
1155
+ trigger.title = config.title;
1156
+
1157
+ const menu = document.createElement('div');
1158
+ menu.className = 'editium-dropdown-menu';
1159
+
1160
+ config.dropdown.forEach(item => {
1161
+ const menuItem = document.createElement('button');
1162
+ menuItem.type = 'button';
1163
+ menuItem.textContent = item.label;
1164
+ menuItem.onclick = (e) => {
1165
+ e.preventDefault();
1166
+ item.action();
1167
+ this.closeDropdown();
1168
+ };
1169
+ menu.appendChild(menuItem);
1170
+ });
1171
+
1172
+ trigger.onclick = (e) => {
1173
+ e.preventDefault();
1174
+ this.toggleDropdown(menu);
1175
+ };
1176
+
1177
+ dropdown.appendChild(trigger);
1178
+ dropdown.appendChild(menu);
1179
+
1180
+ return dropdown;
1181
+ }
1182
+
1183
+ getButtonConfig(type) {
1184
+ const configs = {
1185
+ 'bold': { icon: '<i class="fa-solid fa-bold"></i>', title: 'Bold (Ctrl+B)', action: () => this.execCommand('bold') },
1186
+ 'italic': { icon: '<i class="fa-solid fa-italic"></i>', title: 'Italic (Ctrl+I)', action: () => this.execCommand('italic') },
1187
+ 'underline': { icon: '<i class="fa-solid fa-underline"></i>', title: 'Underline (Ctrl+U)', action: () => this.execCommand('underline') },
1188
+ 'strikethrough': { icon: '<i class="fa-solid fa-strikethrough"></i>', title: 'Strikethrough', action: () => this.execCommand('strikeThrough') },
1189
+ 'superscript': { icon: '<i class="fa-solid fa-superscript"></i>', title: 'Superscript', action: () => this.execCommand('superscript') },
1190
+ 'subscript': { icon: '<i class="fa-solid fa-subscript"></i>', title: 'Subscript', action: () => this.execCommand('subscript') },
1191
+ 'code': { icon: '<i class="fa-solid fa-code"></i>', title: 'Code', action: () => this.toggleInlineCode() },
1192
+ 'left': { icon: '<i class="fa-solid fa-align-left"></i>', title: 'Align Left', action: () => this.execCommand('justifyLeft') },
1193
+ 'center': { icon: '<i class="fa-solid fa-align-center"></i>', title: 'Align Center', action: () => this.execCommand('justifyCenter') },
1194
+ 'right': { icon: '<i class="fa-solid fa-align-right"></i>', title: 'Align Right', action: () => this.execCommand('justifyRight') },
1195
+ 'justify': { icon: '<i class="fa-solid fa-align-justify"></i>', title: 'Justify', action: () => this.execCommand('justifyFull') },
1196
+ 'bulleted-list': { icon: '<i class="fa-solid fa-list-ul"></i>', title: 'Bulleted List', action: () => this.execCommand('insertUnorderedList') },
1197
+ 'numbered-list': { icon: '<i class="fa-solid fa-list-ol"></i>', title: 'Numbered List', action: () => this.execCommand('insertOrderedList') },
1198
+ 'indent': { icon: '<i class="fa-solid fa-indent"></i>', title: 'Indent', action: () => this.execCommand('indent') },
1199
+ 'outdent': { icon: '<i class="fa-solid fa-outdent"></i>', title: 'Outdent', action: () => this.execCommand('outdent') },
1200
+ 'link': { icon: '<i class="fa-solid fa-link"></i>', title: 'Insert Link', action: () => this.showLinkModal() },
1201
+ 'image': { icon: '<i class="fa-solid fa-image"></i>', title: 'Insert Image', action: () => this.showImageModal() },
1202
+ 'blockquote': { icon: '<i class="fa-solid fa-quote-left"></i>', title: 'Blockquote', action: () => this.execCommand('formatBlock', '<blockquote>') },
1203
+ 'code-block': { icon: '<i class="fa-solid fa-file-code"></i>', title: 'Code Block', action: () => this.insertCodeBlock() },
1204
+ 'horizontal-rule': { icon: '<i class="fa-solid fa-minus"></i>', title: 'Horizontal Rule', action: () => this.execCommand('insertHorizontalRule') },
1205
+ 'table': { icon: '<i class="fa-solid fa-table"></i>', title: 'Insert Table', action: () => this.showTableModal() },
1206
+ 'text-color': { icon: '<i class="fa-solid fa-palette"></i>', title: 'Text Color', action: () => this.showColorPicker('foreColor') },
1207
+ 'bg-color': { icon: '<i class="fa-solid fa-fill-drip"></i>', title: 'Background Color', action: () => this.showColorPicker('hiliteColor') },
1208
+ 'undo': { icon: '<i class="fa-solid fa-rotate-left"></i>', title: 'Undo (Ctrl+Z)', action: () => this.undo() },
1209
+ 'redo': { icon: '<i class="fa-solid fa-rotate-right"></i>', title: 'Redo (Ctrl+Y)', action: () => this.redo() },
1210
+ 'preview': { icon: '<i class="fa-solid fa-eye"></i>', title: 'Preview', action: () => this.viewOutput('preview') },
1211
+ 'view-html': { icon: '<i class="fa-solid fa-code"></i>', title: 'View HTML', action: () => this.viewOutput('html') },
1212
+ 'view-json': { icon: '<i class="fa-solid fa-brackets-curly"></i>', title: 'View JSON', action: () => this.viewOutput('json') },
1213
+ 'find-replace': { icon: '<i class="fa-solid fa-magnifying-glass"></i>', title: 'Find & Replace', action: () => this.toggleFindReplace() },
1214
+ 'fullscreen': { icon: '<i class="fa-solid fa-expand"></i>', title: 'Toggle Fullscreen (F11)', action: () => this.toggleFullscreen() }
1215
+ };
1216
+
1217
+ return configs[type];
1218
+ }
1219
+
1220
+ execCommand(command, value = null) {
1221
+ document.execCommand(command, false, value);
1222
+ this.editor.focus();
1223
+ this.saveState();
1224
+ this.triggerChange();
1225
+ }
1226
+
1227
+ toggleInlineCode() {
1228
+ const selection = window.getSelection();
1229
+ if (!selection.rangeCount) return;
1230
+
1231
+ const range = selection.getRangeAt(0);
1232
+ const selectedText = range.toString();
1233
+
1234
+ if (selectedText) {
1235
+ const code = document.createElement('code');
1236
+ code.style.backgroundColor = '#f4f4f4';
1237
+ code.style.padding = '2px 4px';
1238
+ code.style.borderRadius = '3px';
1239
+ code.style.fontFamily = 'monospace';
1240
+ code.textContent = selectedText;
1241
+
1242
+ range.deleteContents();
1243
+ range.insertNode(code);
1244
+
1245
+ this.saveState();
1246
+ this.triggerChange();
1247
+ }
1248
+ }
1249
+
1250
+ showLinkModal() {
1251
+ this.editor.focus();
1252
+ const selection = window.getSelection();
1253
+ const selectedText = selection.toString();
1254
+ let savedRange = null;
1255
+
1256
+ if (selection.rangeCount > 0) savedRange = selection.getRangeAt(0).cloneRange();
1257
+
1258
+ const modal = this.createModal('Insert Link', `
1259
+ <div style="margin-bottom: 16px;">
1260
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Link Text:</label>
1261
+ <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;">
1262
+ </div>
1263
+ <div style="margin-bottom: 16px;">
1264
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">URL: *</label>
1265
+ <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;">
1266
+ </div>
1267
+ <div style="margin-bottom: 16px;">
1268
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Title (optional):</label>
1269
+ <input type="text" id="link-title" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
1270
+ </div>
1271
+ <div style="margin-bottom: 16px;">
1272
+ <label style="display: inline-flex; align-items: center; font-size: 14px; color: #333; cursor: pointer;">
1273
+ <input type="checkbox" id="link-target" style="margin-right: 8px;"> Open in new tab
1274
+ </label>
1275
+ </div>
1276
+ `, () => {
1277
+ const url = document.getElementById('link-url').value.trim();
1278
+ const text = document.getElementById('link-text').value.trim();
1279
+ const title = document.getElementById('link-title').value.trim();
1280
+ const target = document.getElementById('link-target').checked;
1281
+
1282
+ if (!url) {
1283
+ alert('URL is required');
1284
+ return false;
1285
+ }
1286
+
1287
+ try {
1288
+ new URL(url);
1289
+ } catch {
1290
+ alert('Please enter a valid URL');
1291
+ return false;
1292
+ }
1293
+
1294
+ if (savedRange) {
1295
+ this.editor.focus();
1296
+ const sel = window.getSelection();
1297
+ sel.removeAllRanges();
1298
+ sel.addRange(savedRange);
1299
+ }
1300
+
1301
+ const link = document.createElement('a');
1302
+ link.href = url;
1303
+ link.textContent = text || url;
1304
+ link.contentEditable = 'false';
1305
+ if (title) link.title = title;
1306
+ if (target) link.target = '_blank';
1307
+
1308
+ const sel = window.getSelection();
1309
+ if (sel.rangeCount) {
1310
+ const range = sel.getRangeAt(0);
1311
+ range.deleteContents();
1312
+ range.insertNode(link);
1313
+
1314
+ const space = document.createTextNode('\u00A0');
1315
+ range.setStartAfter(link);
1316
+ range.insertNode(space);
1317
+
1318
+ range.setStartAfter(space);
1319
+ range.setEndAfter(space);
1320
+ sel.removeAllRanges();
1321
+ sel.addRange(range);
1322
+ }
1323
+
1324
+ this.saveState();
1325
+ this.triggerChange();
1326
+ return true;
1327
+ });
1328
+
1329
+ document.body.appendChild(modal);
1330
+ document.getElementById('link-url').focus();
1331
+ }
1332
+
1333
+ showLinkPopup(linkElement) {
1334
+ this.selectedLink = linkElement;
1335
+
1336
+ this.closeLinkPopup();
1337
+
1338
+ const rect = linkElement.getBoundingClientRect();
1339
+
1340
+ this.linkPopup = document.createElement('div');
1341
+ this.linkPopup.className = 'editium-link-popup';
1342
+ this.linkPopup.style.cssText = `
1343
+ position: fixed;
1344
+ top: ${rect.bottom + window.scrollY + 5}px;
1345
+ left: ${rect.left + window.scrollX}px;
1346
+ background-color: #ffffff;
1347
+ border: 1px solid #d1d5db;
1348
+ border-radius: 8px;
1349
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
1350
+ min-width: 200px;
1351
+ overflow: hidden;
1352
+ z-index: 10000;
1353
+ `;
1354
+
1355
+ this.linkPopup.innerHTML = `
1356
+ <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; background-color: #f9fafb;">
1357
+ <div style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">Link URL:</div>
1358
+ <div style="font-size: 13px; color: #111827; word-break: break-all; font-family: monospace;">
1359
+ ${this.escapeHtml(linkElement.href)}
1360
+ </div>
1361
+ </div>
1362
+ <button class="editium-link-popup-btn editium-link-open" style="
1363
+ width: 100%;
1364
+ padding: 12px 16px;
1365
+ border: none;
1366
+ background-color: transparent;
1367
+ color: #374151;
1368
+ font-size: 14px;
1369
+ text-align: left;
1370
+ cursor: pointer;
1371
+ display: flex;
1372
+ align-items: center;
1373
+ gap: 10px;
1374
+ font-weight: 500;
1375
+ ">
1376
+ <svg style="width: 16px; height: 16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1377
+ <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" />
1378
+ </svg>
1379
+ Open Link
1380
+ </button>
1381
+ <button class="editium-link-popup-btn editium-link-edit" style="
1382
+ width: 100%;
1383
+ padding: 12px 16px;
1384
+ border: none;
1385
+ background-color: transparent;
1386
+ color: #374151;
1387
+ font-size: 14px;
1388
+ text-align: left;
1389
+ cursor: pointer;
1390
+ display: flex;
1391
+ align-items: center;
1392
+ gap: 10px;
1393
+ font-weight: 500;
1394
+ ">
1395
+ <svg style="width: 16px; height: 16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1396
+ <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" />
1397
+ </svg>
1398
+ Edit Link
1399
+ </button>
1400
+ <button class="editium-link-popup-btn editium-link-remove" style="
1401
+ width: 100%;
1402
+ padding: 12px 16px;
1403
+ border: none;
1404
+ border-top: 1px solid #e5e7eb;
1405
+ background-color: transparent;
1406
+ color: #ef4444;
1407
+ font-size: 14px;
1408
+ text-align: left;
1409
+ cursor: pointer;
1410
+ display: flex;
1411
+ align-items: center;
1412
+ gap: 10px;
1413
+ font-weight: 500;
1414
+ ">
1415
+ <svg style="width: 16px; height: 16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1416
+ <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" />
1417
+ </svg>
1418
+ Remove Link
1419
+ </button>
1420
+ `;
1421
+
1422
+ const buttons = this.linkPopup.querySelectorAll('.editium-link-popup-btn');
1423
+ buttons.forEach(btn => {
1424
+ btn.addEventListener('mouseenter', () => {
1425
+ if (btn.classList.contains('editium-link-remove')) {
1426
+ btn.style.backgroundColor = '#fef2f2';
1427
+ } else {
1428
+ btn.style.backgroundColor = '#f3f4f6';
1429
+ }
1430
+ });
1431
+ btn.addEventListener('mouseleave', () => {
1432
+ btn.style.backgroundColor = 'transparent';
1433
+ });
1434
+ });
1435
+
1436
+ this.linkPopup.querySelector('.editium-link-open').addEventListener('click', () => {
1437
+ window.open(linkElement.href, linkElement.target || '_self');
1438
+ this.closeLinkPopup();
1439
+ });
1440
+
1441
+ this.linkPopup.querySelector('.editium-link-edit').addEventListener('click', () => {
1442
+ this.closeLinkPopup();
1443
+ this.editLink(linkElement);
1444
+ });
1445
+
1446
+ this.linkPopup.querySelector('.editium-link-remove').addEventListener('click', () => {
1447
+ this.removeLink(linkElement);
1448
+ this.closeLinkPopup();
1449
+ });
1450
+
1451
+ document.body.appendChild(this.linkPopup);
1452
+ }
1453
+
1454
+ closeLinkPopup() {
1455
+ if (this.linkPopup) {
1456
+ this.linkPopup.remove();
1457
+ this.linkPopup = null;
1458
+ }
1459
+ this.selectedLink = null;
1460
+ }
1461
+
1462
+ editLink(linkElement) {
1463
+
1464
+ const savedLinkElement = linkElement;
1465
+
1466
+ const modal = this.createModal('Edit Link', `
1467
+ <div style="margin-bottom: 16px;">
1468
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Link Text:</label>
1469
+ <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;">
1470
+ </div>
1471
+ <div style="margin-bottom: 16px;">
1472
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">URL: *</label>
1473
+ <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;">
1474
+ </div>
1475
+ <div style="margin-bottom: 16px;">
1476
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Title (optional):</label>
1477
+ <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;">
1478
+ </div>
1479
+ <div style="margin-bottom: 16px;">
1480
+ <label style="display: inline-flex; align-items: center; font-size: 14px; color: #333; cursor: pointer;">
1481
+ <input type="checkbox" id="link-target" ${linkElement.target === '_blank' ? 'checked' : ''} style="margin-right: 8px;"> Open in new tab
1482
+ </label>
1483
+ </div>
1484
+ `, () => {
1485
+ const url = document.getElementById('link-url').value.trim();
1486
+ const text = document.getElementById('link-text').value.trim();
1487
+ const title = document.getElementById('link-title').value.trim();
1488
+ const target = document.getElementById('link-target').checked;
1489
+
1490
+ if (!url) {
1491
+ alert('URL is required');
1492
+ return false;
1493
+ }
1494
+
1495
+ try {
1496
+ new URL(url);
1497
+ } catch {
1498
+ alert('Please enter a valid URL');
1499
+ return false;
1500
+ }
1501
+
1502
+ savedLinkElement.href = url;
1503
+ savedLinkElement.textContent = text || url;
1504
+ savedLinkElement.title = title;
1505
+ savedLinkElement.target = target ? '_blank' : '';
1506
+ savedLinkElement.contentEditable = 'false';
1507
+
1508
+ this.saveState();
1509
+ this.triggerChange();
1510
+ return true;
1511
+ });
1512
+
1513
+ document.body.appendChild(modal);
1514
+ document.getElementById('link-url').focus();
1515
+ }
1516
+
1517
+ removeLink(linkElement) {
1518
+
1519
+ const textNode = document.createTextNode(linkElement.textContent);
1520
+ linkElement.parentNode.replaceChild(textNode, linkElement);
1521
+
1522
+ this.saveState();
1523
+ this.triggerChange();
1524
+ }
1525
+
1526
+ showImageModal() {
1527
+
1528
+ this.editor.focus();
1529
+ const selection = window.getSelection();
1530
+ let savedRange = null;
1531
+
1532
+ if (selection.rangeCount > 0) {
1533
+ savedRange = selection.getRangeAt(0).cloneRange();
1534
+ }
1535
+
1536
+ const modal = this.createModal('Insert Image', `
1537
+ <div style="margin-bottom: 16px;">
1538
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Image URL:</label>
1539
+ <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;">
1540
+ </div>
1541
+ ${this.onImageUpload ? `
1542
+ <div style="margin-bottom: 16px; text-align: center;">
1543
+ <div style="color: #666; margin-bottom: 8px;">- OR -</div>
1544
+ <input type="file" id="image-file" accept="image/*" style="display: block; margin: 0 auto;">
1545
+ </div>
1546
+ ` : ''}
1547
+ <div style="margin-bottom: 16px;">
1548
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Alt Text:</label>
1549
+ <input type="text" id="image-alt" placeholder="Image description" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px;">
1550
+ </div>
1551
+ <div style="margin-bottom: 16px;">
1552
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Width (optional):</label>
1553
+ <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;">
1554
+ </div>
1555
+ `, async () => {
1556
+ let url = document.getElementById('image-url').value.trim();
1557
+ const alt = document.getElementById('image-alt').value.trim();
1558
+ const width = document.getElementById('image-width').value.trim();
1559
+ const fileInput = document.getElementById('image-file');
1560
+
1561
+ if (fileInput && fileInput.files.length > 0) {
1562
+ if (this.onImageUpload) {
1563
+ try {
1564
+ url = await this.onImageUpload(fileInput.files[0]);
1565
+ } catch (error) {
1566
+ alert('Failed to upload image');
1567
+ return false;
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ if (!url) {
1573
+ alert('Image URL is required');
1574
+ return false;
1575
+ }
1576
+
1577
+ this.insertImage(url, alt || 'Image', width ? parseInt(width) : null, savedRange);
1578
+
1579
+ return true;
1580
+ });
1581
+
1582
+ document.body.appendChild(modal);
1583
+ document.getElementById('image-url').focus();
1584
+ }
1585
+
1586
+ insertImage(url, alt = 'Image', width = null, savedRange = null) {
1587
+
1588
+ if (savedRange) {
1589
+ this.editor.focus();
1590
+ const selection = window.getSelection();
1591
+ selection.removeAllRanges();
1592
+ selection.addRange(savedRange);
1593
+ } else {
1594
+
1595
+ this.editor.focus();
1596
+ }
1597
+
1598
+ const imageWrapper = document.createElement('div');
1599
+ imageWrapper.className = 'editium-image-wrapper align-left';
1600
+ imageWrapper.contentEditable = 'false';
1601
+ imageWrapper.style.textAlign = 'left';
1602
+
1603
+ const imageContainer = document.createElement('div');
1604
+ imageContainer.style.position = 'relative';
1605
+ imageContainer.style.display = 'inline-block';
1606
+
1607
+ const img = document.createElement('img');
1608
+ img.src = url;
1609
+ img.alt = alt;
1610
+ img.style.maxWidth = '100%';
1611
+ img.style.height = 'auto';
1612
+ img.style.display = 'block';
1613
+ img.style.marginLeft = '0';
1614
+ img.style.marginRight = 'auto';
1615
+ img.className = 'resizable';
1616
+ img.draggable = false;
1617
+
1618
+ if (width) {
1619
+ img.style.width = width + 'px';
1620
+ }
1621
+
1622
+ const toolbar = this.createImageToolbar(imageWrapper, img);
1623
+
1624
+ imageContainer.appendChild(img);
1625
+ imageContainer.appendChild(toolbar);
1626
+ imageWrapper.appendChild(imageContainer);
1627
+
1628
+ this.makeImageResizable(img);
1629
+
1630
+ const selection = window.getSelection();
1631
+ let inserted = false;
1632
+
1633
+ if (selection.rangeCount > 0) {
1634
+ const range = selection.getRangeAt(0);
1635
+
1636
+ range.deleteContents();
1637
+
1638
+ try {
1639
+ range.insertNode(imageWrapper);
1640
+
1641
+ const newPara = document.createElement('p');
1642
+ newPara.innerHTML = '<br>';
1643
+
1644
+ if (imageWrapper.nextSibling) {
1645
+ imageWrapper.parentNode.insertBefore(newPara, imageWrapper.nextSibling);
1646
+ } else {
1647
+ imageWrapper.parentNode.appendChild(newPara);
1648
+ }
1649
+
1650
+ range.setStart(newPara, 0);
1651
+ range.setEnd(newPara, 0);
1652
+ selection.removeAllRanges();
1653
+ selection.addRange(range);
1654
+
1655
+ inserted = true;
1656
+ } catch (e) {
1657
+ console.error('Error inserting image at cursor:', e);
1658
+ }
1659
+ }
1660
+
1661
+ if (!inserted) {
1662
+ this.editor.appendChild(imageWrapper);
1663
+ const newPara = document.createElement('p');
1664
+ newPara.innerHTML = '<br>';
1665
+ this.editor.appendChild(newPara);
1666
+
1667
+ const range = document.createRange();
1668
+ range.setStart(newPara, 0);
1669
+ range.setEnd(newPara, 0);
1670
+ selection.removeAllRanges();
1671
+ selection.addRange(range);
1672
+ }
1673
+
1674
+ this.saveState();
1675
+ this.triggerChange();
1676
+ }
1677
+
1678
+ createImageToolbar(wrapper, img) {
1679
+ const toolbar = document.createElement('div');
1680
+ toolbar.className = 'editium-image-toolbar';
1681
+
1682
+ const alignmentGroup = document.createElement('div');
1683
+ alignmentGroup.className = 'editium-image-toolbar-group';
1684
+
1685
+ const alignments = [
1686
+ { value: 'left', label: '⬅', title: 'Align left' },
1687
+ { value: 'center', label: '↔', title: 'Align center' },
1688
+ { value: 'right', label: '➡', title: 'Align right' }
1689
+ ];
1690
+
1691
+ alignments.forEach(align => {
1692
+ const btn = document.createElement('button');
1693
+ btn.textContent = align.label;
1694
+ btn.title = align.title;
1695
+ btn.className = align.value === 'left' ? 'active' : '';
1696
+ btn.onclick = (e) => {
1697
+ e.preventDefault();
1698
+ e.stopPropagation();
1699
+ this.changeImageAlignment(wrapper, align.value);
1700
+
1701
+ alignmentGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));
1702
+ btn.classList.add('active');
1703
+ };
1704
+ alignmentGroup.appendChild(btn);
1705
+ });
1706
+
1707
+ toolbar.appendChild(alignmentGroup);
1708
+
1709
+ const actionGroup = document.createElement('div');
1710
+ actionGroup.className = 'editium-image-toolbar-group';
1711
+
1712
+ const removeBtn = document.createElement('button');
1713
+ removeBtn.innerHTML = '<i class="fa-solid fa-trash"></i>';
1714
+ removeBtn.title = 'Remove Image';
1715
+ removeBtn.style.color = '#dc3545';
1716
+ removeBtn.onclick = (e) => {
1717
+ e.preventDefault();
1718
+ e.stopPropagation();
1719
+ if (confirm('Remove this image?')) {
1720
+ wrapper.remove();
1721
+ this.saveState();
1722
+ this.triggerChange();
1723
+ }
1724
+ };
1725
+ actionGroup.appendChild(removeBtn);
1726
+
1727
+ toolbar.appendChild(actionGroup);
1728
+
1729
+ return toolbar;
1730
+ }
1731
+
1732
+ changeImageAlignment(wrapper, alignment) {
1733
+
1734
+ wrapper.classList.remove('align-left', 'align-center', 'align-right');
1735
+
1736
+ wrapper.classList.add(`align-${alignment}`);
1737
+
1738
+ const container = wrapper.querySelector('div[style*="position: relative"]');
1739
+ const img = wrapper.querySelector('img');
1740
+
1741
+ if (container && img) {
1742
+
1743
+ if (alignment === 'left') {
1744
+ wrapper.style.textAlign = 'left';
1745
+ img.style.marginLeft = '0';
1746
+ img.style.marginRight = 'auto';
1747
+ } else if (alignment === 'center') {
1748
+ wrapper.style.textAlign = 'center';
1749
+ img.style.marginLeft = 'auto';
1750
+ img.style.marginRight = 'auto';
1751
+ } else if (alignment === 'right') {
1752
+ wrapper.style.textAlign = 'right';
1753
+ img.style.marginLeft = 'auto';
1754
+ img.style.marginRight = '0';
1755
+ }
1756
+ }
1757
+
1758
+ this.saveState();
1759
+ this.triggerChange();
1760
+ }
1761
+
1762
+ makeImageResizable(img) {
1763
+ let isResizing = false;
1764
+ let startX, startWidth;
1765
+
1766
+ const startResize = (e) => {
1767
+ e.preventDefault();
1768
+ e.stopPropagation();
1769
+
1770
+ isResizing = true;
1771
+ startX = e.clientX || e.touches[0].clientX;
1772
+ startWidth = img.offsetWidth;
1773
+
1774
+ img.classList.add('resizing');
1775
+
1776
+ document.addEventListener('mousemove', resize);
1777
+ document.addEventListener('mouseup', stopResize);
1778
+ document.addEventListener('touchmove', resize);
1779
+ document.addEventListener('touchend', stopResize);
1780
+ };
1781
+
1782
+ const resize = (e) => {
1783
+ if (!isResizing) return;
1784
+
1785
+ e.preventDefault();
1786
+ const currentX = e.clientX || e.touches[0].clientX;
1787
+ const diff = currentX - startX;
1788
+ const newWidth = startWidth + diff;
1789
+
1790
+ if (newWidth > 50 && newWidth <= this.editor.offsetWidth) {
1791
+ img.style.width = newWidth + 'px';
1792
+ }
1793
+ };
1794
+
1795
+ const stopResize = () => {
1796
+ if (!isResizing) return;
1797
+
1798
+ isResizing = false;
1799
+ img.classList.remove('resizing');
1800
+
1801
+ document.removeEventListener('mousemove', resize);
1802
+ document.removeEventListener('mouseup', stopResize);
1803
+ document.removeEventListener('touchmove', resize);
1804
+ document.removeEventListener('touchend', stopResize);
1805
+
1806
+ this.saveState();
1807
+ this.triggerChange();
1808
+ };
1809
+
1810
+ img.addEventListener('mousedown', (e) => {
1811
+ const rect = img.getBoundingClientRect();
1812
+ const offsetX = e.clientX - rect.left;
1813
+
1814
+ if (offsetX > rect.width - 20) {
1815
+ startResize(e);
1816
+ }
1817
+ });
1818
+
1819
+ img.addEventListener('touchstart', (e) => {
1820
+ const rect = img.getBoundingClientRect();
1821
+ const touch = e.touches[0];
1822
+ const offsetX = touch.clientX - rect.left;
1823
+
1824
+ if (offsetX > rect.width - 20) {
1825
+ startResize(e);
1826
+ }
1827
+ });
1828
+ }
1829
+
1830
+ showTableModal() {
1831
+ const modal = this.createModal('Insert Table', `
1832
+ <div style="margin-bottom: 16px;">
1833
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Rows:</label>
1834
+ <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;">
1835
+ </div>
1836
+ <div style="margin-bottom: 16px;">
1837
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Columns:</label>
1838
+ <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;">
1839
+ </div>
1840
+ `, () => {
1841
+ const rows = parseInt(document.getElementById('table-rows').value) || 3;
1842
+ const cols = parseInt(document.getElementById('table-cols').value) || 3;
1843
+
1844
+ const table = document.createElement('table');
1845
+ table.style.cssText = 'border-collapse: collapse; width: 100%; margin-bottom: 1em; border: 1px solid #ccc;';
1846
+
1847
+ for (let i = 0; i < rows; i++) {
1848
+ const tr = document.createElement('tr');
1849
+ for (let j = 0; j < cols; j++) {
1850
+ const cell = i === 0 ? document.createElement('th') : document.createElement('td');
1851
+ cell.style.cssText = 'border: 1px solid #ccc; padding: 8px; text-align: left;';
1852
+ if (i === 0) {
1853
+ cell.style.backgroundColor = '#f0f0f0';
1854
+ cell.style.fontWeight = 'bold';
1855
+ cell.textContent = `Header ${j + 1}`;
1856
+ } else {
1857
+ cell.innerHTML = '<br>';
1858
+ }
1859
+ cell.contentEditable = 'true';
1860
+ tr.appendChild(cell);
1861
+ }
1862
+ table.appendChild(tr);
1863
+ }
1864
+
1865
+ this.insertNodeAtCursor(table);
1866
+ this.saveState();
1867
+ this.triggerChange();
1868
+ return true;
1869
+ });
1870
+
1871
+ document.body.appendChild(modal);
1872
+ }
1873
+
1874
+ showColorPicker(command) {
1875
+ const colors = [
1876
+ '#000000', '#495057', '#6c757d', '#adb5bd',
1877
+ '#dc3545', '#fd7e14', '#ffc107', '#28a745',
1878
+ '#20c997', '#007bff', '#6610f2', '#6f42c1',
1879
+ '#e83e8c', '#ffffff'
1880
+ ];
1881
+
1882
+ const colorHTML = colors.map(color =>
1883
+ `<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>`
1884
+ ).join('');
1885
+
1886
+ const modal = this.createModal('Choose Color', `
1887
+ <div style="display: flex; flex-wrap: wrap; justify-content: center; margin-bottom: 16px;">
1888
+ ${colorHTML}
1889
+ </div>
1890
+ <div>
1891
+ <label style="display: block; margin-bottom: 4px; font-size: 14px; color: #333;">Or enter custom color:</label>
1892
+ <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;">
1893
+ </div>
1894
+ `, () => {
1895
+ const customColor = document.getElementById('custom-color').value.trim();
1896
+ if (customColor) {
1897
+ this.execCommand(command, customColor);
1898
+ }
1899
+ return true;
1900
+ });
1901
+
1902
+ modal.querySelectorAll('[data-color]').forEach(btn => {
1903
+ btn.onclick = () => {
1904
+ this.execCommand(command, btn.getAttribute('data-color'));
1905
+ modal.remove();
1906
+ };
1907
+ });
1908
+
1909
+ document.body.appendChild(modal);
1910
+ }
1911
+
1912
+ createModal(title, content, onSubmit) {
1913
+ const overlay = document.createElement('div');
1914
+ 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;';
1915
+
1916
+ const modal = document.createElement('div');
1917
+ 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;';
1918
+
1919
+ modal.innerHTML = `
1920
+ <div style="padding:16px 20px;border-bottom:1px solid #ccc;display:flex;justify-content:space-between;align-items:center;">
1921
+ <h3 style="margin:0;font-size:18px;font-weight:600;color:#222f3e;">${title}</h3>
1922
+ <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>
1923
+ </div>
1924
+ <div style="padding:20px;">
1925
+ ${content}
1926
+ </div>
1927
+ <div style="padding:16px 20px;border-top:1px solid #ccc;display:flex;justify-content:flex-end;gap:10px;">
1928
+ <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>
1929
+ <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>
1930
+ </div>
1931
+ `;
1932
+
1933
+ overlay.appendChild(modal);
1934
+
1935
+ const close = () => overlay.remove();
1936
+
1937
+ modal.querySelector('.modal-close').onclick = close;
1938
+ modal.querySelector('.modal-cancel').onclick = close;
1939
+ modal.querySelector('.modal-submit').onclick = async () => {
1940
+ const result = await onSubmit();
1941
+ if (result !== false) {
1942
+ close();
1943
+ }
1944
+ };
1945
+
1946
+ overlay.onclick = (e) => {
1947
+ if (e.target === overlay) close();
1948
+ };
1949
+
1950
+ return overlay;
1951
+ }
1952
+
1953
+ insertCodeBlock() {
1954
+ const pre = document.createElement('pre');
1955
+ const code = document.createElement('code');
1956
+ code.textContent = 'Code here...';
1957
+ pre.appendChild(code);
1958
+ pre.style.cssText = 'background:#f4f4f4;padding:10px;border-radius:3px;font-family:monospace;overflow:auto;margin:1em 0;';
1959
+
1960
+ this.insertNodeAtCursor(pre);
1961
+ this.saveState();
1962
+ this.triggerChange();
1963
+ }
1964
+
1965
+ insertNodeAtCursor(node) {
1966
+ const selection = window.getSelection();
1967
+ if (selection.rangeCount) {
1968
+ const range = selection.getRangeAt(0);
1969
+ range.deleteContents();
1970
+ range.insertNode(node);
1971
+
1972
+ range.setStartAfter(node);
1973
+ range.setEndAfter(node);
1974
+ selection.removeAllRanges();
1975
+ selection.addRange(range);
1976
+ } else {
1977
+ this.editor.appendChild(node);
1978
+ }
1979
+ }
1980
+
1981
+ toggleFullscreen() {
1982
+ this.isFullscreen = !this.isFullscreen;
1983
+ if (this.isFullscreen) {
1984
+ this.wrapper.classList.add('editium-fullscreen');
1985
+
1986
+ document.body.classList.add('editium-fullscreen-active');
1987
+
1988
+ this.editor.style.height = 'auto';
1989
+ this.editor.style.minHeight = 'auto';
1990
+ this.editor.style.maxHeight = 'none';
1991
+ } else {
1992
+ this.wrapper.classList.remove('editium-fullscreen');
1993
+
1994
+ document.body.classList.remove('editium-fullscreen-active');
1995
+
1996
+ this.editor.style.height = typeof this.height === 'number' ? `${this.height}px` : this.height;
1997
+ this.editor.style.minHeight = typeof this.minHeight === 'number' ? `${this.minHeight}px` : this.minHeight;
1998
+ this.editor.style.maxHeight = typeof this.maxHeight === 'number' ? `${this.maxHeight}px` : this.maxHeight;
1999
+ }
2000
+ }
2001
+
2002
+ toggleFindReplace() {
2003
+ if (this.findReplacePanel) {
2004
+ this.findReplacePanel.remove();
2005
+ this.findReplacePanel = null;
2006
+ this.clearSearch();
2007
+ return;
2008
+ }
2009
+
2010
+ const panel = document.createElement('div');
2011
+ panel.className = 'editium-find-replace';
2012
+ panel.innerHTML = `
2013
+ <div style="display: flex; align-items: flex-start; gap: 12px;">
2014
+ <!-- Search Input Container -->
2015
+ <div style="flex: 1; min-width: 200px;">
2016
+ <div style="position: relative;">
2017
+ <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>
2018
+ <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;">
2019
+ </div>
2020
+ <div class="editium-match-info" style="margin-top: 4px; font-size: 11px; color: #6b7280; min-height: 14px;"></div>
2021
+ </div>
2022
+
2023
+ <!-- Navigation Buttons -->
2024
+ <div class="editium-nav-buttons" style="display: none; gap: 4px;">
2025
+ <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;">
2026
+ <i class="fa-solid fa-chevron-left" style="font-size: 14px;"></i>
2027
+ </button>
2028
+ <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;">
2029
+ <i class="fa-solid fa-chevron-right" style="font-size: 14px;"></i>
2030
+ </button>
2031
+ </div>
2032
+
2033
+ <!-- Replace Input -->
2034
+ <div style="flex: 1; min-width: 200px;">
2035
+ <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;">
2036
+ </div>
2037
+
2038
+ <!-- Action Buttons -->
2039
+ <div style="display: flex; gap: 6px;">
2040
+ <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;">
2041
+ Replace
2042
+ </button>
2043
+ <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;">
2044
+ Replace All
2045
+ </button>
2046
+ </div>
2047
+
2048
+ <!-- Close Button -->
2049
+ <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;">
2050
+ <i class="fa-solid fa-xmark" style="font-size: 16px;"></i>
2051
+ </button>
2052
+ </div>
2053
+ `;
2054
+
2055
+ this.editorContainer.insertBefore(panel, this.editor);
2056
+ this.findReplacePanel = panel;
2057
+
2058
+ const findInput = panel.querySelector('.editium-find-input');
2059
+ const replaceInput = panel.querySelector('.editium-replace-input');
2060
+ const matchInfo = panel.querySelector('.editium-match-info');
2061
+ const navButtons = panel.querySelector('.editium-nav-buttons');
2062
+ const prevBtn = panel.querySelector('.editium-btn-prev');
2063
+ const nextBtn = panel.querySelector('.editium-btn-next');
2064
+ const replaceBtn = panel.querySelector('.editium-btn-replace');
2065
+ const replaceAllBtn = panel.querySelector('.editium-btn-replace-all');
2066
+ const closeBtn = panel.querySelector('.editium-btn-close');
2067
+
2068
+ findInput.addEventListener('focus', () => {
2069
+ if (!this.searchQuery || this.searchMatches.length > 0) {
2070
+ findInput.style.borderColor = '#3b82f6';
2071
+ }
2072
+ });
2073
+
2074
+ findInput.addEventListener('blur', () => {
2075
+ if (this.searchQuery && this.searchMatches.length === 0) {
2076
+ findInput.style.borderColor = '#ef4444';
2077
+ } else {
2078
+ findInput.style.borderColor = '#d1d5db';
2079
+ }
2080
+ });
2081
+
2082
+ replaceInput.addEventListener('focus', () => {
2083
+ replaceInput.style.borderColor = '#3b82f6';
2084
+ });
2085
+
2086
+ replaceInput.addEventListener('blur', () => {
2087
+ replaceInput.style.borderColor = '#d1d5db';
2088
+ });
2089
+
2090
+ findInput.addEventListener('input', () => {
2091
+ this.searchQuery = findInput.value;
2092
+ this.performSearch();
2093
+
2094
+ if (this.searchQuery) {
2095
+ if (this.searchMatches.length === 0) {
2096
+ matchInfo.textContent = 'No matches';
2097
+ matchInfo.style.color = '#ef4444';
2098
+ findInput.style.borderColor = '#ef4444';
2099
+ navButtons.style.display = 'none';
2100
+ replaceBtn.disabled = true;
2101
+ replaceAllBtn.disabled = true;
2102
+ replaceBtn.style.backgroundColor = '#f3f4f6';
2103
+ replaceBtn.style.color = '#9ca3af';
2104
+ replaceBtn.style.cursor = 'not-allowed';
2105
+ replaceAllBtn.style.backgroundColor = '#cbd5e1';
2106
+ replaceAllBtn.style.cursor = 'not-allowed';
2107
+ } else {
2108
+ matchInfo.textContent = `${this.currentMatchIndex + 1} of ${this.searchMatches.length}`;
2109
+ matchInfo.style.color = '#6b7280';
2110
+ findInput.style.borderColor = '#d1d5db';
2111
+ navButtons.style.display = 'flex';
2112
+ replaceBtn.disabled = false;
2113
+ replaceAllBtn.disabled = false;
2114
+ replaceBtn.style.backgroundColor = 'white';
2115
+ replaceBtn.style.color = '#374151';
2116
+ replaceBtn.style.cursor = 'pointer';
2117
+ replaceAllBtn.style.backgroundColor = '#3b82f6';
2118
+ replaceAllBtn.style.cursor = 'pointer';
2119
+ }
2120
+ } else {
2121
+ matchInfo.textContent = '';
2122
+ navButtons.style.display = 'none';
2123
+ replaceBtn.disabled = true;
2124
+ replaceAllBtn.disabled = true;
2125
+ }
2126
+ });
2127
+
2128
+ prevBtn.onclick = () => {
2129
+ this.navigateSearch(-1, matchInfo);
2130
+ };
2131
+
2132
+ nextBtn.onclick = () => {
2133
+ this.navigateSearch(1, matchInfo);
2134
+ };
2135
+
2136
+ [prevBtn, nextBtn].forEach(btn => {
2137
+ btn.addEventListener('mouseenter', () => {
2138
+ if (this.searchMatches.length > 0) {
2139
+ btn.style.backgroundColor = '#f3f4f6';
2140
+ btn.style.borderColor = '#9ca3af';
2141
+ }
2142
+ });
2143
+ btn.addEventListener('mouseleave', () => {
2144
+ if (this.searchMatches.length > 0) {
2145
+ btn.style.backgroundColor = 'white';
2146
+ btn.style.borderColor = '#d1d5db';
2147
+ }
2148
+ });
2149
+ });
2150
+
2151
+ replaceBtn.addEventListener('mouseenter', () => {
2152
+ if (!replaceBtn.disabled) {
2153
+ replaceBtn.style.backgroundColor = '#f3f4f6';
2154
+ replaceBtn.style.borderColor = '#9ca3af';
2155
+ }
2156
+ });
2157
+ replaceBtn.addEventListener('mouseleave', () => {
2158
+ if (!replaceBtn.disabled) {
2159
+ replaceBtn.style.backgroundColor = 'white';
2160
+ replaceBtn.style.borderColor = '#d1d5db';
2161
+ }
2162
+ });
2163
+
2164
+ replaceAllBtn.addEventListener('mouseenter', () => {
2165
+ if (!replaceAllBtn.disabled) {
2166
+ replaceAllBtn.style.backgroundColor = '#2563eb';
2167
+ }
2168
+ });
2169
+ replaceAllBtn.addEventListener('mouseleave', () => {
2170
+ if (!replaceAllBtn.disabled) {
2171
+ replaceAllBtn.style.backgroundColor = '#3b82f6';
2172
+ }
2173
+ });
2174
+
2175
+ closeBtn.addEventListener('mouseenter', () => {
2176
+ closeBtn.style.backgroundColor = '#fee2e2';
2177
+ closeBtn.style.borderColor = '#ef4444';
2178
+ closeBtn.style.color = '#dc2626';
2179
+ });
2180
+ closeBtn.addEventListener('mouseleave', () => {
2181
+ closeBtn.style.backgroundColor = 'white';
2182
+ closeBtn.style.borderColor = '#d1d5db';
2183
+ closeBtn.style.color = '#6b7280';
2184
+ });
2185
+
2186
+ replaceBtn.onclick = () => {
2187
+ this.replaceCurrentMatch(replaceInput.value, matchInfo);
2188
+ };
2189
+
2190
+ replaceAllBtn.onclick = () => {
2191
+ this.replaceAllMatches(replaceInput.value);
2192
+ this.clearSearch();
2193
+ findInput.value = '';
2194
+ replaceInput.value = '';
2195
+ matchInfo.textContent = '';
2196
+ navButtons.style.display = 'none';
2197
+ };
2198
+
2199
+ closeBtn.onclick = () => {
2200
+ panel.remove();
2201
+ this.findReplacePanel = null;
2202
+ this.clearSearch();
2203
+ };
2204
+
2205
+ findInput.focus();
2206
+ }
2207
+
2208
+ performSearch() {
2209
+ this.clearHighlights();
2210
+ if (!this.searchQuery || this.searchQuery.trim() === '') {
2211
+ this.searchMatches = [];
2212
+ return;
2213
+ }
2214
+
2215
+ const searchLower = this.searchQuery.toLowerCase();
2216
+ const matches = [];
2217
+
2218
+ const walker = document.createTreeWalker(
2219
+ this.editor,
2220
+ NodeFilter.SHOW_TEXT,
2221
+ {
2222
+ acceptNode: (node) => {
2223
+
2224
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-match')) {
2225
+ return NodeFilter.FILTER_REJECT;
2226
+ }
2227
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-current')) {
2228
+ return NodeFilter.FILTER_REJECT;
2229
+ }
2230
+ return NodeFilter.FILTER_ACCEPT;
2231
+ }
2232
+ }
2233
+ );
2234
+
2235
+ let currentNode;
2236
+ const nodesToProcess = [];
2237
+
2238
+ while (currentNode = walker.nextNode()) {
2239
+ nodesToProcess.push(currentNode);
2240
+ }
2241
+
2242
+ nodesToProcess.forEach(node => {
2243
+ const text = node.textContent;
2244
+ const textLower = text.toLowerCase();
2245
+ let index = 0;
2246
+
2247
+ while ((index = textLower.indexOf(searchLower, index)) !== -1) {
2248
+ matches.push({
2249
+ node: node,
2250
+ offset: index,
2251
+ length: this.searchQuery.length
2252
+ });
2253
+ index += 1;
2254
+ }
2255
+ });
2256
+
2257
+ this.searchMatches = matches;
2258
+ this.currentMatchIndex = 0;
2259
+
2260
+ if (this.searchMatches.length > 0) {
2261
+ this.highlightAllMatches();
2262
+ }
2263
+ }
2264
+
2265
+ highlightAllMatches() {
2266
+
2267
+ const sortedMatches = [...this.searchMatches].reverse();
2268
+
2269
+ sortedMatches.forEach((match, reverseIdx) => {
2270
+ const actualIdx = this.searchMatches.length - 1 - reverseIdx;
2271
+ const { node, offset, length } = match;
2272
+
2273
+ if (!node.parentNode) return;
2274
+
2275
+ try {
2276
+
2277
+ const text = node.textContent;
2278
+ const before = text.substring(0, offset);
2279
+ const matchText = text.substring(offset, offset + length);
2280
+ const after = text.substring(offset + length);
2281
+
2282
+ const mark = document.createElement('mark');
2283
+ const isCurrent = actualIdx === this.currentMatchIndex;
2284
+ mark.className = isCurrent ? 'editium-search-current' : 'editium-search-match';
2285
+ mark.textContent = matchText;
2286
+
2287
+ if (isCurrent) {
2288
+ mark.style.backgroundColor = '#ff9800';
2289
+ mark.style.color = '#ffffff';
2290
+ mark.style.fontWeight = '600';
2291
+ mark.setAttribute('data-current-match', 'true');
2292
+ } else {
2293
+ mark.style.backgroundColor = '#ffeb3b';
2294
+ mark.style.color = '#000000';
2295
+ }
2296
+ mark.style.padding = '2px 4px';
2297
+ mark.style.borderRadius = '2px';
2298
+
2299
+ const parent = node.parentNode;
2300
+ const fragment = document.createDocumentFragment();
2301
+
2302
+ if (before) {
2303
+ fragment.appendChild(document.createTextNode(before));
2304
+ }
2305
+ fragment.appendChild(mark);
2306
+ if (after) {
2307
+ fragment.appendChild(document.createTextNode(after));
2308
+ }
2309
+
2310
+ parent.replaceChild(fragment, node);
2311
+
2312
+ } catch (e) {
2313
+ console.warn('Failed to highlight match:', e);
2314
+ }
2315
+ });
2316
+
2317
+ if (this.searchMatches.length > 0) {
2318
+ setTimeout(() => {
2319
+ const currentMark = this.editor.querySelector('[data-current-match="true"]');
2320
+ if (currentMark) {
2321
+ currentMark.scrollIntoView({ behavior: 'smooth', block: 'center' });
2322
+ }
2323
+ }, 10);
2324
+ }
2325
+ }
2326
+
2327
+ clearHighlights() {
2328
+ this.editor.querySelectorAll('.editium-search-match, .editium-search-current').forEach(mark => {
2329
+ const parent = mark.parentNode;
2330
+ while (mark.firstChild) {
2331
+ parent.insertBefore(mark.firstChild, mark);
2332
+ }
2333
+ parent.removeChild(mark);
2334
+ parent.normalize();
2335
+ });
2336
+ }
2337
+
2338
+ navigateSearch(direction, matchInfoElement) {
2339
+ if (this.searchMatches.length === 0) return;
2340
+
2341
+ this.currentMatchIndex += direction;
2342
+ if (this.currentMatchIndex < 0) {
2343
+ this.currentMatchIndex = this.searchMatches.length - 1;
2344
+ } else if (this.currentMatchIndex >= this.searchMatches.length) {
2345
+ this.currentMatchIndex = 0;
2346
+ }
2347
+
2348
+ this.clearHighlights();
2349
+
2350
+ const searchLower = this.searchQuery.toLowerCase();
2351
+ const matches = [];
2352
+
2353
+ const walker = document.createTreeWalker(
2354
+ this.editor,
2355
+ NodeFilter.SHOW_TEXT,
2356
+ {
2357
+ acceptNode: (node) => {
2358
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-match')) {
2359
+ return NodeFilter.FILTER_REJECT;
2360
+ }
2361
+ if (node.parentElement && node.parentElement.classList.contains('editium-search-current')) {
2362
+ return NodeFilter.FILTER_REJECT;
2363
+ }
2364
+ return NodeFilter.FILTER_ACCEPT;
2365
+ }
2366
+ }
2367
+ );
2368
+
2369
+ let currentNode;
2370
+ const nodesToProcess = [];
2371
+
2372
+ while (currentNode = walker.nextNode()) {
2373
+ nodesToProcess.push(currentNode);
2374
+ }
2375
+
2376
+ nodesToProcess.forEach(node => {
2377
+ const text = node.textContent;
2378
+ const textLower = text.toLowerCase();
2379
+ let index = 0;
2380
+
2381
+ while ((index = textLower.indexOf(searchLower, index)) !== -1) {
2382
+ matches.push({
2383
+ node: node,
2384
+ offset: index,
2385
+ length: this.searchQuery.length
2386
+ });
2387
+ index += 1;
2388
+ }
2389
+ });
2390
+
2391
+ this.searchMatches = matches;
2392
+
2393
+ if (this.searchMatches.length > 0) {
2394
+ this.highlightAllMatches();
2395
+ }
2396
+
2397
+ if (matchInfoElement) {
2398
+ matchInfoElement.textContent = this.searchMatches.length > 0
2399
+ ? `${this.currentMatchIndex + 1} of ${this.searchMatches.length}`
2400
+ : 'No matches';
2401
+ matchInfoElement.style.color = this.searchMatches.length > 0 ? '#6b7280' : '#ef4444';
2402
+ }
2403
+ }
2404
+
2405
+ updateMatchCount(element) {
2406
+ if (element) {
2407
+ element.textContent = this.searchMatches.length > 0
2408
+ ? `${this.currentMatchIndex + 1}/${this.searchMatches.length}`
2409
+ : '0/0';
2410
+ }
2411
+ }
2412
+
2413
+ replaceCurrentMatch(replacement, matchCountElement) {
2414
+ if (this.searchMatches.length === 0) return;
2415
+
2416
+ const currentMark = this.editor.querySelectorAll('.editium-search-match, .editium-search-current')[this.currentMatchIndex];
2417
+ if (currentMark) {
2418
+ currentMark.textContent = replacement;
2419
+ const parent = currentMark.parentNode;
2420
+ while (currentMark.firstChild) {
2421
+ parent.insertBefore(currentMark.firstChild, currentMark);
2422
+ }
2423
+ parent.removeChild(currentMark);
2424
+ parent.normalize();
2425
+
2426
+ this.performSearch();
2427
+ this.updateMatchCount(matchCountElement);
2428
+ this.saveState();
2429
+ this.triggerChange();
2430
+ }
2431
+ }
2432
+
2433
+ replaceAllMatches(replacement) {
2434
+ this.editor.querySelectorAll('.editium-search-match, .editium-search-current').forEach(mark => {
2435
+ mark.textContent = replacement;
2436
+ const parent = mark.parentNode;
2437
+ while (mark.firstChild) {
2438
+ parent.insertBefore(mark.firstChild, mark);
2439
+ }
2440
+ parent.removeChild(mark);
2441
+ parent.normalize();
2442
+ });
2443
+
2444
+ this.saveState();
2445
+ this.triggerChange();
2446
+ }
2447
+
2448
+ clearSearch() {
2449
+ this.clearHighlights();
2450
+ this.searchMatches = [];
2451
+ this.currentMatchIndex = 0;
2452
+ this.searchQuery = '';
2453
+ }
2454
+
2455
+ viewOutput(type) {
2456
+ const modal = document.createElement('div');
2457
+ 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;';
2458
+
2459
+ let content = '';
2460
+ let title = '';
2461
+
2462
+ if (type === 'html') {
2463
+ content = this.formatHTML(this.getHTML());
2464
+ title = 'HTML Output';
2465
+
2466
+ if (!content || content.trim() === '') {
2467
+ content = '<!-- No content -->';
2468
+ }
2469
+ } else if (type === 'json') {
2470
+ content = JSON.stringify(this.getJSON(), null, 2);
2471
+ title = 'JSON Output';
2472
+ } else if (type === 'preview') {
2473
+ const htmlContent = this.getHTML();
2474
+ const previewContent = htmlContent && htmlContent.trim() !== ''
2475
+ ? htmlContent
2476
+ : '<p style="color:#999;text-align:center;padding:40px;">No content to preview</p>';
2477
+
2478
+ modal.innerHTML = `
2479
+ <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;">
2480
+ <div style="padding:16px 20px;border-bottom:1px solid #ccc;display:flex;justify-content:space-between;align-items:center;">
2481
+ <h3 style="margin:0;font-size:18px;font-weight:600;color:#222f3e;">Preview</h3>
2482
+ <button class="modal-close" style="background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0;">×</button>
2483
+ </div>
2484
+ <div style="padding:20px;flex:1;overflow:auto;">${previewContent}</div>
2485
+ <div style="padding:16px 20px;border-top:1px solid #ccc;display:flex;justify-content:flex-end;gap:10px;">
2486
+ <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>
2487
+ </div>
2488
+ </div>
2489
+ `;
2490
+ document.body.appendChild(modal);
2491
+
2492
+ const closeBtn = modal.querySelector('.modal-close');
2493
+ const copyBtn = modal.querySelector('.btn-copy');
2494
+
2495
+ closeBtn.onclick = () => modal.remove();
2496
+ modal.onclick = (e) => {
2497
+ if (e.target === modal) modal.remove();
2498
+ };
2499
+
2500
+ copyBtn.onclick = () => {
2501
+ const html = this.getHTML();
2502
+ if (html && html.trim() !== '') {
2503
+ navigator.clipboard.writeText(html).then(() => {
2504
+ copyBtn.textContent = 'Copied!';
2505
+ copyBtn.style.backgroundColor = '#28a745';
2506
+ copyBtn.style.borderColor = '#28a745';
2507
+ setTimeout(() => {
2508
+ copyBtn.textContent = 'Copy HTML';
2509
+ copyBtn.style.backgroundColor = '#007bff';
2510
+ copyBtn.style.borderColor = '#007bff';
2511
+ }, 2000);
2512
+ });
2513
+ }
2514
+ };
2515
+ return;
2516
+ }
2517
+
2518
+ modal.innerHTML = `
2519
+ <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;">
2520
+ <div style="padding:16px 20px;border-bottom:1px solid #ccc;display:flex;justify-content:space-between;align-items:center;">
2521
+ <h3 style="margin:0;font-size:18px;font-weight:600;color:#222f3e;">${title}</h3>
2522
+ <button class="modal-close" style="background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0;">×</button>
2523
+ </div>
2524
+ <div style="padding:0;flex:1;overflow:auto;background:#282c34;">
2525
+ <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>
2526
+ </div>
2527
+ <div style="padding:16px 20px;border-top:1px solid #ccc;display:flex;justify-content:flex-end;gap:10px;">
2528
+ <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>
2529
+ <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>
2530
+ </div>
2531
+ </div>
2532
+ `;
2533
+
2534
+ document.body.appendChild(modal);
2535
+
2536
+ const closeBtn = modal.querySelector('.modal-close');
2537
+ const copyBtn = modal.querySelector('.btn-copy');
2538
+ const downloadBtn = modal.querySelector('.btn-download');
2539
+
2540
+ closeBtn.onclick = () => modal.remove();
2541
+ modal.onclick = (e) => {
2542
+ if (e.target === modal) modal.remove();
2543
+ };
2544
+
2545
+ copyBtn.onclick = () => {
2546
+ navigator.clipboard.writeText(content).then(() => {
2547
+ copyBtn.textContent = 'Copied!';
2548
+ copyBtn.style.backgroundColor = '#28a745';
2549
+ copyBtn.style.borderColor = '#28a745';
2550
+ setTimeout(() => {
2551
+ copyBtn.textContent = 'Copy to Clipboard';
2552
+ copyBtn.style.backgroundColor = '#007bff';
2553
+ copyBtn.style.borderColor = '#007bff';
2554
+ }, 2000);
2555
+ });
2556
+ };
2557
+
2558
+ downloadBtn.onclick = () => {
2559
+ const blob = new Blob([content], { type: type === 'html' ? 'text/html' : 'application/json' });
2560
+ const url = URL.createObjectURL(blob);
2561
+ const a = document.createElement('a');
2562
+ a.href = url;
2563
+ a.download = `editium-output.${type === 'html' ? 'html' : 'json'}`;
2564
+ document.body.appendChild(a);
2565
+ a.click();
2566
+ document.body.removeChild(a);
2567
+ URL.revokeObjectURL(url);
2568
+ };
2569
+ }
2570
+
2571
+ formatHTML(html) {
2572
+ if (!html || html.trim() === '') return '';
2573
+
2574
+ let indentLevel = 0;
2575
+ const tab = ' ';
2576
+ let formattedHTML = '';
2577
+
2578
+ const rawTokens = html.split(/(<[^>]+>)/);
2579
+
2580
+ const tokens = [];
2581
+ for (let i = 0; i < rawTokens.length; i++) {
2582
+ const token = rawTokens[i];
2583
+ if (token.startsWith('<') && token.endsWith('>')) {
2584
+
2585
+ tokens.push({ type: 'tag', content: token });
2586
+ } else if (token.trim()) {
2587
+
2588
+ tokens.push({ type: 'text', content: token.trim() });
2589
+ }
2590
+ }
2591
+
2592
+ const isBlockTag = (tag) => {
2593
+ const tagNameMatch = tag.match(/<\/?([a-zA-Z0-9]+)/);
2594
+ if (!tagNameMatch) return false;
2595
+ 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'];
2596
+ return blockTags.includes(tagNameMatch[1].toLowerCase());
2597
+ };
2598
+
2599
+ const isInlineTag = (tag) => {
2600
+ const tagNameMatch = tag.match(/<\/?([a-zA-Z0-9]+)/);
2601
+ if (!tagNameMatch) return false;
2602
+ const inlineTags = ['strong', 'em', 'u', 's', 'a', 'span', 'code', 'b', 'i', 'sub', 'sup', 'img', 'br'];
2603
+ return inlineTags.includes(tagNameMatch[1].toLowerCase());
2604
+ };
2605
+
2606
+ const isSelfClosing = (tag) => {
2607
+ return tag.endsWith('/>') || /<br|<hr|<img/.test(tag);
2608
+ };
2609
+
2610
+ let onNewLine = true;
2611
+
2612
+ for (let i = 0; i < tokens.length; i++) {
2613
+ const token = tokens[i];
2614
+ const prevToken = i > 0 ? tokens[i - 1] : null;
2615
+ const nextToken = i < tokens.length - 1 ? tokens[i + 1] : null;
2616
+
2617
+ if (token.type === 'tag') {
2618
+ const tag = token.content;
2619
+ const isClosing = tag.startsWith('</');
2620
+ const isBlock = isBlockTag(tag);
2621
+ const isInline = isInlineTag(tag);
2622
+
2623
+ if (isBlock) {
2624
+ if (isClosing) {
2625
+ indentLevel = Math.max(0, indentLevel - 1);
2626
+ }
2627
+
2628
+ if (!onNewLine) {
2629
+ formattedHTML += '\n';
2630
+ }
2631
+
2632
+ formattedHTML += tab.repeat(indentLevel) + tag;
2633
+ onNewLine = true;
2634
+
2635
+ if (!isClosing && !isSelfClosing(tag)) {
2636
+ indentLevel++;
2637
+ }
2638
+ } else if (isInline) {
2639
+
2640
+ if (onNewLine) {
2641
+ formattedHTML += tab.repeat(indentLevel);
2642
+ onNewLine = false;
2643
+ }
2644
+
2645
+ if (!isClosing && prevToken && prevToken.type === 'text') {
2646
+ formattedHTML += ' ';
2647
+ }
2648
+
2649
+ formattedHTML += tag;
2650
+
2651
+ if (isClosing && nextToken && nextToken.type === 'text') {
2652
+ formattedHTML += ' ';
2653
+ }
2654
+ } else {
2655
+
2656
+ formattedHTML += tag;
2657
+ }
2658
+ } else if (token.type === 'text') {
2659
+
2660
+ if (onNewLine) {
2661
+ formattedHTML += tab.repeat(indentLevel);
2662
+ onNewLine = false;
2663
+ }
2664
+
2665
+ formattedHTML += token.content;
2666
+ }
2667
+ }
2668
+
2669
+ return formattedHTML.trim();
2670
+ }
2671
+
2672
+ highlightCode(code, type) {
2673
+
2674
+ const escaped = this.escapeHtml(code);
2675
+
2676
+ if (type === 'html') {
2677
+
2678
+ return escaped
2679
+
2680
+ .replace(/(&lt;\/?)([a-z][a-z0-9]*)(\s[^&]*?)?(&gt;)/gi, (match, open, tagName, attrs, close) => {
2681
+ let result = open + '<span style="color:#e06c75;">' + tagName + '</span>';
2682
+ if (attrs) {
2683
+
2684
+ result += attrs.replace(/\s([a-z-]+)(=)(&quot;[^&quot;]*&quot;)/gi, ' <span style="color:#d19a66;">$1</span>=<span style="color:#98c379;">$3</span>');
2685
+ }
2686
+ result += close;
2687
+ return result;
2688
+ });
2689
+ } else if (type === 'json') {
2690
+
2691
+ return escaped
2692
+
2693
+ .replace(/(&quot;)(.*?)(&quot;)(\s*:)/g, '<span style="color:#e06c75;">$1$2$3</span><span style="color:#abb2bf;">$4</span>')
2694
+
2695
+ .replace(/(:)(\s*)(&quot;)((?:[^&]|&(?!quot;))*?)(&quot;)/g, '$1$2<span style="color:#98c379;">$3$4$5</span>')
2696
+
2697
+ .replace(/:\s*(-?\d+\.?\d*)/g, ': <span style="color:#d19a66;">$1</span>')
2698
+
2699
+ .replace(/:\s*(true|false|null)/g, ': <span style="color:#56b6c2;">$1</span>')
2700
+
2701
+ .replace(/([{}\[\]])/g, '<span style="color:#abb2bf;">$1</span>')
2702
+
2703
+ .replace(/(,)/g, '<span style="color:#abb2bf;">$1</span>');
2704
+ }
2705
+
2706
+ return escaped;
2707
+ }
2708
+
2709
+ escapeHtml(text) {
2710
+ const div = document.createElement('div');
2711
+ div.textContent = text;
2712
+ return div.innerHTML;
2713
+ }
2714
+
2715
+ saveState() {
2716
+ const state = this.editor.innerHTML;
2717
+
2718
+ if (this.historyIndex < this.history.length - 1) {
2719
+ this.history = this.history.slice(0, this.historyIndex + 1);
2720
+ }
2721
+
2722
+ if (this.history[this.historyIndex] === state) {
2723
+ return;
2724
+ }
2725
+
2726
+ this.history.push(state);
2727
+ this.historyIndex++;
2728
+
2729
+ if (this.history.length > this.maxHistory) {
2730
+ this.history.shift();
2731
+ this.historyIndex--;
2732
+ }
2733
+ }
2734
+
2735
+ undo() {
2736
+ if (this.historyIndex > 0) {
2737
+ this.historyIndex--;
2738
+ this.editor.innerHTML = this.history[this.historyIndex];
2739
+ this.triggerChange();
2740
+ }
2741
+ }
2742
+
2743
+ redo() {
2744
+ if (this.historyIndex < this.history.length - 1) {
2745
+ this.historyIndex++;
2746
+ this.editor.innerHTML = this.history[this.historyIndex];
2747
+ this.triggerChange();
2748
+ }
2749
+ }
2750
+
2751
+ attachEventListeners() {
2752
+ this.editor.addEventListener('input', () => {
2753
+ this.makeExistingLinksNonEditable();
2754
+ this.saveState();
2755
+ this.triggerChange();
2756
+ this.updateWordCount();
2757
+ });
2758
+
2759
+ this.editor.addEventListener('keydown', (e) => {
2760
+ if (e.ctrlKey && e.key === 'b') {
2761
+ e.preventDefault();
2762
+ this.execCommand('bold');
2763
+ } else if (e.ctrlKey && e.key === 'i') {
2764
+ e.preventDefault();
2765
+ this.execCommand('italic');
2766
+ } else if (e.ctrlKey && e.key === 'u') {
2767
+ e.preventDefault();
2768
+ this.execCommand('underline');
2769
+ } else if (e.ctrlKey && e.key === 'k') {
2770
+
2771
+ e.preventDefault();
2772
+ const selection = window.getSelection();
2773
+ if (selection.rangeCount > 0) {
2774
+ let node = selection.anchorNode;
2775
+
2776
+ while (node && node !== this.editor) {
2777
+ if (node.nodeType === 1 && node.tagName === 'A') {
2778
+ this.editLink(node);
2779
+ return;
2780
+ }
2781
+ node = node.parentNode;
2782
+ }
2783
+ }
2784
+
2785
+ this.showLinkModal();
2786
+ } else if (e.ctrlKey && e.key === 'z' && !e.shiftKey) {
2787
+ e.preventDefault();
2788
+ this.undo();
2789
+ } else if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) {
2790
+ e.preventDefault();
2791
+ this.redo();
2792
+ } else if (e.key === 'F11') {
2793
+ e.preventDefault();
2794
+ this.toggleFullscreen();
2795
+ } else if (e.key === 'Escape' && this.isFullscreen) {
2796
+ e.preventDefault();
2797
+ this.toggleFullscreen();
2798
+ } else if (e.key === 'Escape' && this.linkPopup) {
2799
+
2800
+ e.preventDefault();
2801
+ this.closeLinkPopup();
2802
+ }
2803
+ });
2804
+
2805
+ this.editor.addEventListener('mouseup', () => this.updateToolbarStates());
2806
+ this.editor.addEventListener('keyup', () => this.updateToolbarStates());
2807
+
2808
+ this.editor.addEventListener('beforeinput', (e) => {
2809
+ const selection = window.getSelection();
2810
+ if (selection.rangeCount > 0) {
2811
+ let node = selection.anchorNode;
2812
+
2813
+ while (node && node !== this.editor) {
2814
+ if (node.nodeType === 1 && node.tagName === 'A') {
2815
+
2816
+ if (e.inputType.startsWith('delete') ||
2817
+ e.inputType.startsWith('insert') ||
2818
+ e.inputType.startsWith('format') ||
2819
+ e.inputType === 'historyUndo' ||
2820
+ e.inputType === 'historyRedo') {
2821
+ e.preventDefault();
2822
+
2823
+ node.style.backgroundColor = 'rgba(255, 152, 0, 0.2)';
2824
+ setTimeout(() => {
2825
+ node.style.backgroundColor = '';
2826
+ }, 200);
2827
+ return;
2828
+ }
2829
+ }
2830
+ node = node.parentNode;
2831
+ }
2832
+ }
2833
+ });
2834
+
2835
+ this.editor.addEventListener('click', (e) => {
2836
+ if (e.target.tagName === 'A') {
2837
+ e.preventDefault();
2838
+ this.showLinkPopup(e.target);
2839
+ }
2840
+ });
2841
+
2842
+ this.editor.addEventListener('mousedown', (e) => {
2843
+ if (e.target.tagName === 'A') {
2844
+ e.preventDefault();
2845
+ }
2846
+ });
2847
+
2848
+ document.addEventListener('click', (e) => {
2849
+ if (!e.target.closest('.editium-dropdown')) {
2850
+ this.closeDropdown();
2851
+ }
2852
+
2853
+ if (this.linkPopup && !e.target.closest('.editium-link-popup') && !e.target.closest('a')) {
2854
+ this.closeLinkPopup();
2855
+ }
2856
+ });
2857
+ }
2858
+
2859
+ updateToolbarStates() {
2860
+ if (!this.toolbarElement) return;
2861
+
2862
+ const commands = {
2863
+ 'bold': 'bold',
2864
+ 'italic': 'italic',
2865
+ 'underline': 'underline',
2866
+ 'strikethrough': 'strikeThrough'
2867
+ };
2868
+
2869
+ Object.entries(commands).forEach(([type, cmd]) => {
2870
+ const isActive = document.queryCommandState(cmd);
2871
+ const button = this.toolbarElement.querySelector(`[data-command="${type}"]`);
2872
+ if (button) {
2873
+ button.classList.toggle('active', isActive);
2874
+ }
2875
+ });
2876
+ }
2877
+
2878
+ updateWordCount() {
2879
+ if (!this.wordCountElement) return;
2880
+
2881
+ let statsHTML = '';
2882
+
2883
+ if (this.showWordCount) {
2884
+ const text = this.editor.textContent || '';
2885
+ const words = text.trim().split(/\s+/).filter(w => w.length > 0).length;
2886
+ const chars = text.length;
2887
+ const charsNoSpaces = text.replace(/\s/g, '').length;
2888
+
2889
+ statsHTML = `
2890
+ <div class="editium-word-count-stats">
2891
+ <span>Words: ${words}</span>
2892
+ <span>Characters: ${chars}</span>
2893
+ <span>Characters (no spaces): ${charsNoSpaces}</span>
2894
+ </div>
2895
+ `;
2896
+ }
2897
+
2898
+ this.wordCountElement.innerHTML = `
2899
+ ${statsHTML}
2900
+ <div class="editium-word-count-branding">
2901
+ Built with <a href="https://www.npmjs.com/package/editium" target="_blank" rel="noopener noreferrer" class="editium-brand">Editium</a>
2902
+ </div>
2903
+ `;
2904
+ }
2905
+
2906
+ triggerChange() {
2907
+ this.onChange({
2908
+ html: this.getHTML(),
2909
+ json: this.getJSON(),
2910
+ text: this.getText()
2911
+ });
2912
+ }
2913
+
2914
+ getHTML() {
2915
+
2916
+ const clone = this.editor.cloneNode(true);
2917
+
2918
+ const toolbars = clone.querySelectorAll('.editium-image-toolbar');
2919
+ toolbars.forEach(toolbar => toolbar.remove());
2920
+
2921
+ const wrappers = clone.querySelectorAll('.editium-image-wrapper');
2922
+ wrappers.forEach(wrapper => {
2923
+
2924
+ const alignment = wrapper.classList.contains('align-center') ? 'center' :
2925
+ wrapper.classList.contains('align-right') ? 'right' : 'left';
2926
+
2927
+ wrapper.className = '';
2928
+ wrapper.removeAttribute('contenteditable');
2929
+
2930
+ wrapper.style.textAlign = alignment;
2931
+ wrapper.style.margin = '10px 0';
2932
+ wrapper.style.display = 'block';
2933
+ });
2934
+
2935
+ const containers = clone.querySelectorAll('div[style*="position: relative"]');
2936
+ containers.forEach(container => {
2937
+ if (container.querySelector('img')) {
2938
+
2939
+ container.style.position = '';
2940
+ container.style.display = '';
2941
+ }
2942
+ });
2943
+
2944
+ const images = clone.querySelectorAll('img');
2945
+ images.forEach(img => {
2946
+ img.classList.remove('resizable', 'resizing');
2947
+ img.removeAttribute('draggable');
2948
+ });
2949
+
2950
+ let html = clone.innerHTML;
2951
+
2952
+ if (html === '<p><br></p>' || html === '<h1><br></h1>' || html.match(/^<[^>]+><br><\/[^>]+>$/)) {
2953
+ return '';
2954
+ }
2955
+
2956
+ return html;
2957
+ }
2958
+
2959
+ getText() {
2960
+ return this.editor.textContent || '';
2961
+ }
2962
+
2963
+ getJSON() {
2964
+ const nodes = [];
2965
+ const editorContent = this.editor.cloneNode(true);
2966
+
2967
+ editorContent.querySelectorAll('.editium-image-toolbar').forEach(el => el.remove());
2968
+
2969
+ Array.from(editorContent.childNodes).forEach(node => {
2970
+ const parsed = this.parseNodeToJSON(node);
2971
+ if (parsed) {
2972
+ nodes.push(parsed);
2973
+ }
2974
+ });
2975
+
2976
+ return nodes;
2977
+ }
2978
+
2979
+ parseNodeToJSON(node) {
2980
+
2981
+ if (node.nodeType === Node.TEXT_NODE) {
2982
+ const text = node.textContent;
2983
+ if (text.trim() === '') return null;
2984
+ return { text: text };
2985
+ }
2986
+
2987
+ if (node.nodeType === Node.ELEMENT_NODE) {
2988
+ const tagName = node.tagName.toLowerCase();
2989
+
2990
+ const typeMap = {
2991
+ 'p': 'paragraph',
2992
+ 'h1': 'heading-one',
2993
+ 'h2': 'heading-two',
2994
+ 'h3': 'heading-three',
2995
+ 'h4': 'heading-four',
2996
+ 'h5': 'heading-five',
2997
+ 'h6': 'heading-six',
2998
+ 'ul': 'bulleted-list',
2999
+ 'ol': 'numbered-list',
3000
+ 'li': 'list-item',
3001
+ 'blockquote': 'quote',
3002
+ 'pre': 'code-block',
3003
+ 'a': 'link',
3004
+ 'img': 'image',
3005
+ 'table': 'table',
3006
+ 'tr': 'table-row',
3007
+ 'td': 'table-cell',
3008
+ 'th': 'table-header',
3009
+ 'hr': 'horizontal-rule'
3010
+ };
3011
+
3012
+ const type = typeMap[tagName] || 'paragraph';
3013
+ const result = { type };
3014
+
3015
+ if (tagName === 'a') {
3016
+ result.url = node.getAttribute('href') || '';
3017
+ } else if (tagName === 'img') {
3018
+ const wrapper = node.closest('.editium-image-wrapper');
3019
+ return {
3020
+ type: 'image',
3021
+ url: node.getAttribute('src') || '',
3022
+ alt: node.getAttribute('alt') || '',
3023
+ width: node.style.width || node.getAttribute('width') || null,
3024
+ alignment: wrapper ? (wrapper.classList.contains('align-left') ? 'left' : wrapper.classList.contains('align-right') ? 'right' : 'center') : 'left'
3025
+ };
3026
+ } else if (tagName === 'hr') {
3027
+ return { type: 'horizontal-rule' };
3028
+ } else if (tagName === 'br') {
3029
+ return null;
3030
+ } else if (tagName === 'div' && node.classList.contains('editium-image-wrapper')) {
3031
+
3032
+ const img = node.querySelector('img');
3033
+ if (img) {
3034
+ return this.parseNodeToJSON(img);
3035
+ }
3036
+ return null;
3037
+ }
3038
+
3039
+ const children = [];
3040
+ Array.from(node.childNodes).forEach(child => {
3041
+ const parsed = this.parseInlineNode(child);
3042
+ if (parsed) {
3043
+ children.push(parsed);
3044
+ }
3045
+ });
3046
+
3047
+ if (children.length === 0) {
3048
+ children.push({ text: '' });
3049
+ }
3050
+
3051
+ result.children = children;
3052
+ return result;
3053
+ }
3054
+
3055
+ return null;
3056
+ }
3057
+
3058
+ parseInlineNode(node) {
3059
+
3060
+ if (node.nodeType === Node.TEXT_NODE) {
3061
+ const text = node.textContent;
3062
+ if (text === '') return null;
3063
+ return { text: text };
3064
+ }
3065
+
3066
+ if (node.nodeType === Node.ELEMENT_NODE) {
3067
+ const tagName = node.tagName.toLowerCase();
3068
+
3069
+ const marks = {};
3070
+
3071
+ if (tagName === 'strong' || tagName === 'b') {
3072
+ marks.bold = true;
3073
+ } else if (tagName === 'em' || tagName === 'i') {
3074
+ marks.italic = true;
3075
+ } else if (tagName === 'u') {
3076
+ marks.underline = true;
3077
+ } else if (tagName === 's' || tagName === 'strike') {
3078
+ marks.strikethrough = true;
3079
+ } else if (tagName === 'code') {
3080
+ marks.code = true;
3081
+ } else if (tagName === 'sub') {
3082
+ marks.subscript = true;
3083
+ } else if (tagName === 'sup') {
3084
+ marks.superscript = true;
3085
+ } else if (tagName === 'a') {
3086
+ return {
3087
+ type: 'link',
3088
+ url: node.getAttribute('href') || '',
3089
+ children: this.parseInlineChildren(node)
3090
+ };
3091
+ } else if (tagName === 'span') {
3092
+
3093
+ const style = node.getAttribute('style') || '';
3094
+ if (style.includes('color:')) {
3095
+ const colorMatch = style.match(/color:\s*([^;]+)/);
3096
+ if (colorMatch) {
3097
+ marks.color = colorMatch[1].trim();
3098
+ }
3099
+ }
3100
+ if (style.includes('background-color:')) {
3101
+ const bgMatch = style.match(/background-color:\s*([^;]+)/);
3102
+ if (bgMatch) {
3103
+ marks.backgroundColor = bgMatch[1].trim();
3104
+ }
3105
+ }
3106
+ } else if (tagName === 'br') {
3107
+ return { text: '\n' };
3108
+ } else if (tagName === 'img') {
3109
+
3110
+ return {
3111
+ type: 'image',
3112
+ url: node.getAttribute('src') || '',
3113
+ alt: node.getAttribute('alt') || '',
3114
+ children: [{ text: '' }]
3115
+ };
3116
+ }
3117
+
3118
+ const blockElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'table', 'tr', 'td', 'th'];
3119
+ if (blockElements.includes(tagName)) {
3120
+ return this.parseNodeToJSON(node);
3121
+ }
3122
+
3123
+ const children = this.parseInlineChildren(node);
3124
+
3125
+ return children.map(child => {
3126
+ if (child.text !== undefined) {
3127
+ return { ...child, ...marks };
3128
+ }
3129
+ return child;
3130
+ });
3131
+ }
3132
+
3133
+ return null;
3134
+ }
3135
+
3136
+ parseInlineChildren(node) {
3137
+ const children = [];
3138
+ Array.from(node.childNodes).forEach(child => {
3139
+ const parsed = this.parseInlineNode(child);
3140
+ if (parsed) {
3141
+ if (Array.isArray(parsed)) {
3142
+ children.push(...parsed);
3143
+ } else {
3144
+ children.push(parsed);
3145
+ }
3146
+ }
3147
+ });
3148
+
3149
+ if (children.length === 0) {
3150
+ return [{ text: '' }];
3151
+ }
3152
+
3153
+ return children;
3154
+ }
3155
+
3156
+ setContent(content) {
3157
+ if (typeof content === 'string') {
3158
+ this.editor.innerHTML = content;
3159
+ } else if (typeof content === 'object' && content.content) {
3160
+ this.editor.innerHTML = content.content;
3161
+ }
3162
+
3163
+ this.makeExistingImagesResizable();
3164
+
3165
+ this.saveState();
3166
+ this.triggerChange();
3167
+ }
3168
+
3169
+ makeExistingImagesResizable() {
3170
+ const images = this.editor.querySelectorAll('img');
3171
+ images.forEach(img => {
3172
+
3173
+ const parent = img.parentElement;
3174
+
3175
+ if (parent && parent.classList.contains('editium-image-wrapper')) {
3176
+
3177
+ if (!img.classList.contains('resizable')) {
3178
+ img.classList.add('resizable');
3179
+ img.draggable = false;
3180
+ this.makeImageResizable(img);
3181
+ }
3182
+
3183
+ if (!parent.querySelector('.editium-image-toolbar')) {
3184
+ const toolbar = this.createImageToolbar(parent, img);
3185
+ const container = img.parentElement;
3186
+ if (container) {
3187
+ container.appendChild(toolbar);
3188
+ }
3189
+ }
3190
+ } else {
3191
+
3192
+ const wrapper = document.createElement('div');
3193
+ wrapper.className = 'editium-image-wrapper align-left';
3194
+ wrapper.contentEditable = 'false';
3195
+ wrapper.style.textAlign = 'left';
3196
+
3197
+ const container = document.createElement('div');
3198
+ container.style.position = 'relative';
3199
+ container.style.display = 'inline-block';
3200
+
3201
+ img.parentNode.insertBefore(wrapper, img);
3202
+ img.classList.add('resizable');
3203
+ img.draggable = false;
3204
+
3205
+ if (!img.style.marginLeft && !img.style.marginRight) {
3206
+ img.style.marginLeft = '0';
3207
+ img.style.marginRight = 'auto';
3208
+ }
3209
+
3210
+ container.appendChild(img);
3211
+ const toolbar = this.createImageToolbar(wrapper, img);
3212
+ container.appendChild(toolbar);
3213
+ wrapper.appendChild(container);
3214
+
3215
+ this.makeImageResizable(img);
3216
+ }
3217
+ });
3218
+ }
3219
+
3220
+ makeExistingLinksNonEditable() {
3221
+ const links = this.editor.querySelectorAll('a');
3222
+ links.forEach(link => {
3223
+ link.contentEditable = 'false';
3224
+ });
3225
+ }
3226
+
3227
+ clear() {
3228
+ this.editor.innerHTML = '<p><br></p>';
3229
+ this.saveState();
3230
+ this.triggerChange();
3231
+ }
3232
+
3233
+ focus() {
3234
+ this.editor.focus();
3235
+ }
3236
+
3237
+ destroy() {
3238
+
3239
+ if (this.isFullscreen) {
3240
+ document.body.classList.remove('editium-fullscreen-active');
3241
+ }
3242
+ this.container.innerHTML = '';
3243
+ }
3244
+ }
3245
+
3246
+ if (typeof module !== 'undefined' && module.exports) {
3247
+ module.exports = Editium;
3248
+ }
3249
+ if (typeof window !== 'undefined') {
3250
+ window.Editium = Editium;
3251
+ }
3252
+
3253
+
3254
+ if (typeof module !== 'undefined' && module.exports) module.exports = Editium;
3255
+ if (typeof window !== 'undefined') window.Editium = Editium;
3256
+
3257
+ })();