@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,607 @@
1
+ import Module from '../core/module.js';
2
+ import ColorPicker from '../ui/color-picker.js';
3
+ import IconUtils from '../ui/icons.js';
4
+ import createCustomButton from '../ui/select-button.js';
5
+
6
+ /**
7
+ * Toolbar Module - Pure UI component with dual toolbar support
8
+ * Only handles toolbar creation and event emission
9
+ * No business logic or state management
10
+ */
11
+ class Toolbar extends Module {
12
+ static DEFAULTS = {
13
+ container: null,
14
+ toolbar1: [
15
+ // Most-used inline formatting leads; the block-style (Paragraph) picker
16
+ // sits after it rather than first.
17
+ { group: 'text-format', items: ['bold', 'italic', 'underline', 'strike'] },
18
+ { group: 'paragraph', items: ['heading'] },
19
+ { group: 'colors', items: ['color', 'background'] },
20
+ { group: 'link', items: ['link'] },
21
+ { group: 'paragraph-ops', items: ['list', 'indent-increase', 'indent-decrease', 'text-align'] },
22
+ { group: 'insert', items: ['image', 'table'] },
23
+ // Undo/redo live on the right and stay hidden until there's history.
24
+ { group: 'history', items: ['undo', 'redo'] },
25
+ { group: 'more', items: ['more'] }
26
+ ],
27
+ toolbar2: [
28
+ { group: 'font', items: ['font-family', 'text-size', 'line-height'] },
29
+ { group: 'script', items: ['subscript', 'superscript', 'capitalization'] },
30
+ { group: 'media', items: ['emoji', 'video', 'tag', 'horizontal-rule'] },
31
+ { group: 'tools', items: ['clear-format', 'text-direction', 'find', 'code-view'] }
32
+ ]
33
+ };
34
+
35
+ constructor(editor, options = {}) {
36
+ super(editor, options);
37
+ this.buttons = new Map();
38
+ this.toolbar2Visible = false;
39
+ this.events = new Map(); // Add event system
40
+
41
+
42
+ // Handle toolbar configuration
43
+ if (Array.isArray(options.toolbar)) {
44
+ // If toolbar array is provided, use only those items - COMPLETELY OVERRIDE DEFAULTS
45
+ this.options = {
46
+ container: null,
47
+ toolbar1: [
48
+ { group: 'text-format', items: options.toolbar }
49
+ ],
50
+ toolbar2: []
51
+ };
52
+ } else if (options.toolbar1 || options.toolbar2) {
53
+ // If specific toolbar1/toolbar2 config is provided, use it - COMPLETELY OVERRIDE DEFAULTS
54
+ this.options = {
55
+ container: null,
56
+ toolbar1: options.toolbar1 || [],
57
+ toolbar2: options.toolbar2 || []
58
+ };
59
+ } else {
60
+ // Use full default configuration
61
+ this.options = { ...Toolbar.DEFAULTS, ...options };
62
+ }
63
+
64
+
65
+ this.init();
66
+ this.preloadIcons();
67
+ }
68
+
69
+ init() {
70
+ this.container = this.createToolbarContainer();
71
+ }
72
+
73
+ /**
74
+ * Preload icons for better performance
75
+ */
76
+ async preloadIcons() {
77
+ // Icons are now inline, no need to preload
78
+ // This method is kept for backward compatibility
79
+ }
80
+
81
+ /**
82
+ * Create main toolbar container with both toolbars
83
+ */
84
+ createToolbarContainer() {
85
+ const container = document.createElement('div');
86
+ container.className = 'rich-editor-toolbar-container';
87
+ container.setAttribute('role', 'toolbar');
88
+ container.setAttribute('aria-label', 'Text formatting');
89
+
90
+ // Prevent toolbar from taking focus away from editor
91
+ this.editor.preventFocusLoss(container);
92
+
93
+ // Keep the editor's text selection when a toolbar button is pressed (mouse
94
+ // OR touch). Without this, tapping e.g. Bold on mobile can clear the
95
+ // selection before the click handler runs, so the format applies to nothing.
96
+ // preventing pointerdown's default stops the focus/selection change while
97
+ // the click still fires normally.
98
+ container.addEventListener('pointerdown', (e) => {
99
+ if (e.target.closest('button')) e.preventDefault();
100
+ });
101
+
102
+ // Primary (always-visible) row and overflow ("more") row
103
+ this.toolbar1 = document.createElement('div');
104
+ this.toolbar1.className = 'rich-editor-toolbar-1';
105
+ this.toolbar2 = document.createElement('div');
106
+ this.toolbar2.className = 'rich-editor-toolbar-2';
107
+ this.toolbar2.style.display = 'none';
108
+
109
+ // Build every group (toolbar1 first = higher priority, then toolbar2) into
110
+ // the primary row. reflow() later moves whatever doesn't fit into the
111
+ // overflow row, so the toolbar adapts to any width instead of wrapping.
112
+ this.flowGroups = [];
113
+ const merged = [...(this.options.toolbar1 || []), ...(this.options.toolbar2 || [])];
114
+ merged.forEach(group => {
115
+ if (!group || !group.group || !Array.isArray(group.items)) return;
116
+ // The "more" toggle is managed separately (added at the end).
117
+ if (group.items.length === 1 && group.items[0] === 'more') return;
118
+ const groupContainer = document.createElement('div');
119
+ groupContainer.className = `toolbar-group toolbar-group-${group.group}`;
120
+ group.items.forEach(item => {
121
+ if (typeof item === 'string') this.addButton(groupContainer, item);
122
+ });
123
+ this.toolbar1.appendChild(groupContainer);
124
+ this.flowGroups.push(groupContainer);
125
+ });
126
+
127
+ // The "more" button lives at the end of the primary row; shown only when
128
+ // there is overflow.
129
+ this.addMoreButton(this.toolbar1);
130
+ this.moreBtn = this.buttons.get('more');
131
+ if (this.moreBtn) this.moreBtn.classList.add('more-btn');
132
+
133
+ container.appendChild(this.toolbar1);
134
+ container.appendChild(this.toolbar2);
135
+
136
+ // Responsive reflow: re-distribute groups whenever the toolbar resizes.
137
+ if (typeof ResizeObserver !== 'undefined') {
138
+ this._ro = new ResizeObserver(() => this._scheduleReflow());
139
+ this._ro.observe(container);
140
+ }
141
+ requestAnimationFrame(() => this.reflow());
142
+
143
+ // Keyboard navigation (arrow keys move between buttons; roving tabindex).
144
+ this._setupKeyboardNav(container);
145
+
146
+ return container;
147
+ }
148
+
149
+ /**
150
+ * All currently focusable (visible, enabled) toolbar buttons in order.
151
+ */
152
+ _focusableButtons() {
153
+ return Array.from(
154
+ this.container.querySelectorAll('.rich-editor-toolbar-btn, .custom-select-button')
155
+ ).filter(b => !b.disabled && b.offsetParent !== null);
156
+ }
157
+
158
+ /**
159
+ * Roving tabindex: only one button is in the tab order at a time.
160
+ */
161
+ _updateRoving() {
162
+ const btns = this._focusableButtons();
163
+ btns.forEach((b, i) => { b.tabIndex = i === 0 ? 0 : -1; });
164
+ }
165
+
166
+ /**
167
+ * Arrow-key navigation across the toolbar (ARIA toolbar pattern).
168
+ */
169
+ _setupKeyboardNav(container) {
170
+ container.addEventListener('keydown', (e) => {
171
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
172
+ const btns = this._focusableButtons();
173
+ if (!btns.length) return;
174
+ const cur = btns.indexOf(document.activeElement);
175
+ let next;
176
+ if (e.key === 'Home') next = 0;
177
+ else if (e.key === 'End') next = btns.length - 1;
178
+ else if (cur === -1) next = 0;
179
+ else next = e.key === 'ArrowRight'
180
+ ? (cur + 1) % btns.length
181
+ : (cur - 1 + btns.length) % btns.length;
182
+ e.preventDefault();
183
+ btns.forEach(b => { b.tabIndex = -1; });
184
+ btns[next].tabIndex = 0;
185
+ btns[next].focus();
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Debounce reflow to one run per animation frame.
191
+ */
192
+ _scheduleReflow() {
193
+ if (this._reflowQueued) return;
194
+ this._reflowQueued = true;
195
+ requestAnimationFrame(() => {
196
+ this._reflowQueued = false;
197
+ this.reflow();
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Distribute groups between the primary row and the overflow ("more") row so
203
+ * the primary row always fits on a single line at the current width.
204
+ */
205
+ reflow() {
206
+ if (!this.toolbar1 || !this.flowGroups || !this.moreBtn) return;
207
+ const GAP = 12; // matches .toolbar-group spacing
208
+
209
+ // Pull every group back into the primary row (in priority order) to measure.
210
+ this.flowGroups.forEach(g => this.toolbar1.insertBefore(g, this.moreBtn));
211
+
212
+ // On small screens, skip the "More" split entirely — keep every tool in one
213
+ // horizontally-scrollable row (how Google Docs / Notion handle mobile)
214
+ // instead of wrapping into a cramped multi-row panel.
215
+ if (typeof window !== 'undefined' && window.matchMedia &&
216
+ window.matchMedia('(max-width: 640px)').matches) {
217
+ this.moreBtn.style.display = 'none';
218
+ this.toolbar2.style.display = 'none';
219
+ this.toolbar2Visible = false;
220
+ this._syncMoreButton();
221
+ this._updateRoving();
222
+ return;
223
+ }
224
+
225
+ const cs = getComputedStyle(this.toolbar1);
226
+ const avail = this.toolbar1.clientWidth -
227
+ (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);
228
+ if (avail <= 0) return; // not laid out yet; will reflow on resize
229
+
230
+ let total = 0;
231
+ this.flowGroups.forEach((g, i) => { total += g.offsetWidth + (i > 0 ? GAP : 0); });
232
+
233
+ if (total <= avail) {
234
+ // Everything fits — no overflow needed.
235
+ this.moreBtn.style.display = 'none';
236
+ this.toolbar2.style.display = 'none';
237
+ this.toolbar2Visible = false;
238
+ this._syncMoreButton();
239
+ this._updateRoving();
240
+ return;
241
+ }
242
+
243
+ // Overflow needed — keep groups that fit (reserving room for "more").
244
+ const budget = avail - ((this.moreBtn.offsetWidth || 32) + GAP);
245
+ let used = 0;
246
+ let cut = this.flowGroups.length;
247
+ for (let i = 0; i < this.flowGroups.length; i++) {
248
+ const w = this.flowGroups[i].offsetWidth + (i > 0 ? GAP : 0);
249
+ if (used + w > budget) { cut = i; break; }
250
+ used += w;
251
+ }
252
+ if (cut < 1) cut = 1; // always keep at least one group visible
253
+
254
+ for (let i = cut; i < this.flowGroups.length; i++) {
255
+ this.toolbar2.appendChild(this.flowGroups[i]);
256
+ }
257
+
258
+ this.moreBtn.style.display = '';
259
+ this.toolbar2.style.display = this.toolbar2Visible ? 'flex' : 'none';
260
+ this._syncMoreButton();
261
+ this._updateRoving();
262
+ }
263
+
264
+ /**
265
+ * Sync the "more" button visual state with toolbar2 visibility.
266
+ */
267
+ _syncMoreButton() {
268
+ const m = this.moreBtn;
269
+ if (!m) return;
270
+ m.setAttribute('aria-expanded', this.toolbar2Visible ? 'true' : 'false');
271
+ if (this.toolbar2Visible) {
272
+ m.classList.add('active');
273
+ m.title = 'Hide more options';
274
+ } else {
275
+ m.classList.remove('active');
276
+ m.title = 'More options';
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Create toolbar element
282
+ */
283
+ createToolbar(className, toolbarItems) {
284
+ const toolbar = document.createElement('div');
285
+ toolbar.className = className;
286
+
287
+ // Create button groups based on toolbar config
288
+ if (Array.isArray(toolbarItems)) {
289
+ toolbarItems.forEach(group => {
290
+ if (group && group.group && Array.isArray(group.items)) {
291
+ // Create group container
292
+ const groupContainer = document.createElement('div');
293
+ groupContainer.className = `toolbar-group toolbar-group-${group.group}`;
294
+
295
+ // Add buttons to group
296
+ group.items.forEach(item => {
297
+ if (typeof item === 'string') {
298
+ this.addButton(groupContainer, item);
299
+ }
300
+ });
301
+
302
+ toolbar.appendChild(groupContainer);
303
+ }
304
+ });
305
+ }
306
+
307
+ return toolbar;
308
+ }
309
+
310
+ /**
311
+ * Add button to toolbar
312
+ */
313
+ addButton(container, format) {
314
+ // Special handling for more button
315
+ if (format === 'more') {
316
+ return this.addMoreButton(container);
317
+ }
318
+
319
+ // Custom buttons with dropdowns
320
+ const customButtons = {
321
+ 'heading': { text: 'Paragraph', width: '124px', title: 'Paragraph style', icon: 'heading' },
322
+ 'font-family': { text: 'Font Family', width: '156px', title: 'Font', icon: 'font-family' },
323
+ 'line-height': { text: 'Line Height', width: '116px', title: 'Line spacing', icon: 'line-height' },
324
+ 'capitalization': { text: 'Capitalization', width: '146px', title: 'Letter case', icon: 'capitalization' },
325
+ 'text-size': { text: 'Text Size', width: '116px', title: 'Font size', icon: 'text-size' }
326
+ };
327
+
328
+ if (customButtons[format]) {
329
+ const config = customButtons[format];
330
+ const customButton = createCustomButton(config.text, { width: config.width, icon: config.icon });
331
+ customButton.dataset.command = format;
332
+ customButton.classList.add('rich-editor-toolbar-btn', `${format}-btn`);
333
+ customButton.title = config.title;
334
+ customButton.setAttribute('aria-label', config.title);
335
+ customButton.setAttribute('aria-haspopup', 'true');
336
+
337
+ customButton.addEventListener('click', (e) => {
338
+ e.preventDefault();
339
+ this.emit('toolbar-click', { command: format, button: customButton });
340
+ // Maintain editor focus after button click
341
+ setTimeout(() => {
342
+ this.editor.focus();
343
+ }, 0);
344
+ });
345
+
346
+ this.buttons.set(format, customButton);
347
+ container.appendChild(customButton);
348
+ return customButton;
349
+ }
350
+
351
+ // Icon buttons with popups
352
+ const iconButtons = {
353
+ 'text-align': { icon: 'align-left', title: 'Align Left' },
354
+ 'list': { icon: 'list', title: 'List' }
355
+ };
356
+
357
+ if (iconButtons[format]) {
358
+ const config = iconButtons[format];
359
+ const button = document.createElement('button');
360
+ button.type = 'button';
361
+ button.className = `rich-editor-toolbar-btn ${format}-btn`;
362
+ button.dataset.command = format;
363
+ button.title = config.title;
364
+ button.setAttribute('aria-label', config.title);
365
+
366
+ const svgContent = IconUtils.getIcon(config.icon);
367
+ if (svgContent) {
368
+ button.innerHTML = `<span class="icon">${svgContent}</span>`;
369
+ } else {
370
+ button.textContent = format === 'text-align' ? '≡' : '•';
371
+ }
372
+
373
+ button.addEventListener('click', (e) => {
374
+ e.preventDefault();
375
+ this.emit('toolbar-click', { command: format, button: button });
376
+ // Maintain editor focus after button click
377
+ setTimeout(() => {
378
+ this.editor.focus();
379
+ }, 0);
380
+ });
381
+
382
+ this.buttons.set(format, button);
383
+ container.appendChild(button);
384
+ return button;
385
+ }
386
+
387
+ // Regular icon buttons
388
+ const button = document.createElement('button');
389
+ button.type = 'button';
390
+ button.className = `rich-editor-toolbar-btn ${format}-btn`;
391
+ button.dataset.command = format;
392
+
393
+ // Add icon
394
+ const iconElement = IconUtils.createIconElement(format, {
395
+ width: '16px',
396
+ height: '16px'
397
+ });
398
+ button.appendChild(iconElement);
399
+
400
+ // Set title based on format
401
+ const titles = {
402
+ 'bold': 'Bold (Ctrl+B)',
403
+ 'italic': 'Italic (Ctrl+I)',
404
+ 'underline': 'Underline (Ctrl+U)',
405
+ 'strike': 'Strikethrough',
406
+ 'subscript': 'Subscript',
407
+ 'superscript': 'Superscript',
408
+ 'color': 'Text Color',
409
+ 'background': 'Background Color',
410
+ 'link': 'Insert/Edit Link',
411
+ 'table': 'Insert Table',
412
+ 'undo': 'Undo (Ctrl+Z)',
413
+ 'redo': 'Redo (Ctrl+Y)',
414
+ 'indent-increase': 'Increase Indent',
415
+ 'indent-decrease': 'Decrease Indent',
416
+ 'emoji': 'Insert Emoji',
417
+ 'image': 'Insert Image',
418
+ 'video': 'Insert Video',
419
+ 'tag': 'Insert Tag',
420
+ 'horizontal-rule': 'Insert Horizontal Rule',
421
+ 'clear-format': 'Clear Formatting',
422
+ 'text-direction': 'Toggle Text Direction (LTR/RTL)',
423
+ 'find': 'Find & Replace (Ctrl+F)',
424
+
425
+ 'import': 'Import Files',
426
+ 'code-view': 'Switch to HTML Editor',
427
+
428
+ };
429
+
430
+ button.title = titles[format] || format;
431
+ button.setAttribute('aria-label', button.title);
432
+
433
+ // Colour buttons get a swatch bar that reflects the colour at the caret.
434
+ if (format === 'color' || format === 'background') {
435
+ const swatch = document.createElement('span');
436
+ swatch.className = 'rte-swatch';
437
+ button.appendChild(swatch);
438
+ }
439
+
440
+ // Add fallback for code-view
441
+ if (format === 'code-view') {
442
+ setTimeout(() => {
443
+ if (!iconElement.innerHTML.trim()) {
444
+ iconElement.innerHTML = '&lt;/&gt;';
445
+ iconElement.style.fontSize = '12px';
446
+ iconElement.style.fontWeight = 'bold';
447
+ }
448
+ }, 1000);
449
+ }
450
+
451
+ button.addEventListener('click', (e) => {
452
+ e.preventDefault();
453
+ this.emit('toolbar-click', { command: format, button });
454
+ // Maintain editor focus after button click
455
+ setTimeout(() => {
456
+ this.editor.focus();
457
+ }, 0);
458
+ });
459
+
460
+ this.buttons.set(format, button);
461
+ container.appendChild(button);
462
+ return button;
463
+ }
464
+
465
+ /**
466
+ * Add more button to toggle toolbar 2
467
+ */
468
+ addMoreButton(container) {
469
+ const button = document.createElement('button');
470
+ button.type = 'button';
471
+ button.className = 'rich-editor-toolbar-btn more-btn';
472
+ button.dataset.command = 'more';
473
+
474
+ const iconElement = IconUtils.createIconElement('more', {
475
+ width: '16px',
476
+ height: '16px'
477
+ });
478
+ button.appendChild(iconElement);
479
+ button.title = 'More Options';
480
+ button.setAttribute('aria-label', 'More Options');
481
+ button.setAttribute('aria-expanded', 'false');
482
+
483
+ button.addEventListener('click', (e) => {
484
+ e.preventDefault();
485
+ this.toggleToolbar2();
486
+ // Maintain editor focus after button click
487
+ setTimeout(() => {
488
+ this.editor.focus();
489
+ }, 0);
490
+ });
491
+
492
+ this.buttons.set('more', button);
493
+ container.appendChild(button);
494
+ return button;
495
+ }
496
+
497
+ /**
498
+ * Toggle toolbar 2 visibility
499
+ */
500
+ toggleToolbar2() {
501
+ // Nothing to toggle when there's no overflow.
502
+ if (this.moreBtn && this.moreBtn.style.display === 'none') return;
503
+
504
+ this.toolbar2Visible = !this.toolbar2Visible;
505
+ this.toolbar2.style.display = this.toolbar2Visible ? 'flex' : 'none';
506
+ this._syncMoreButton();
507
+ this._updateRoving();
508
+ }
509
+
510
+ /**
511
+ * Get toolbar container element
512
+ */
513
+ getContainer() {
514
+ return this.container;
515
+ }
516
+
517
+ /**
518
+ * Get button by command
519
+ */
520
+ getButton(command) {
521
+ return this.buttons.get(command);
522
+ }
523
+
524
+ /**
525
+ * Set button active state
526
+ */
527
+ setButtonActive(command, isActive) {
528
+ const button = this.buttons.get(command);
529
+ if (button && button.classList) {
530
+ if (isActive) {
531
+ button.classList.add('active');
532
+ } else {
533
+ button.classList.remove('active');
534
+ }
535
+ button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Set button disabled state
541
+ */
542
+ setButtonDisabled(command, isDisabled) {
543
+ const button = this.buttons.get(command);
544
+ if (button) {
545
+ button.disabled = isDisabled;
546
+ button.style.opacity = isDisabled ? '0.5' : '1';
547
+ button.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Set button title
553
+ */
554
+ setButtonTitle(command, title) {
555
+ const button = this.buttons.get(command);
556
+ if (button) {
557
+ button.title = title;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Check if toolbar 2 is visible
563
+ */
564
+ isToolbar2Visible() {
565
+ return this.toolbar2Visible;
566
+ }
567
+
568
+ /**
569
+ * Event system methods
570
+ */
571
+ on(event, callback) {
572
+ if (!this.events.has(event)) {
573
+ this.events.set(event, []);
574
+ }
575
+ this.events.get(event).push(callback);
576
+ }
577
+
578
+ emit(event, data) {
579
+ const callbacks = this.events.get(event);
580
+ if (callbacks) {
581
+ callbacks.forEach(callback => {
582
+ try {
583
+ callback(data);
584
+ } catch (error) {
585
+ console.error(`Error in toolbar event ${event}:`, error);
586
+ }
587
+ });
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Destroy toolbar
593
+ */
594
+ destroy() {
595
+ if (this._ro) {
596
+ this._ro.disconnect();
597
+ this._ro = null;
598
+ }
599
+ if (this.container && this.container.parentNode) {
600
+ this.container.parentNode.removeChild(this.container);
601
+ }
602
+ this.buttons.clear();
603
+ this.events.clear();
604
+ }
605
+ }
606
+
607
+ export default Toolbar;