panda-editor 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,957 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Panda Editor - Footnote Tool Test</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ max-width: 900px;
11
+ margin: 0 auto;
12
+ padding: 40px 20px;
13
+ background: #f9fafb;
14
+ }
15
+
16
+ h1 {
17
+ color: #111827;
18
+ margin-bottom: 10px;
19
+ }
20
+
21
+ .subtitle {
22
+ color: #6b7280;
23
+ margin-bottom: 30px;
24
+ font-size: 14px;
25
+ }
26
+
27
+ .test-container {
28
+ background: white;
29
+ border-radius: 8px;
30
+ padding: 30px;
31
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
32
+ margin-bottom: 30px;
33
+ }
34
+
35
+ .section-title {
36
+ font-size: 18px;
37
+ font-weight: 600;
38
+ margin-bottom: 15px;
39
+ color: #374151;
40
+ }
41
+
42
+ #editorjs {
43
+ border: 1px solid #e5e7eb;
44
+ border-radius: 6px;
45
+ min-height: 300px;
46
+ background: white;
47
+ }
48
+
49
+ .button-group {
50
+ display: flex;
51
+ gap: 10px;
52
+ margin-top: 20px;
53
+ }
54
+
55
+ button {
56
+ padding: 10px 20px;
57
+ border: 1px solid #d1d5db;
58
+ border-radius: 6px;
59
+ background: white;
60
+ color: #374151;
61
+ font-size: 14px;
62
+ font-weight: 500;
63
+ cursor: pointer;
64
+ transition: all 0.2s;
65
+ }
66
+
67
+ button:hover {
68
+ background: #f9fafb;
69
+ border-color: #9ca3af;
70
+ }
71
+
72
+ button.primary {
73
+ background: #3b82f6;
74
+ color: white;
75
+ border-color: #3b82f6;
76
+ }
77
+
78
+ button.primary:hover {
79
+ background: #2563eb;
80
+ border-color: #2563eb;
81
+ }
82
+
83
+ #output {
84
+ background: #f9fafb;
85
+ border: 1px solid #e5e7eb;
86
+ border-radius: 6px;
87
+ padding: 20px;
88
+ font-family: 'Monaco', 'Courier New', monospace;
89
+ font-size: 12px;
90
+ line-height: 1.6;
91
+ overflow-x: auto;
92
+ white-space: pre-wrap;
93
+ word-wrap: break-word;
94
+ }
95
+
96
+ #rendered {
97
+ background: white;
98
+ border: 1px solid #e5e7eb;
99
+ border-radius: 6px;
100
+ padding: 20px;
101
+ min-height: 100px;
102
+ line-height: 1.8;
103
+ }
104
+
105
+ .instructions {
106
+ background: #fef3c7;
107
+ border: 1px solid #fbbf24;
108
+ border-radius: 6px;
109
+ padding: 15px;
110
+ margin-bottom: 20px;
111
+ font-size: 14px;
112
+ line-height: 1.6;
113
+ }
114
+
115
+ .instructions h3 {
116
+ margin: 0 0 10px 0;
117
+ color: #92400e;
118
+ font-size: 16px;
119
+ }
120
+
121
+ .instructions ol {
122
+ margin: 10px 0 0 20px;
123
+ padding: 0;
124
+ }
125
+
126
+ .instructions li {
127
+ margin-bottom: 5px;
128
+ color: #78350f;
129
+ }
130
+
131
+ /* EditorJS Styles */
132
+ .codex-editor {
133
+ position: relative;
134
+ }
135
+
136
+ .codex-editor::before {
137
+ content: '';
138
+ position: absolute;
139
+ left: 0;
140
+ top: 0;
141
+ bottom: 0;
142
+ width: 65px;
143
+ margin-right: 5px;
144
+ background-color: #f9fafb;
145
+ border-right: 2px dashed #e5e7eb;
146
+ z-index: 0;
147
+ }
148
+
149
+ .ce-block {
150
+ padding-left: 70px;
151
+ position: relative;
152
+ min-height: 40px;
153
+ margin: 0;
154
+ padding-bottom: 1em;
155
+ }
156
+
157
+ .ce-block__content {
158
+ position: relative;
159
+ max-width: none;
160
+ margin: 0;
161
+ }
162
+
163
+ .ce-paragraph {
164
+ padding: 0;
165
+ line-height: 1.6;
166
+ min-height: 1.6em;
167
+ margin: 0;
168
+ }
169
+
170
+ /* Footnote marker styles */
171
+ .footnote-marker {
172
+ display: inline-block;
173
+ color: #3b82f6;
174
+ font-size: 0.75em;
175
+ font-weight: 600;
176
+ vertical-align: super;
177
+ cursor: pointer;
178
+ padding: 0 2px;
179
+ user-select: none;
180
+ margin-left: 1px;
181
+ }
182
+
183
+ .footnote-marker:hover {
184
+ color: #2563eb;
185
+ text-decoration: underline;
186
+ }
187
+
188
+ /* Toolbar styles */
189
+ .ce-toolbar {
190
+ left: 0 !important;
191
+ right: auto !important;
192
+ background: none !important;
193
+ position: absolute !important;
194
+ width: 65px !important;
195
+ height: 40px !important;
196
+ display: flex !important;
197
+ align-items: center !important;
198
+ justify-content: flex-start !important;
199
+ padding: 0 !important;
200
+ margin-left: -70px !important;
201
+ margin-top: -5px !important;
202
+ opacity: 1 !important;
203
+ visibility: visible !important;
204
+ pointer-events: all !important;
205
+ z-index: 2 !important;
206
+ }
207
+
208
+ .status-message {
209
+ padding: 12px;
210
+ border-radius: 6px;
211
+ margin-bottom: 15px;
212
+ font-size: 14px;
213
+ }
214
+
215
+ .status-message.info {
216
+ background: #dbeafe;
217
+ border: 1px solid #3b82f6;
218
+ color: #1e40af;
219
+ }
220
+
221
+ .status-message.error {
222
+ background: #fee2e2;
223
+ border: 1px solid #ef4444;
224
+ color: #991b1b;
225
+ }
226
+
227
+ .status-message.success {
228
+ background: #d1fae5;
229
+ border: 1px solid #10b981;
230
+ color: #065f46;
231
+ }
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <h1>🦾 Panda Editor - Footnote Tool Test</h1>
236
+ <p class="subtitle">Testing the new EditorJS inline footnote tool with auto-generated IDs</p>
237
+
238
+ <div class="test-container">
239
+ <div id="status-area"></div>
240
+
241
+ <div class="instructions">
242
+ <h3>📋 How to Test</h3>
243
+ <ol>
244
+ <li>Wait for the editor to load (status message will appear)</li>
245
+ <li>Type some text in the editor below</li>
246
+ <li>Select a word or phrase where you want to add a footnote</li>
247
+ <li>Click the <strong>footnote button (fn)</strong> in the inline toolbar</li>
248
+ <li>Enter a citation in the modal (e.g., "Smith, J. (2023). Research Study.")</li>
249
+ <li>Click "Add Footnote" or press Cmd/Ctrl+Enter</li>
250
+ <li>You should see a blue <strong>numbered marker</strong> appear (1, 2, 3, etc.)</li>
251
+ <li>Try adding multiple footnotes - they automatically renumber!</li>
252
+ <li>Click "Get JSON Data" to see the footnote data structure</li>
253
+ </ol>
254
+ </div>
255
+
256
+ <h2 class="section-title">Editor</h2>
257
+ <div id="editorjs"></div>
258
+
259
+ <div class="button-group">
260
+ <button class="primary" onclick="saveData()">Get JSON Data</button>
261
+ <button onclick="clearEditor()">Clear Editor</button>
262
+ <button onclick="loadSample()">Load Sample with Footnotes</button>
263
+ </div>
264
+ </div>
265
+
266
+ <div class="test-container">
267
+ <h2 class="section-title">JSON Output</h2>
268
+ <div id="output">Click "Get JSON Data" to see the editor content with footnote data...</div>
269
+ </div>
270
+
271
+ <div class="test-container">
272
+ <h2 class="section-title">Rendered Preview (simulated)</h2>
273
+ <div id="rendered">The rendered HTML will appear here after you save...</div>
274
+ </div>
275
+
276
+ <!-- Load EditorJS and tools from CDN -->
277
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
278
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
279
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.1"></script>
280
+
281
+ <script>
282
+ // Status helper
283
+ function showStatus(message, type = 'info') {
284
+ const statusArea = document.getElementById('status-area');
285
+ statusArea.innerHTML = `<div class="status-message ${type}">${message}</div>`;
286
+ }
287
+
288
+ showStatus('⏳ Loading footnote tool...', 'info');
289
+
290
+ // NOTE: For this standalone test, we'll use a simplified footnote implementation
291
+ // The full implementation requires serving from a web server due to ES6 module restrictions
292
+
293
+ /**
294
+ * Simplified FootnoteTool for testing
295
+ */
296
+ class FootnoteTool {
297
+ static get isInline() {
298
+ return true;
299
+ }
300
+
301
+ static get title() {
302
+ return 'Footnote';
303
+ }
304
+
305
+ static get sanitize() {
306
+ return {
307
+ sup: {
308
+ class: 'footnote-marker',
309
+ 'data-footnote-id': true,
310
+ 'data-footnote-content': true
311
+ }
312
+ };
313
+ }
314
+
315
+ constructor({ api }) {
316
+ this.api = api;
317
+ this.button = null;
318
+ this.state = false;
319
+
320
+ this.CSS = {
321
+ button: 'ce-inline-tool',
322
+ buttonActive: 'ce-inline-tool--active',
323
+ buttonModifier: 'ce-inline-tool--footnote'
324
+ };
325
+
326
+ this.iconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
327
+ <text x="2" y="16" font-size="12" font-weight="bold" fill="currentColor">fn</text>
328
+ </svg>`;
329
+ }
330
+
331
+ render() {
332
+ this.button = document.createElement('button');
333
+ this.button.type = 'button';
334
+ this.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
335
+ this.button.innerHTML = this.iconSVG;
336
+ this.button.title = 'Add Footnote';
337
+
338
+ return this.button;
339
+ }
340
+
341
+ surround(range) {
342
+ if (this.state) {
343
+ this.unwrap(range);
344
+ return;
345
+ }
346
+
347
+ const position = this.getCaretPosition(range);
348
+
349
+ this.showFootnoteModal((content) => {
350
+ if (!content || content.trim() === '') {
351
+ return;
352
+ }
353
+
354
+ const footnoteId = this.generateFootnoteId();
355
+ this.wrap(range, footnoteId, content.trim());
356
+ });
357
+ }
358
+
359
+ wrap(range, footnoteId, content) {
360
+ const marker = document.createElement('sup');
361
+ marker.classList.add('footnote-marker');
362
+ marker.dataset.footnoteId = footnoteId;
363
+ marker.dataset.footnoteContent = content;
364
+ marker.contentEditable = false;
365
+
366
+ range.collapse(false);
367
+ range.insertNode(marker);
368
+
369
+ range.setStartAfter(marker);
370
+ range.collapse(true);
371
+
372
+ const selection = window.getSelection();
373
+ selection.removeAllRanges();
374
+ selection.addRange(range);
375
+
376
+ // Renumber all footnotes in the document
377
+ setTimeout(() => this.renumberAllFootnotes(), 0);
378
+ }
379
+
380
+ unwrap(range) {
381
+ const marker = this.findMarkerInRange(range);
382
+ if (marker) {
383
+ marker.remove();
384
+
385
+ // Renumber remaining footnotes
386
+ setTimeout(() => this.renumberAllFootnotes(), 0);
387
+ }
388
+ }
389
+
390
+ checkState(selection) {
391
+ if (!selection || !selection.anchorNode) {
392
+ this.state = false;
393
+ return false;
394
+ }
395
+
396
+ const marker = this.findMarkerInSelection(selection);
397
+ this.state = !!marker;
398
+
399
+ return this.state;
400
+ }
401
+
402
+ findMarkerInSelection(selection) {
403
+ if (!selection.anchorNode) return null;
404
+
405
+ let node = selection.anchorNode;
406
+
407
+ while (node) {
408
+ if (node.nodeType === Node.ELEMENT_NODE &&
409
+ node.tagName === 'SUP' &&
410
+ node.classList.contains('footnote-marker')) {
411
+ return node;
412
+ }
413
+ node = node.parentNode;
414
+ }
415
+
416
+ return null;
417
+ }
418
+
419
+ findMarkerInRange(range) {
420
+ const container = range.commonAncestorContainer;
421
+
422
+ if (container.nodeType === Node.ELEMENT_NODE &&
423
+ container.classList && container.classList.contains('footnote-marker')) {
424
+ return container;
425
+ }
426
+
427
+ const markers = container.querySelectorAll?.('.footnote-marker');
428
+ return markers?.[0] || null;
429
+ }
430
+
431
+ getCaretPosition(range) {
432
+ const block = this.api.blocks.getBlockByIndex(this.api.blocks.getCurrentBlockIndex());
433
+ if (!block) return 0;
434
+
435
+ const blockElement = block.holder;
436
+ const contentElement = blockElement.querySelector('.ce-paragraph');
437
+
438
+ if (!contentElement) return 0;
439
+
440
+ const preCaretRange = range.cloneRange();
441
+ preCaretRange.selectNodeContents(contentElement);
442
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
443
+
444
+ return preCaretRange.toString().length;
445
+ }
446
+
447
+ generateFootnoteId() {
448
+ return `fn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
449
+ }
450
+
451
+ renumberAllFootnotes() {
452
+ try {
453
+ const blocksCount = this.api.blocks.getBlocksCount();
454
+ let footnoteNumber = 0;
455
+
456
+ // Scan through all blocks in order
457
+ for (let i = 0; i < blocksCount; i++) {
458
+ const block = this.api.blocks.getBlockByIndex(i);
459
+ if (!block) continue;
460
+
461
+ const blockElement = block.holder;
462
+ const markers = blockElement.querySelectorAll('.footnote-marker');
463
+
464
+ // Update each marker in this block
465
+ markers.forEach(marker => {
466
+ footnoteNumber++;
467
+ marker.textContent = footnoteNumber.toString();
468
+ });
469
+ }
470
+
471
+ console.debug('[Footnote Tool] Renumbered', footnoteNumber, 'footnotes');
472
+ } catch (error) {
473
+ console.error('[Footnote Tool] Error renumbering footnotes:', error);
474
+ }
475
+ }
476
+
477
+ showFootnoteModal(onSave) {
478
+ const overlay = document.createElement('div');
479
+ overlay.style.cssText = `
480
+ position: fixed;
481
+ top: 0;
482
+ left: 0;
483
+ right: 0;
484
+ bottom: 0;
485
+ background: rgba(0, 0, 0, 0.5);
486
+ display: flex;
487
+ align-items: center;
488
+ justify-content: center;
489
+ z-index: 10000;
490
+ `;
491
+
492
+ const modal = document.createElement('div');
493
+ modal.style.cssText = `
494
+ background: white;
495
+ border-radius: 8px;
496
+ padding: 24px;
497
+ max-width: 500px;
498
+ width: 90%;
499
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
500
+ `;
501
+
502
+ modal.innerHTML = `
503
+ <h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600;">Add Footnote</h3>
504
+ <div style="margin-bottom: 16px;">
505
+ <label style="display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500;">
506
+ Citation Content
507
+ </label>
508
+ <textarea
509
+ class="footnote-content-input"
510
+ placeholder="Enter citation or reference (e.g., Smith, J. et al. (2023). Study Title. Journal Name.)"
511
+ style="
512
+ width: 100%;
513
+ min-height: 100px;
514
+ padding: 8px 12px;
515
+ border: 1px solid #d1d5db;
516
+ border-radius: 4px;
517
+ font-size: 14px;
518
+ font-family: inherit;
519
+ resize: vertical;
520
+ box-sizing: border-box;
521
+ "
522
+ ></textarea>
523
+ <p style="margin: 8px 0 0 0; font-size: 12px; color: #6b7280;">
524
+ Tip: You can paste URLs directly - they'll be automatically converted to clickable links.
525
+ </p>
526
+ </div>
527
+ <div style="display: flex; gap: 8px; justify-content: flex-end;">
528
+ <button class="footnote-cancel-btn" style="
529
+ padding: 8px 16px;
530
+ border: 1px solid #d1d5db;
531
+ border-radius: 4px;
532
+ background: white;
533
+ color: #374151;
534
+ font-size: 14px;
535
+ font-weight: 500;
536
+ cursor: pointer;
537
+ ">Cancel</button>
538
+ <button class="footnote-save-btn" style="
539
+ padding: 8px 16px;
540
+ border: none;
541
+ border-radius: 4px;
542
+ background: #3b82f6;
543
+ color: white;
544
+ font-size: 14px;
545
+ font-weight: 500;
546
+ cursor: pointer;
547
+ ">Add Footnote</button>
548
+ </div>
549
+ `;
550
+
551
+ overlay.appendChild(modal);
552
+ document.body.appendChild(overlay);
553
+
554
+ const textarea = modal.querySelector('.footnote-content-input');
555
+ const saveBtn = modal.querySelector('.footnote-save-btn');
556
+ const cancelBtn = modal.querySelector('.footnote-cancel-btn');
557
+
558
+ setTimeout(() => textarea.focus(), 0);
559
+
560
+ const handleSave = () => {
561
+ const content = textarea.value;
562
+ onSave(content);
563
+ overlay.remove();
564
+ };
565
+
566
+ const handleCancel = () => {
567
+ overlay.remove();
568
+ };
569
+
570
+ saveBtn.addEventListener('click', handleSave);
571
+ cancelBtn.addEventListener('click', handleCancel);
572
+ overlay.addEventListener('click', (e) => {
573
+ if (e.target === overlay) {
574
+ handleCancel();
575
+ }
576
+ });
577
+
578
+ textarea.addEventListener('keydown', (e) => {
579
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
580
+ e.preventDefault();
581
+ handleSave();
582
+ } else if (e.key === 'Escape') {
583
+ e.preventDefault();
584
+ handleCancel();
585
+ }
586
+ });
587
+ }
588
+
589
+ clear() {
590
+ this.state = false;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Custom Paragraph with Footnotes
596
+ */
597
+ class ParagraphWithFootnotes {
598
+ static get toolbox() {
599
+ return {
600
+ title: 'Paragraph',
601
+ icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>'
602
+ };
603
+ }
604
+
605
+ static get contentless() {
606
+ return false;
607
+ }
608
+
609
+ static get enableLineBreaks() {
610
+ return true;
611
+ }
612
+
613
+ static get DEFAULT_PLACEHOLDER() {
614
+ return 'Start writing or press Tab to add content...';
615
+ }
616
+
617
+ constructor({ data, config, api, readOnly }) {
618
+ this.api = api;
619
+ this.readOnly = readOnly;
620
+
621
+ this._CSS = {
622
+ block: this.api.styles.block,
623
+ wrapper: 'ce-paragraph'
624
+ };
625
+
626
+ if (!this.readOnly) {
627
+ this.onKeyUp = this.onKeyUp.bind(this);
628
+ }
629
+
630
+ this._placeholder = config.placeholder || ParagraphWithFootnotes.DEFAULT_PLACEHOLDER;
631
+ this._data = {};
632
+ this._element = null;
633
+ this._preserveBlank = config.preserveBlank !== undefined ? config.preserveBlank : false;
634
+
635
+ this.data = data;
636
+ }
637
+
638
+ render() {
639
+ const div = document.createElement('DIV');
640
+ div.classList.add(this._CSS.wrapper, this._CSS.block);
641
+ div.contentEditable = !this.readOnly;
642
+ div.dataset.placeholder = this.api.i18n.t(this._placeholder);
643
+
644
+ if (this._data.text) {
645
+ div.innerHTML = this._data.text;
646
+ }
647
+
648
+ if (!this.readOnly) {
649
+ div.addEventListener('keyup', this.onKeyUp);
650
+ }
651
+
652
+ this._element = div;
653
+
654
+ return div;
655
+ }
656
+
657
+ onKeyUp(event) {
658
+ if (event.code !== 'Backspace' && event.code !== 'Delete') {
659
+ return;
660
+ }
661
+
662
+ const { textContent } = this._element;
663
+
664
+ if (textContent === '') {
665
+ this._element.innerHTML = '';
666
+ }
667
+ }
668
+
669
+ validate(savedData) {
670
+ if (savedData.text?.trim() === '' && !this._preserveBlank) {
671
+ return false;
672
+ }
673
+
674
+ return true;
675
+ }
676
+
677
+ save(toolsContent) {
678
+ // Clone the element to work with it without modifying the DOM
679
+ const clone = toolsContent.cloneNode(true);
680
+
681
+ // Extract footnotes before removing markers
682
+ const footnotes = this.extractFootnotes(clone);
683
+
684
+ // Remove all footnote markers from the clone to get clean text
685
+ const markers = clone.querySelectorAll('.footnote-marker');
686
+ markers.forEach(marker => marker.remove());
687
+
688
+ const data = {
689
+ text: clone.innerHTML
690
+ };
691
+
692
+ // Add footnotes array if any exist
693
+ if (footnotes.length > 0) {
694
+ data.footnotes = footnotes;
695
+ }
696
+
697
+ return data;
698
+ }
699
+
700
+ extractFootnotes(element) {
701
+ const footnotes = [];
702
+ const markers = element.querySelectorAll('.footnote-marker');
703
+
704
+ markers.forEach((marker) => {
705
+ const footnoteId = marker.dataset.footnoteId;
706
+ const footnoteContent = marker.dataset.footnoteContent;
707
+
708
+ if (footnoteId && footnoteContent) {
709
+ const range = document.createRange();
710
+ range.selectNodeContents(element);
711
+ range.setEnd(marker, 0);
712
+ const position = range.toString().length;
713
+
714
+ footnotes.push({
715
+ id: footnoteId,
716
+ content: footnoteContent,
717
+ position: position
718
+ });
719
+ }
720
+ });
721
+
722
+ return footnotes;
723
+ }
724
+
725
+ merge(data) {
726
+ const newData = {
727
+ text: this.data.text + data.text
728
+ };
729
+
730
+ this.data = newData;
731
+ }
732
+
733
+ get data() {
734
+ let text = this._element ? this._element.innerHTML : this._data.text || '';
735
+
736
+ this._data.text = text;
737
+
738
+ if (this._element) {
739
+ const footnotes = this.extractFootnotes(this._element);
740
+ if (footnotes.length > 0) {
741
+ this._data.footnotes = footnotes;
742
+ }
743
+ }
744
+
745
+ return this._data;
746
+ }
747
+
748
+ set data(data) {
749
+ this._data = data || {};
750
+
751
+ if (this._element) {
752
+ this._element.innerHTML = this._data.text || '';
753
+ }
754
+ }
755
+
756
+ onPaste(event) {
757
+ const data = {
758
+ text: event.detail.data.innerHTML
759
+ };
760
+
761
+ this.data = data;
762
+ }
763
+
764
+ static get conversionConfig() {
765
+ return {
766
+ export: 'text',
767
+ import: 'text'
768
+ };
769
+ }
770
+
771
+ static get sanitize() {
772
+ return {
773
+ text: {
774
+ br: true,
775
+ sup: {
776
+ class: 'footnote-marker',
777
+ 'data-footnote-id': true,
778
+ 'data-footnote-content': true
779
+ }
780
+ }
781
+ };
782
+ }
783
+
784
+ static get isReadOnlySupported() {
785
+ return true;
786
+ }
787
+
788
+ get currentBlock() {
789
+ return this.api.blocks.getCurrentBlockIndex();
790
+ }
791
+ }
792
+
793
+ // Initialize EditorJS
794
+ try {
795
+ window.editor = new EditorJS({
796
+ holder: 'editorjs',
797
+ placeholder: 'Start writing and add footnotes...',
798
+ tools: {
799
+ paragraph: {
800
+ class: ParagraphWithFootnotes,
801
+ inlineToolbar: true,
802
+ config: {
803
+ placeholder: 'Click to start writing...'
804
+ }
805
+ },
806
+ header: {
807
+ class: Header,
808
+ inlineToolbar: true
809
+ },
810
+ footnote: {
811
+ class: FootnoteTool
812
+ }
813
+ },
814
+ data: {
815
+ blocks: [
816
+ {
817
+ type: 'paragraph',
818
+ data: {
819
+ text: 'Welcome to the Panda Editor footnote tool test! Select this text and click the footnote button to add a citation.'
820
+ }
821
+ }
822
+ ]
823
+ },
824
+ onReady: () => {
825
+ showStatus('✅ Editor loaded successfully! You can now add footnotes by selecting text and clicking the footnote button.', 'success');
826
+ }
827
+ });
828
+ } catch (error) {
829
+ showStatus('❌ Error loading editor: ' + error.message, 'error');
830
+ console.error('Editor initialization error:', error);
831
+ }
832
+
833
+ // Save function
834
+ window.saveData = async function() {
835
+ try {
836
+ const outputData = await window.editor.save();
837
+ document.getElementById('output').textContent = JSON.stringify(outputData, null, 2);
838
+
839
+ renderPreview(outputData);
840
+ showStatus('✅ Data saved successfully!', 'success');
841
+ } catch (error) {
842
+ console.error('Saving failed: ', error);
843
+ document.getElementById('output').textContent = 'Error: ' + error.message;
844
+ showStatus('❌ Error saving: ' + error.message, 'error');
845
+ }
846
+ };
847
+
848
+ // Clear function
849
+ window.clearEditor = async function() {
850
+ await window.editor.clear();
851
+ document.getElementById('output').textContent = 'Editor cleared.';
852
+ document.getElementById('rendered').innerHTML = 'The rendered HTML will appear here after you save...';
853
+ showStatus('Editor cleared.', 'info');
854
+ };
855
+
856
+ // Load sample function
857
+ window.loadSample = async function() {
858
+ await window.editor.render({
859
+ blocks: [
860
+ {
861
+ type: 'paragraph',
862
+ data: {
863
+ text: 'Climate change has accelerated significantly since 1980 with global temperatures rising 1.1°C above pre-industrial levels.',
864
+ footnotes: [
865
+ {
866
+ id: 'fn-sample-1',
867
+ content: 'IPCC. (2023). Climate Change 2023: Synthesis Report. https://www.ipcc.ch/report/ar6/syr/',
868
+ position: 57
869
+ },
870
+ {
871
+ id: 'fn-sample-2',
872
+ content: 'NASA. (2023). Global Climate Change: Vital Signs of the Planet.',
873
+ position: 116
874
+ }
875
+ ]
876
+ }
877
+ },
878
+ {
879
+ type: 'paragraph',
880
+ data: {
881
+ text: 'Renewable energy adoption has increased dramatically in recent years.'
882
+ }
883
+ }
884
+ ]
885
+ });
886
+ showStatus('Sample data loaded with two footnotes.', 'success');
887
+ };
888
+
889
+ // Render preview (simulated Ruby rendering)
890
+ window.renderPreview = function(data) {
891
+ let html = '';
892
+ const allFootnotes = [];
893
+ let footnoteCounter = 0;
894
+
895
+ // First pass: collect all footnotes with their block index and assign numbers
896
+ data.blocks.forEach((block, blockIndex) => {
897
+ if (block.type === 'paragraph' && block.data.footnotes && block.data.footnotes.length > 0) {
898
+ // Sort by position ascending (reading order) to assign numbers correctly
899
+ const sortedForNumbering = [...block.data.footnotes].sort((a, b) => a.position - b.position);
900
+
901
+ sortedForNumbering.forEach(fn => {
902
+ footnoteCounter++;
903
+ allFootnotes.push({
904
+ blockIndex: blockIndex,
905
+ footnote: fn,
906
+ number: footnoteCounter
907
+ });
908
+ });
909
+ }
910
+ });
911
+
912
+ // Second pass: render blocks with correct footnote numbers
913
+ data.blocks.forEach((block, blockIndex) => {
914
+ if (block.type === 'paragraph') {
915
+ let text = block.data.text;
916
+
917
+ if (block.data.footnotes && block.data.footnotes.length > 0) {
918
+ // Get footnotes for this block with their assigned numbers
919
+ const blockFootnotes = allFootnotes
920
+ .filter(item => item.blockIndex === blockIndex)
921
+ .sort((a, b) => b.footnote.position - a.footnote.position); // Sort descending for insertion
922
+
923
+ blockFootnotes.forEach(item => {
924
+ const marker = `<sup id="fnref:${item.number}"><a href="#fn:${item.number}" class="footnote" style="color: #3b82f6; text-decoration: none;">${item.number}</a></sup>`;
925
+ text = text.slice(0, item.footnote.position) + marker + text.slice(item.footnote.position);
926
+ });
927
+ }
928
+
929
+ html += `<p>${text}</p>`;
930
+ } else if (block.type === 'header') {
931
+ html += `<h${block.data.level}>${block.data.text}</h${block.data.level}>`;
932
+ }
933
+ });
934
+
935
+ // Render footnotes section in correct order
936
+ if (allFootnotes.length > 0) {
937
+ html += '<div style="margin-top: 30px; padding: 20px; background: #f9fafb; border-radius: 8px;">';
938
+ html += '<h3 style="margin: 0 0 15px 0; font-size: 16px;">Sources/References</h3>';
939
+ html += '<ol style="margin: 0; padding-left: 20px; line-height: 1.8;">';
940
+
941
+ // Sort by number to ensure correct order
942
+ allFootnotes.sort((a, b) => a.number - b.number).forEach(item => {
943
+ html += `<li id="fn:${item.number}" style="margin-bottom: 10px;">`;
944
+ html += `<p style="margin: 0;">${item.footnote.content} <a href="#fnref:${item.number}" style="color: #3b82f6; text-decoration: none;">↩</a></p>`;
945
+ html += '</li>';
946
+ });
947
+ html += '</ol></div>';
948
+ }
949
+
950
+ document.getElementById('rendered').innerHTML = html || 'No content yet.';
951
+ };
952
+
953
+ console.log('✅ Panda Editor footnote test loaded successfully!');
954
+ console.log('Editor instance available as window.editor');
955
+ </script>
956
+ </body>
957
+ </html>