@oix1987/yjd 1.0.3 → 2.1.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.
Files changed (73) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +223 -142
  3. package/core.js +82 -0
  4. package/dist/core.esm.js +2 -0
  5. package/dist/core.esm.js.map +1 -0
  6. package/dist/rich-editor.esm.js +1 -1
  7. package/dist/rich-editor.esm.js.map +1 -1
  8. package/dist/rich-editor.min.js +1 -1
  9. package/dist/rich-editor.min.js.map +1 -1
  10. package/index.d.ts +230 -103
  11. package/index.js +297 -0
  12. package/lib/core/editor.js +1885 -0
  13. package/lib/core/format.js +540 -0
  14. package/lib/core/module.js +81 -0
  15. package/lib/core/registry.js +158 -0
  16. package/lib/formats/background.js +213 -0
  17. package/lib/formats/bold.js +49 -0
  18. package/lib/formats/capitalization.js +579 -0
  19. package/lib/formats/color.js +183 -0
  20. package/lib/formats/emoji.js +282 -0
  21. package/lib/formats/font-family.js +548 -0
  22. package/lib/formats/heading.js +502 -0
  23. package/lib/formats/image.js +341 -0
  24. package/lib/formats/import.js +385 -0
  25. package/lib/formats/indent.js +297 -0
  26. package/lib/formats/italic.js +27 -0
  27. package/lib/formats/line-height.js +562 -0
  28. package/lib/formats/link.js +251 -0
  29. package/lib/formats/list.js +635 -0
  30. package/lib/formats/strike.js +31 -0
  31. package/lib/formats/subscript.js +40 -0
  32. package/lib/formats/superscript.js +39 -0
  33. package/lib/formats/table.js +293 -0
  34. package/lib/formats/tag.js +304 -0
  35. package/lib/formats/text-align.js +422 -0
  36. package/lib/formats/text-size.js +498 -0
  37. package/lib/formats/underline.js +30 -0
  38. package/lib/formats/video.js +381 -0
  39. package/lib/modules/block-toolbar.js +639 -0
  40. package/lib/modules/code-view.js +447 -0
  41. package/lib/modules/find-replace.js +273 -0
  42. package/lib/modules/history.js +425 -0
  43. package/lib/modules/mention.js +200 -0
  44. package/lib/modules/resize-handles.js +701 -0
  45. package/lib/modules/slash-menu.js +183 -0
  46. package/lib/modules/table-toolbar.js +635 -0
  47. package/lib/modules/toolbar.js +607 -0
  48. package/lib/serialize.js +241 -0
  49. package/lib/static.js +28 -0
  50. package/lib/styles-loader.js +142 -0
  51. package/{dist → lib}/styles.css +1392 -35
  52. package/lib/styles.css.js +2 -0
  53. package/lib/styles.min.css +1 -0
  54. package/lib/ui/color-picker.js +296 -0
  55. package/lib/ui/customselect.js +351 -0
  56. package/lib/ui/emoji-picker.js +196 -0
  57. package/lib/ui/icons.js +145 -0
  58. package/lib/ui/image-popup.js +435 -0
  59. package/lib/ui/import-popup.js +288 -0
  60. package/lib/ui/link-popup.js +139 -0
  61. package/lib/ui/list-picker.js +307 -0
  62. package/lib/ui/select-button.js +68 -0
  63. package/lib/ui/table-popup.js +171 -0
  64. package/lib/ui/tag-popup.js +249 -0
  65. package/lib/ui/text-align-picker.js +278 -0
  66. package/lib/ui/video-popup.js +413 -0
  67. package/lib/utils/exec-command.js +72 -0
  68. package/lib/utils/history-helper.js +50 -0
  69. package/lib/utils/popup-helper.js +219 -0
  70. package/lib/utils/popup-positioning.js +234 -0
  71. package/lib/utils/sanitize.js +164 -0
  72. package/package.json +51 -32
  73. package/umd-entry.js +19 -0
@@ -0,0 +1,639 @@
1
+ import Module from '../core/module.js';
2
+ import IconUtils from '../ui/icons.js';
3
+ import { execFormat, queryFormatState } from '../utils/exec-command.js';
4
+
5
+ /**
6
+ * Block Toolbar Module - Floating toolbar hiện lên khi select text hoặc ấn Enter
7
+ */
8
+ class BlockToolbar extends Module {
9
+ static DEFAULTS = {
10
+ showOnSelection: true,
11
+ showOnEnter: true,
12
+ buttons: ['bold', 'italic', 'underline', 'strike', 'code']
13
+ };
14
+
15
+ constructor(editor, options = {}) {
16
+ super(editor, options);
17
+ this.blockToolbar = null;
18
+ this.isVisible = false;
19
+ this.currentSelection = null; // Store current selection for scroll updates
20
+ this.currentCursorPosition = null; // Store current cursor position for scroll updates
21
+ this.originalTags = new Map(); // Store original tags before converting to code
22
+ this.init();
23
+ }
24
+
25
+ init() {
26
+ this.preloadIcons();
27
+ this.createBlockToolbar();
28
+ this.setupEventListeners();
29
+ }
30
+
31
+ async preloadIcons() {
32
+ // Icons are now inline, no need to preload
33
+ // This method is kept for backward compatibility
34
+ }
35
+
36
+ createBlockToolbar() {
37
+ this.blockToolbar = document.createElement('div');
38
+ this.blockToolbar.className = 'block-toolbar';
39
+
40
+ // Create toolbar container
41
+ const toolbarContainer = document.createElement('div');
42
+ toolbarContainer.className = 'block-toolbar-container';
43
+
44
+ // Button set is configurable via options.buttons (array of command names);
45
+ // defaults to the inline formatting set.
46
+ const meta = {
47
+ bold: { icon: 'bold', title: 'Bold (Ctrl+B)' },
48
+ italic: { icon: 'italic', title: 'Italic (Ctrl+I)' },
49
+ underline: { icon: 'underline', title: 'Underline (Ctrl+U)' },
50
+ strike: { icon: 'strike', title: 'Strikethrough' },
51
+ code: { icon: 'code', title: 'Code' },
52
+ 'font-family': { icon: 'font-family', title: 'Font Family' },
53
+ link: { icon: 'link', title: 'Insert link' },
54
+ color: { icon: 'color', title: 'Text color' },
55
+ background: { icon: 'background', title: 'Background color' }
56
+ };
57
+ const names = Array.isArray(this.options.buttons) && this.options.buttons.length
58
+ ? this.options.buttons
59
+ : ['bold', 'italic', 'underline', 'strike', 'code', 'font-family'];
60
+ const buttons = names.map(cmd => ({ cmd, icon: (meta[cmd] && meta[cmd].icon) || cmd, title: (meta[cmd] && meta[cmd].title) || cmd }));
61
+ buttons.forEach(({ cmd, icon, title }) => {
62
+ const button = document.createElement('button');
63
+ button.className = 'block-toolbar-btn';
64
+ button.title = title;
65
+ button.dataset.command = cmd;
66
+ const iconElement = IconUtils.createIconElement(icon, { width: '16px', height: '16px' });
67
+ button.appendChild(iconElement);
68
+ button.addEventListener('click', (e) => {
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ this.handleCommand(cmd, button);
72
+ });
73
+ toolbarContainer.appendChild(button);
74
+ });
75
+
76
+ // Create arrow element
77
+ const arrow = document.createElement('div');
78
+ arrow.className = 'block-toolbar-arrow';
79
+
80
+ // Add container and arrow to toolbar
81
+ this.blockToolbar.appendChild(toolbarContainer);
82
+ this.blockToolbar.appendChild(arrow);
83
+
84
+ this.editor.wrapper.appendChild(this.blockToolbar);
85
+ }
86
+
87
+ setupEventListeners() {
88
+ // Keep references so listeners can be removed in destroy() (prevents leaks
89
+ // on document/window when multiple editors are created/destroyed).
90
+ this._onEditorMouseup = () => {
91
+ setTimeout(() => this.handleSelectionChange(), 0);
92
+ };
93
+
94
+ this._onEditorKeydown = (e) => {
95
+ // Only react to Enter; do NOT hide on every keystroke (that broke typing UX).
96
+ if (e.key === 'Enter' && !e.shiftKey) {
97
+ requestAnimationFrame(() => {
98
+ setTimeout(() => this.showAtCursorAfterEnter(), 10);
99
+ });
100
+ }
101
+ };
102
+
103
+ this._onDocMousedown = (e) => {
104
+ // Don't hide if clicking on font-family popup or its items
105
+ if (e.target.closest('.font-family-select-popup') || e.target.closest('.custom-select-popup')) {
106
+ return;
107
+ }
108
+ if (!e.target.closest('.block-toolbar') && !e.target.closest('.rich-editor-area')) {
109
+ this.hide();
110
+ }
111
+ };
112
+
113
+ this._onWindowScroll = () => {
114
+ if (this.isVisible) this.updateToolbarPosition();
115
+ };
116
+
117
+ this._onEditorScroll = () => {
118
+ if (this.isVisible) this.updateToolbarPosition();
119
+ };
120
+
121
+ this._onEditorKeyup = (e) => {
122
+ // Shift + Enter hides the toolbar
123
+ if (e.key === 'Enter' && e.shiftKey) {
124
+ this.hide();
125
+ return;
126
+ }
127
+ if (this.isVisible) {
128
+ this.updateButtonStates();
129
+ } else {
130
+ this.handleSelectionChange();
131
+ }
132
+ };
133
+
134
+ if (this.options.showOnSelection) {
135
+ this.editor.editor.addEventListener('mouseup', this._onEditorMouseup);
136
+ }
137
+ if (this.options.showOnEnter) {
138
+ this.editor.editor.addEventListener('keydown', this._onEditorKeydown);
139
+ }
140
+ document.addEventListener('mousedown', this._onDocMousedown);
141
+ window.addEventListener('scroll', this._onWindowScroll);
142
+ this.editor.editor.addEventListener('scroll', this._onEditorScroll);
143
+ this.editor.editor.addEventListener('keyup', this._onEditorKeyup);
144
+ }
145
+
146
+ handleSelectionChange() {
147
+ const selection = window.getSelection();
148
+ if (!selection || selection.rangeCount === 0) return this.hide();
149
+ const range = selection.getRangeAt(0);
150
+ const isInEditableArea = this.editor.isSelectionInEditableArea ?
151
+ this.editor.isSelectionInEditableArea(selection) :
152
+ this.editor.editor.contains(range.commonAncestorContainer);
153
+ if (!isInEditableArea) return this.hide();
154
+ if (!range.collapsed && selection.toString().trim().length > 0) {
155
+ this.showAtSelection(selection);
156
+ } else {
157
+ this.hide();
158
+ }
159
+ }
160
+
161
+ showAtSelection(selection) {
162
+ if (!selection || selection.rangeCount === 0) return;
163
+
164
+ // Store current selection for scroll updates
165
+ this.currentSelection = selection;
166
+ this.currentCursorPosition = null;
167
+
168
+ const range = selection.getRangeAt(0);
169
+ const rect = range.getBoundingClientRect();
170
+ const editorRect = this.editor.wrapper.getBoundingClientRect();
171
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
172
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
173
+ this.showAt(
174
+ rect.left + rect.width / 2 - editorRect.left + scrollLeft,
175
+ rect.top - editorRect.top + scrollTop - 10
176
+ );
177
+ }
178
+
179
+ showAtCursorAfterEnter() {
180
+ this.editor.focus();
181
+ const selection = window.getSelection();
182
+ if (!selection || selection.rangeCount === 0) return;
183
+ const range = selection.getRangeAt(0);
184
+ const isInEditableArea = this.editor.isSelectionInEditableArea ?
185
+ this.editor.isSelectionInEditableArea(selection) :
186
+ this.editor.editor.contains(range.commonAncestorContainer);
187
+ if (!isInEditableArea) return;
188
+
189
+ this.ensureCursorAtEndOfLine(range);
190
+
191
+ // Store current cursor position for scroll updates
192
+ this.currentSelection = selection;
193
+ this.currentCursorPosition = this.getCursorPositionAfterEnter();
194
+
195
+ const rect = this.currentCursorPosition;
196
+ if (!rect) return;
197
+ const editorRect = this.editor.wrapper.getBoundingClientRect();
198
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
199
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
200
+ this.showAt(
201
+ rect.left - editorRect.left + scrollLeft,
202
+ rect.top - editorRect.top + scrollTop - 10
203
+ );
204
+ }
205
+
206
+ ensureCursorAtEndOfLine(range) {
207
+ if (!range.collapsed) return;
208
+ const selection = window.getSelection();
209
+ const currentNode = range.startContainer;
210
+ if (currentNode.nodeType === Node.TEXT_NODE) {
211
+ const textLength = currentNode.textContent.length;
212
+ if (range.startOffset < textLength) {
213
+ range.setStart(currentNode, textLength);
214
+ range.setEnd(currentNode, textLength);
215
+ selection.removeAllRanges();
216
+ selection.addRange(range);
217
+ }
218
+ } else if (currentNode.nodeType === Node.ELEMENT_NODE) {
219
+ const walker = document.createTreeWalker(currentNode, NodeFilter.SHOW_TEXT, null, false);
220
+ let lastTextNode = null, node;
221
+ while (node = walker.nextNode()) lastTextNode = node;
222
+ if (lastTextNode) {
223
+ const textLength = lastTextNode.textContent.length;
224
+ range.setStart(lastTextNode, textLength);
225
+ range.setEnd(lastTextNode, textLength);
226
+ selection.removeAllRanges();
227
+ selection.addRange(range);
228
+ }
229
+ }
230
+ }
231
+
232
+ getCursorPositionAfterEnter() {
233
+ const selection = window.getSelection();
234
+ if (!selection || selection.rangeCount === 0) return null;
235
+ const range = selection.getRangeAt(0);
236
+ const marker = document.createElement('span');
237
+ marker.innerHTML = '&#8203;';
238
+ marker.style.position = 'absolute';
239
+ marker.style.visibility = 'hidden';
240
+ marker.style.pointerEvents = 'none';
241
+ range.insertNode(marker);
242
+ const rect = marker.getBoundingClientRect();
243
+ if (marker.parentNode) marker.parentNode.removeChild(marker);
244
+ const newRange = document.createRange();
245
+ newRange.setStart(range.startContainer, range.startOffset);
246
+ newRange.collapse(true);
247
+ selection.removeAllRanges();
248
+ selection.addRange(newRange);
249
+ return rect;
250
+ }
251
+
252
+ showAtCursor() {
253
+ const selection = window.getSelection();
254
+ if (!selection || selection.rangeCount === 0) return;
255
+ const range = selection.getRangeAt(0);
256
+ const isInEditableArea = this.editor.isSelectionInEditableArea ?
257
+ this.editor.isSelectionInEditableArea(selection) :
258
+ this.editor.editor.contains(range.commonAncestorContainer);
259
+ if (!isInEditableArea) return;
260
+ let rect;
261
+ if (range.collapsed) {
262
+ const span = document.createElement('span');
263
+ span.innerHTML = '&#8203;';
264
+ span.style.position = 'absolute';
265
+ span.style.visibility = 'hidden';
266
+ span.style.pointerEvents = 'none';
267
+ range.insertNode(span);
268
+ rect = span.getBoundingClientRect();
269
+ if (span.parentNode) span.parentNode.removeChild(span);
270
+ const newRange = document.createRange();
271
+ newRange.setStart(range.startContainer, range.startOffset);
272
+ newRange.collapse(true);
273
+ selection.removeAllRanges();
274
+ selection.addRange(newRange);
275
+ } else {
276
+ rect = range.getBoundingClientRect();
277
+ }
278
+ const editorRect = this.editor.wrapper.getBoundingClientRect();
279
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
280
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
281
+ this.showAt(
282
+ rect.left - editorRect.left + scrollLeft,
283
+ rect.top - editorRect.top + scrollTop - 10
284
+ );
285
+ }
286
+
287
+ showAt(x, y) {
288
+ if (!this.blockToolbar) return;
289
+ this.blockToolbar.classList.add('visible');
290
+ this.isVisible = true;
291
+ this.ensureToolbarInViewport(x,y);
292
+ this.updateButtonStates();
293
+ }
294
+
295
+ ensureToolbarInViewport(x,y) {
296
+ if (!this.blockToolbar) return;
297
+
298
+ // Lấy thông tin về editor-area
299
+ const editorArea = this.editor.editor;
300
+ const editorRect = editorArea.getBoundingClientRect();
301
+ const toolbarRect = this.blockToolbar.getBoundingClientRect();
302
+ const toolbarContainer = this.editor.wrapper.querySelector('.rich-editor-toolbar-container');
303
+ const toolbarRect2 = toolbarContainer ? toolbarContainer.getBoundingClientRect() : null;
304
+
305
+
306
+ let left = x - this.blockToolbar.offsetWidth/2;
307
+ let top = editorRect.y + y -(toolbarRect2.height) - editorArea.scrollTop - (editorRect.y + window.scrollY) +toolbarContainer.offsetHeight-49;
308
+
309
+ let arrowLeft = '50%';
310
+ let arrowDirection = 'down'; // mũi tên hướng xuống
311
+
312
+ // Trường hợp 1: Vượt quá lề trái của editor
313
+ if (left < 0) {
314
+ left =(x - (this.blockToolbar.offsetWidth * (10/100)));
315
+ if(left < 0) left = 0;
316
+ arrowLeft = '10%'; // Mũi tên ở 10%
317
+ }
318
+ // Trường hợp 2: Vượt quá lề phải của editor
319
+ if (left + this.blockToolbar.offsetWidth > (this.editor.wrapper.offsetWidth - 2)) {
320
+ left = x - this.blockToolbar.offsetWidth*0.9;
321
+ arrowLeft = '90%'; // Mũi tên ở 90%
322
+ }
323
+
324
+ // Trường hợp 3: Vượt quá lề trên của editor
325
+
326
+ if (top < toolbarRect2.height) {
327
+ top = editorRect.y + y -(toolbarRect2.height) - editorArea.scrollTop +100 - (editorRect.y + window.scrollY)+toolbarContainer.offsetHeight-49;
328
+ arrowDirection = 'up'; // Mũi tên hướng lên
329
+ if(top < toolbarRect2.height ){
330
+ this.hide();
331
+ return;
332
+ }
333
+ }
334
+ if(top > editorRect.height){
335
+ this.hide();
336
+ return;
337
+ }
338
+ // Cập nhật vị trí mũi tên
339
+ const arrow = this.blockToolbar.querySelector('.block-toolbar-arrow');
340
+ if (arrow) {
341
+ arrow.style.left = arrowLeft;
342
+
343
+ if (arrowDirection === 'up') {
344
+ // Mũi tên hướng lên
345
+ arrow.style.bottom = 'auto';
346
+ arrow.style.top = '-8px';
347
+ arrow.style.borderTop = 'none';
348
+ arrow.style.borderBottom = '8px solid #fff';
349
+ arrow.style.borderLeft = '6px solid transparent';
350
+ arrow.style.borderRight = '6px solid transparent';
351
+ } else {
352
+ // Mũi tên hướng xuống (mặc định)
353
+ arrow.style.top = 'auto';
354
+ arrow.style.bottom = '-8px';
355
+ arrow.style.borderBottom = 'none';
356
+ arrow.style.borderTop = '8px solid #fff';
357
+ arrow.style.borderLeft = '6px solid transparent';
358
+ arrow.style.borderRight = '6px solid transparent';
359
+ }
360
+ }
361
+ // Áp dụng vị trí cuối cùng
362
+ this.blockToolbar.style.left = left + 'px';
363
+ this.blockToolbar.style.top = top + 'px';
364
+ }
365
+
366
+ /**
367
+ * Update toolbar position based on current selection or cursor position
368
+ */
369
+ updateToolbarPosition() {
370
+ if (!this.isVisible) return;
371
+
372
+ const selection = window.getSelection();
373
+ if (!selection || selection.rangeCount === 0) {
374
+ this.hide();
375
+ return;
376
+ }
377
+
378
+ const range = selection.getRangeAt(0);
379
+ const isInEditableArea = this.editor.isSelectionInEditableArea ?
380
+ this.editor.isSelectionInEditableArea(selection) :
381
+ this.editor.editor.contains(range.commonAncestorContainer);
382
+
383
+ if (!isInEditableArea) {
384
+ this.hide();
385
+ return;
386
+ }
387
+
388
+ let rect;
389
+
390
+ if (range.collapsed) {
391
+ // For cursor position, get current cursor rect
392
+ const span = document.createElement('span');
393
+ span.innerHTML = '&#8203;';
394
+ span.style.position = 'absolute';
395
+ span.style.visibility = 'hidden';
396
+ span.style.pointerEvents = 'none';
397
+
398
+ try {
399
+ range.insertNode(span);
400
+ rect = span.getBoundingClientRect();
401
+ if (span.parentNode) span.parentNode.removeChild(span);
402
+
403
+ // Restore range
404
+ const newRange = document.createRange();
405
+ newRange.setStart(range.startContainer, range.startOffset);
406
+ newRange.collapse(true);
407
+ selection.removeAllRanges();
408
+ selection.addRange(newRange);
409
+ } catch (e) {
410
+ // If insertion fails, hide toolbar
411
+ this.hide();
412
+ return;
413
+ }
414
+ } else {
415
+ // For selection, use selection rect
416
+ rect = range.getBoundingClientRect();
417
+ }
418
+
419
+ const editorRect = this.editor.wrapper.getBoundingClientRect();
420
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
421
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
422
+
423
+ let x, y;
424
+ if (range.collapsed) {
425
+ x = rect.left - editorRect.left + scrollLeft;
426
+ y = rect.top - editorRect.top + scrollTop - 10;
427
+ } else {
428
+ x = rect.left + rect.width / 2 - editorRect.left + scrollLeft;
429
+ y = rect.top - editorRect.top + scrollTop - 10;
430
+ }
431
+
432
+ this.updateToolbarAt(x, y);
433
+
434
+ // Update font-family popup position if it's visible
435
+ const fontFamilyFormat = this.editor.registry.get('formats/font-family');
436
+ if (fontFamilyFormat && fontFamilyFormat.selectInstance && fontFamilyFormat.selectInstance.isVisible) {
437
+ fontFamilyFormat.selectInstance.updatePosition();
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Update toolbar position at specific coordinates
443
+ */
444
+ updateToolbarAt(x, y) {
445
+ if (!this.blockToolbar) return;
446
+
447
+ this.ensureToolbarInViewport(x, y);
448
+ this.updateButtonStates();
449
+ }
450
+
451
+ hide() {
452
+ if (!this.blockToolbar || !this.isVisible) return;
453
+ this.blockToolbar.classList.remove('visible');
454
+ this.isVisible = false;
455
+ // Clear stored positions
456
+ this.currentSelection = null;
457
+ this.currentCursorPosition = null;
458
+
459
+ // Hide any open font-family popup when block toolbar is hidden
460
+ const fontFamilyFormat = this.editor.registry.get('formats/font-family');
461
+ if (fontFamilyFormat && fontFamilyFormat.selectInstance) {
462
+ fontFamilyFormat.selectInstance.hide();
463
+ }
464
+ }
465
+
466
+ handleCommand(command, button) {
467
+ const selection = window.getSelection();
468
+ const isInEditableArea = this.editor.isSelectionInEditableArea ?
469
+ this.editor.isSelectionInEditableArea(selection) : true;
470
+ if (!isInEditableArea) {
471
+ this.hide();
472
+ return;
473
+ }
474
+
475
+ // Special handling for font-family command
476
+ if (command === 'font-family') {
477
+ const fontFamilyFormat = this.editor.registry.get('formats/font-family');
478
+ if (fontFamilyFormat) {
479
+ const format = new fontFamilyFormat();
480
+ format.toggle(button); // Pass the button as anchor
481
+ this.updateButtonState(command, button);
482
+ this.editor.focus();
483
+ return;
484
+ }
485
+ }
486
+
487
+ // Special handling for code command to use PRE tag from heading format
488
+ if (command === 'code') {
489
+ const headingFormat = this.editor.registry.get('formats/heading');
490
+ if (headingFormat) {
491
+ const heading = new headingFormat();
492
+ const currentTag = heading.getCurrentTag();
493
+
494
+ // If current tag is PRE, convert back to original tag or P
495
+ // If current tag is not PRE, convert to PRE (code format)
496
+ if (currentTag === 'PRE') {
497
+ // Get the selection to find the block element
498
+ const selection = window.getSelection();
499
+ if (selection && selection.rangeCount) {
500
+ const range = selection.getRangeAt(0);
501
+ const block = this.getBlockElement(range.startContainer);
502
+
503
+ if (block) {
504
+ // Get original tag for this block, default to P
505
+ const originalTag = this.originalTags.get(block) || 'P';
506
+ heading.apply(originalTag);
507
+ this.originalTags.delete(block); // Clean up
508
+ } else {
509
+ heading.apply('P');
510
+ }
511
+ } else {
512
+ heading.apply('P');
513
+ }
514
+ } else {
515
+ // Store original tag before converting to PRE
516
+ const selection = window.getSelection();
517
+ if (selection && selection.rangeCount) {
518
+ const range = selection.getRangeAt(0);
519
+ const block = this.getBlockElement(range.startContainer);
520
+
521
+ if (block) {
522
+ this.originalTags.set(block, currentTag || 'P');
523
+ }
524
+ }
525
+
526
+ heading.apply('PRE');
527
+ }
528
+
529
+ this.updateButtonState(command, button);
530
+ this.editor.focus();
531
+ return;
532
+ }
533
+ }
534
+
535
+ const formatClass = this.editor.registry.get(`formats/${command}`);
536
+ if (formatClass) {
537
+ const format = new formatClass();
538
+ if (typeof format.toggle === 'function') format.toggle();
539
+ else if (typeof format.apply === 'function') format.apply();
540
+ } else {
541
+ execFormat(command);
542
+ }
543
+ this.updateButtonState(command, button);
544
+ this.editor.focus();
545
+ }
546
+
547
+ updateButtonStates() {
548
+ if (!this.blockToolbar) return;
549
+ const buttons = this.blockToolbar.querySelectorAll('.block-toolbar-btn');
550
+ buttons.forEach(button => {
551
+ const command = button.dataset.command;
552
+ this.updateButtonState(command, button);
553
+ });
554
+ }
555
+
556
+ updateButtonState(command, button) {
557
+ if (!button) return;
558
+ let isActive = false;
559
+ if (command === 'font-family') {
560
+ const fontFamilyFormat = this.editor.registry.get('formats/font-family');
561
+ if (fontFamilyFormat) {
562
+ const format = new fontFamilyFormat();
563
+ isActive = format.isActive();
564
+ }
565
+ } else if (command === 'code') {
566
+ // Check if current block is PRE tag
567
+ const headingFormat = this.editor.registry.get('formats/heading');
568
+ if (headingFormat) {
569
+ const heading = new headingFormat();
570
+ const currentTag = heading.getCurrentTag();
571
+ isActive = currentTag === 'PRE';
572
+ }
573
+ } else if (command === 'strike') {
574
+ const formatClass = this.editor.registry.get(`formats/${command}`);
575
+ if (formatClass) {
576
+ const format = new formatClass();
577
+ isActive = format.isActive();
578
+ }
579
+ } else {
580
+ isActive = queryFormatState(command);
581
+ }
582
+ if (isActive) button.classList.add('active');
583
+ else button.classList.remove('active');
584
+ }
585
+
586
+ /**
587
+ * Get block element from a node
588
+ * @param {Node} node - Node to find block element for
589
+ * @returns {Element|null} Block element or null
590
+ */
591
+ getBlockElement(node) {
592
+ if (!node) return null;
593
+
594
+ // If node is an element and is a block, return it
595
+ if (node.nodeType === Node.ELEMENT_NODE) {
596
+ const blockTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'PRE', 'BLOCKQUOTE', 'DIV'];
597
+ if (blockTags.includes(node.tagName)) {
598
+ return node;
599
+ }
600
+ }
601
+
602
+ // Walk up the DOM tree to find block element
603
+ let current = node;
604
+ while (current && current !== this.editor.editor) {
605
+ if (current.nodeType === Node.ELEMENT_NODE) {
606
+ const blockTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'PRE', 'BLOCKQUOTE', 'DIV'];
607
+ if (blockTags.includes(current.tagName)) {
608
+ return current;
609
+ }
610
+ }
611
+ current = current.parentNode;
612
+ }
613
+
614
+ return null;
615
+ }
616
+
617
+ destroy() {
618
+ // Remove event listeners (document/window ones would otherwise leak)
619
+ if (this._onDocMousedown) {
620
+ document.removeEventListener('mousedown', this._onDocMousedown);
621
+ window.removeEventListener('scroll', this._onWindowScroll);
622
+ this.editor.editor.removeEventListener('mouseup', this._onEditorMouseup);
623
+ this.editor.editor.removeEventListener('keydown', this._onEditorKeydown);
624
+ this.editor.editor.removeEventListener('scroll', this._onEditorScroll);
625
+ this.editor.editor.removeEventListener('keyup', this._onEditorKeyup);
626
+ this._onDocMousedown = this._onWindowScroll = this._onEditorMouseup = null;
627
+ this._onEditorKeydown = this._onEditorScroll = this._onEditorKeyup = null;
628
+ }
629
+
630
+ if (this.blockToolbar && this.blockToolbar.parentNode) {
631
+ this.blockToolbar.parentNode.removeChild(this.blockToolbar);
632
+ }
633
+ this.blockToolbar = null;
634
+ this.isVisible = false;
635
+ this.originalTags.clear(); // Clean up stored tags
636
+ }
637
+ }
638
+
639
+ export default BlockToolbar;