@oix1987/yjd 1.0.0 → 1.0.2

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 (58) hide show
  1. package/README.md +73 -22
  2. package/dist/rich-editor.esm.js +2 -0
  3. package/dist/rich-editor.esm.js.map +1 -0
  4. package/dist/rich-editor.min.js +2 -0
  5. package/dist/rich-editor.min.js.map +1 -0
  6. package/package.json +12 -7
  7. package/index.js +0 -221
  8. package/lib/core/editor.js +0 -1175
  9. package/lib/core/format.js +0 -542
  10. package/lib/core/module.js +0 -81
  11. package/lib/core/registry.js +0 -152
  12. package/lib/formats/background.js +0 -212
  13. package/lib/formats/bold.js +0 -67
  14. package/lib/formats/capitalization.js +0 -563
  15. package/lib/formats/color.js +0 -165
  16. package/lib/formats/emoji.js +0 -282
  17. package/lib/formats/font-family.js +0 -547
  18. package/lib/formats/heading.js +0 -502
  19. package/lib/formats/image.js +0 -344
  20. package/lib/formats/import.js +0 -385
  21. package/lib/formats/indent.js +0 -297
  22. package/lib/formats/italic.js +0 -27
  23. package/lib/formats/line-height.js +0 -558
  24. package/lib/formats/link.js +0 -251
  25. package/lib/formats/list.js +0 -635
  26. package/lib/formats/strike.js +0 -31
  27. package/lib/formats/subscript.js +0 -36
  28. package/lib/formats/superscript.js +0 -35
  29. package/lib/formats/table.js +0 -288
  30. package/lib/formats/tag.js +0 -304
  31. package/lib/formats/text-align.js +0 -421
  32. package/lib/formats/text-size.js +0 -497
  33. package/lib/formats/underline.js +0 -30
  34. package/lib/formats/video.js +0 -372
  35. package/lib/modules/block-toolbar.js +0 -628
  36. package/lib/modules/code-view.js +0 -434
  37. package/lib/modules/history.js +0 -410
  38. package/lib/modules/resize-handles.js +0 -677
  39. package/lib/modules/table-toolbar.js +0 -618
  40. package/lib/modules/toolbar.js +0 -424
  41. package/lib/styles-loader.js +0 -144
  42. package/lib/styles.css +0 -2123
  43. package/lib/ui/color-picker.js +0 -296
  44. package/lib/ui/customselect.js +0 -319
  45. package/lib/ui/emoji-picker.js +0 -196
  46. package/lib/ui/icons.js +0 -413
  47. package/lib/ui/image-popup.js +0 -444
  48. package/lib/ui/import-popup.js +0 -288
  49. package/lib/ui/link-popup.js +0 -191
  50. package/lib/ui/list-picker.js +0 -307
  51. package/lib/ui/select-button.js +0 -61
  52. package/lib/ui/table-popup.js +0 -171
  53. package/lib/ui/tag-popup.js +0 -249
  54. package/lib/ui/text-align-picker.js +0 -281
  55. package/lib/ui/video-popup.js +0 -422
  56. package/lib/utils/history-helper.js +0 -50
  57. package/lib/utils/popup-helper.js +0 -219
  58. package/lib/utils/popup-positioning.js +0 -231
@@ -1,1175 +0,0 @@
1
- import registry from './registry.js';
2
- import Module from './module.js';
3
-
4
- /**
5
- * Main Editor class - Inspired by Quill's architecture
6
- * This replaces the monolithic EditorCore class
7
- */
8
- export default class Editor {
9
- static DEFAULTS = {
10
- placeholder: 'Start typing...',
11
- theme: 'light',
12
- height: 400,
13
- width: 800,
14
- maxWidth: 1200,
15
- maxHeight: 800,
16
- content: null, // Default content for the editor
17
- features: {
18
- emoji: true,
19
- image: true,
20
- table: true,
21
- wordCount: true,
22
- breadcrumb: true
23
- }
24
- };
25
-
26
- // Static reference to current editor instance
27
- static currentInstance = null;
28
- // Static map to track all editor instances
29
- static instances = new Map();
30
-
31
- constructor(selector, options = {}) {
32
- this.options = { ...Editor.DEFAULTS, ...options };
33
- this.root = typeof selector === 'string' ? document.querySelector(selector) : selector;
34
- this.modules = new Map();
35
- this.formats = new Map();
36
- this.registry = registry;
37
- this.events = new Map(); // Add event system
38
-
39
- // State management
40
- this.toolbarBtns = {};
41
- this.statusbarEls = {};
42
- this.dropdownMenus = {};
43
-
44
- // Popup management - each editor has its own popup instances
45
- this.popupInstances = new Map();
46
-
47
- // Set as current instance
48
- Editor.currentInstance = this;
49
-
50
- // Register this instance
51
- const instanceId = this.generateInstanceId();
52
- this.instanceId = instanceId;
53
- Editor.instances.set(instanceId, this);
54
-
55
- this.init();
56
- }
57
-
58
- /**
59
- * Generate unique instance ID
60
- */
61
- generateInstanceId() {
62
- return 'editor_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
63
- }
64
-
65
- /**
66
- * Initialize editor
67
- */
68
- init() {
69
- this.createStructure();
70
- this.loadModules();
71
- this.loadFormats();
72
- this.setupEventListeners();
73
- this.updateStatusbar();
74
- }
75
-
76
- /**
77
- * Create basic DOM structure - extracted from EditorCore.init()
78
- * TODO: Copy implementation from EditorCore.init()
79
- */
80
- createStructure() {
81
- // Create wrapper
82
- this.wrapper = document.createElement('div');
83
- this.wrapper.className = 'yjd-rich-editor';
84
-
85
- // Apply dynamic sizing
86
- this.wrapper.style.width = this.options.width + 'px';
87
- this.wrapper.style.maxWidth = this.options.maxWidth + 'px';
88
- this.wrapper.style.minHeight = this.options.height + 'px';
89
- this.wrapper.style.maxHeight = this.options.maxHeight + 'px';
90
-
91
- // Set position relative for popup positioning
92
- this.wrapper.style.position = 'relative';
93
-
94
- // Create editor area
95
- this.editor = document.createElement('div');
96
- this.editor.className = 'rich-editor-area';
97
- this.editor.contentEditable = true;
98
- this.editor.setAttribute('data-placeholder', this.options.placeholder);
99
-
100
- // Force browser to create <p> tags instead of <div> when pressing Enter
101
- try {
102
- document.execCommand('defaultParagraphSeparator', false, 'p');
103
- } catch (e) {
104
- console.warn('Could not set defaultParagraphSeparator:', e);
105
- }
106
-
107
- // Add default content
108
- this.editor.innerHTML = this.getDefaultContent();
109
-
110
- this.wrapper.appendChild(this.editor);
111
-
112
- // Create popup container
113
- this.popupContainer = document.createElement('div');
114
- this.popupContainer.className = 'rich-editor-popup-container';
115
- this.popupContainer.style.position = 'absolute';
116
- this.popupContainer.style.top = '0';
117
- this.popupContainer.style.left = '0';
118
- this.popupContainer.style.width = '100%';
119
- this.popupContainer.style.height = '100%';
120
- this.popupContainer.style.pointerEvents = 'none';
121
- this.popupContainer.style.zIndex = '1000';
122
- this.wrapper.appendChild(this.popupContainer);
123
-
124
- // Create statusbar if needed
125
- if (this.options.features.wordCount || this.options.features.breadcrumb) {
126
- this.createStatusbar();
127
- }
128
-
129
- // Add wrapper to root
130
- this.root.appendChild(this.wrapper);
131
-
132
- // Initialize placeholder visibility
133
- this.updatePlaceholderVisibility();
134
- }
135
-
136
- /**
137
- * Check if content is HTML or plain text
138
- * @param {string} content - Content to check
139
- * @returns {boolean} True if content appears to be HTML
140
- */
141
- isHtmlContent(content) {
142
- if (!content || typeof content !== 'string') {
143
- return false;
144
- }
145
-
146
- // Trim whitespace for checking
147
- const trimmed = content.trim();
148
-
149
- // Check for common HTML patterns
150
- const htmlPatterns = [
151
- /<[^>]+>/, // Contains HTML tags
152
- /&[a-zA-Z]+;/, // Contains HTML entities
153
- /&#\d+;/, // Contains numeric HTML entities
154
- ];
155
-
156
- return htmlPatterns.some(pattern => pattern.test(trimmed));
157
- }
158
-
159
- /**
160
- * Wrap plain text content in a paragraph tag
161
- * @param {string} content - Content to wrap
162
- * @returns {string} Wrapped content
163
- */
164
- wrapTextInParagraph(content) {
165
- if (!content || typeof content !== 'string') {
166
- return '<p><br></p>';
167
- }
168
-
169
- const trimmed = content.trim();
170
-
171
- // If content is already HTML, return as is
172
- if (this.isHtmlContent(trimmed)) {
173
- return trimmed;
174
- }
175
-
176
- // If content is empty, return empty paragraph
177
- if (trimmed === '') {
178
- return '<p><br></p>';
179
- }
180
-
181
- // Wrap plain text in paragraph tag
182
- return `<p>${trimmed}</p>`;
183
- }
184
-
185
- /**
186
- * Get default content for editor
187
- */
188
- getDefaultContent() {
189
- // If custom content is provided in options, use it
190
- if (this.options.content) {
191
- return this.wrapTextInParagraph(this.options.content);
192
- }
193
-
194
- // Return completely empty content to show placeholder
195
- return '';
196
- }
197
-
198
- /**
199
- * Create statusbar - extracted from EditorCore
200
- * TODO: Copy implementation from EditorCore.init()
201
- */
202
- createStatusbar() {
203
- this.statusbar = document.createElement('div');
204
- this.statusbar.className = 'rich-editor-statusbar';
205
-
206
- // Create breadcrumb and word count elements
207
- this.statusbarEls.breadcrumb = document.createElement('span');
208
- this.statusbarEls.breadcrumb.className = 'rich-editor-breadcrumb';
209
-
210
- this.statusbarEls.wordcount = document.createElement('span');
211
- this.statusbarEls.wordcount.className = 'wordcount';
212
-
213
- this.statusbar.appendChild(this.statusbarEls.breadcrumb);
214
- this.statusbar.appendChild(this.statusbarEls.wordcount);
215
- this.wrapper.appendChild(this.statusbar);
216
- }
217
-
218
- /**
219
- * Load and initialize modules
220
- */
221
- loadModules() {
222
- // Determine which modules to load
223
- let modulesToLoad;
224
-
225
- // Check if user provided toolbar configuration
226
- const hasToolbarConfig = this.options.toolbar || this.options.toolbar1 || this.options.toolbar2;
227
-
228
- if (hasToolbarConfig) {
229
- // User wants custom toolbar - load only basic modules
230
- modulesToLoad = this.options.modules || ['toolbar', 'history'];
231
- } else {
232
- // No toolbar config - load full feature set
233
- modulesToLoad = this.options.modules || ['toolbar', 'history', 'block-toolbar', 'table-toolbar', 'code-view', 'theme-switcher', 'resize-handles'];
234
- }
235
-
236
-
237
- modulesToLoad.forEach(moduleName => {
238
- const ModuleClass = this.registry.get(`modules/${moduleName}`);
239
- if (ModuleClass) {
240
- // For toolbar module, pass all options so it can detect toolbar config
241
- const moduleOptions = moduleName === 'toolbar' ? this.options : (this.options[moduleName] || this.options);
242
- const moduleInstance = new ModuleClass(this, moduleOptions);
243
- this.modules.set(moduleName, moduleInstance);
244
-
245
- // Insert toolbar before editor
246
- if (moduleName === 'toolbar' && moduleInstance.getContainer) {
247
- const toolbarContainer = moduleInstance.getContainer();
248
- this.wrapper.insertBefore(toolbarContainer, this.editor);
249
-
250
- // Listen for toolbar events
251
- moduleInstance.on('toolbar-click', (data) => {
252
- this.handleToolbarClick(data);
253
- });
254
- }
255
-
256
- } else {
257
- }
258
- });
259
- }
260
-
261
- /**
262
- * Load and initialize formats
263
- */
264
- loadFormats() {
265
- // Determine which formats to load
266
- let formatsToLoad;
267
-
268
- // Check if user provided toolbar configuration
269
- const hasToolbarConfig = this.options.toolbar || this.options.toolbar1 || this.options.toolbar2;
270
-
271
- if (hasToolbarConfig) {
272
- // User wants custom toolbar - load only basic formats
273
- formatsToLoad = this.options.formats || ['bold', 'italic', 'underline', 'strike'];
274
- } else {
275
- // No toolbar config - load full feature set
276
- formatsToLoad = this.options.formats || [
277
- 'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript',
278
- 'color', 'background', 'text-align', 'text-size', 'link',
279
- 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
280
- 'paragraph', 'pre'
281
- ];
282
- }
283
-
284
-
285
- formatsToLoad.forEach(formatName => {
286
- const FormatClass = this.registry.get(`formats/${formatName}`);
287
- if (FormatClass) {
288
- this.formats.set(formatName, FormatClass);
289
- } else {
290
- }
291
- });
292
- }
293
-
294
- /**
295
- * Setup event listeners - extracted from EditorCore
296
- * TODO: Copy implementation from EditorCore.bindEvents()
297
- */
298
- setupEventListeners() {
299
- // Basic input event
300
- this.editor.addEventListener('input', () => {
301
- // Check if editor is empty and create a paragraph element if needed
302
- this.ensureEditorHasContent();
303
-
304
- // Update placeholder visibility
305
- this.updatePlaceholderVisibility();
306
-
307
- this.updateStatusbar();
308
- this.onContentChange();
309
- });
310
-
311
- // Selection change event
312
- document.addEventListener('selectionchange', () => {
313
- if (document.activeElement === this.editor || this.editor.contains(document.activeElement)) {
314
- this.onSelectionChange();
315
- }
316
- });
317
-
318
- // Mouse up event to update text-size button when clicking/moving cursor
319
- this.editor.addEventListener('mouseup', () => {
320
- // Small delay to ensure selection is updated
321
- setTimeout(() => {
322
- this.onSelectionChange();
323
- }, 10);
324
- });
325
-
326
- // Image click event for selection
327
- this.editor.addEventListener('click', (e) => {
328
- // Handle link clicks - open in new tab
329
- // if (e.target.tagName === 'A' && e.target.href) {
330
- // e.preventDefault();
331
- // window.open(e.target.href, '_blank', 'noopener,noreferrer');
332
- // }
333
-
334
- // Ensure there's always a paragraph element for editing when clicking
335
- setTimeout(() => {
336
- this.ensureEditorHasContent();
337
- }, 0);
338
- });
339
-
340
- // Image context menu (right-click)
341
- this.editor.addEventListener('contextmenu', (e) => {
342
- // Image context menu functionality removed - methods don't exist
343
- });
344
-
345
- // Handle keydown events to ensure content structure
346
- this.editor.addEventListener('keydown', (e) => {
347
- // Check for delete/backspace operations that might empty the editor
348
- if (e.key === 'Delete' || e.key === 'Backspace') {
349
- // Use setTimeout to check after the deletion occurs
350
- setTimeout(() => {
351
- this.ensureEditorHasContent();
352
- this.updatePlaceholderVisibility();
353
- }, 0);
354
- }
355
- });
356
-
357
- // Handle paste events
358
- this.editor.addEventListener('paste', () => {
359
- // Check content after paste operation
360
- setTimeout(() => {
361
- this.ensureEditorHasContent();
362
- this.updatePlaceholderVisibility();
363
- }, 0);
364
- });
365
-
366
- // Handle drop events (drag and drop)
367
- this.editor.addEventListener('drop', () => {
368
- // Check content after drop operation
369
- setTimeout(() => {
370
- this.ensureEditorHasContent();
371
- this.updatePlaceholderVisibility();
372
- }, 0);
373
- });
374
-
375
- // Handle cut events
376
- this.editor.addEventListener('cut', () => {
377
- // Check content after cut operation
378
- setTimeout(() => {
379
- this.ensureEditorHasContent();
380
- this.updatePlaceholderVisibility();
381
- }, 0);
382
- });
383
-
384
- // Focus editor on load
385
- setTimeout(() => {
386
- // Ensure editor has proper content structure on load
387
- this.ensureEditorHasContent();
388
- this.updatePlaceholderVisibility();
389
- this.focus();
390
- }, 100);
391
-
392
- // Handle focus events to ensure content structure
393
- this.editor.addEventListener('focus', () => {
394
- // Ensure there's always a paragraph element for editing when focusing
395
- setTimeout(() => {
396
- this.ensureEditorHasContent();
397
- this.updatePlaceholderVisibility();
398
- }, 0);
399
- });
400
- }
401
-
402
- /**
403
- * Handle content changes
404
- */
405
- onContentChange() {
406
- // Check if editor is empty and create a paragraph element if needed
407
- this.ensureEditorHasContent();
408
-
409
- this.modules.forEach(module => {
410
- if (typeof module.onContentChange === 'function') {
411
- module.onContentChange();
412
- }
413
- });
414
-
415
- // Get current content
416
- const content = this.getContent();
417
-
418
- // Call onChange callback if provided
419
- if (this.options.onChange && typeof this.options.onChange === 'function') {
420
- this.options.onChange(content);
421
- }
422
-
423
- // Emit text-change event
424
- this.emit('text-change', content);
425
- }
426
-
427
- /**
428
- * Ensure editor always has a paragraph element for editing
429
- * This prevents users from editing directly in the editor container
430
- */
431
- ensureEditorHasContent() {
432
- // Check if editor is empty or only contains whitespace/empty elements
433
- const isEmpty = this.isEditorEmpty();
434
-
435
- if (isEmpty) {
436
- // Create a new paragraph element
437
- const paragraph = document.createElement('p');
438
- paragraph.innerHTML = '<br>';
439
-
440
- // Clear editor and add the paragraph
441
- this.editor.innerHTML = '';
442
- this.editor.appendChild(paragraph);
443
-
444
- // Set cursor position to the paragraph
445
- this.setCursorToElement(paragraph);
446
-
447
- // Focus the editor
448
- this.editor.focus();
449
- } else {
450
- // Check if we need to ensure there's always a paragraph element for editing
451
- //this.ensureParagraphForEditing();
452
- }
453
- }
454
-
455
- /**
456
- * Ensure there's always a paragraph element available for editing
457
- * This prevents users from editing directly in the editor container
458
- */
459
- ensureParagraphForEditing() {
460
- const children = this.editor.children;
461
-
462
- // If editor has no children, create a paragraph
463
- if (children.length === 0) {
464
- const paragraph = document.createElement('p');
465
- paragraph.innerHTML = '<br>';
466
- this.editor.appendChild(paragraph);
467
- this.setCursorToElement(paragraph);
468
- return;
469
- }
470
-
471
- // Check if the last child is a block element that can contain text
472
- const lastChild = children[children.length - 1];
473
- const blockTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'ARTICLE', 'SECTION', 'MAIN', 'ASIDE'];
474
-
475
- // Only add paragraph if the last child is not a block element that can contain text
476
- if (!blockTags.includes(lastChild.tagName)) {
477
- // Add a paragraph element at the end for editing
478
- const paragraph = document.createElement('p');
479
- paragraph.innerHTML = '<br>';
480
- this.editor.appendChild(paragraph);
481
- }
482
- }
483
-
484
- /**
485
- * Check if editor is empty or contains only empty elements
486
- */
487
- isEditorEmpty() {
488
- const content = this.editor.innerHTML.trim();
489
-
490
- // Check for completely empty content
491
- if (content === '') return true;
492
-
493
- // Check for common empty states
494
- const emptyStates = [
495
- '<br>',
496
- '<div><br></div>',
497
- '<p><br></p>',
498
- '<p></p>',
499
- '<div></div>',
500
- '<p>&nbsp;</p>',
501
- '<div>&nbsp;</div>'
502
- ];
503
-
504
- if (emptyStates.includes(content)) return true;
505
-
506
- // Check if editor only contains empty block elements
507
- const children = this.editor.children;
508
- if (children.length === 0) return true;
509
-
510
- // Check if all children are empty
511
- for (let i = 0; i < children.length; i++) {
512
- const child = children[i];
513
- const childContent = child.innerHTML.trim();
514
-
515
- // If any child has content, editor is not empty
516
- if (childContent !== '' &&
517
- childContent !== '<br>' &&
518
- childContent !== '&nbsp;' &&
519
- childContent !== '<br>&nbsp;' &&
520
- childContent !== '&nbsp;<br>') {
521
- return false;
522
- }
523
- }
524
-
525
- return true;
526
- }
527
-
528
- /**
529
- * Set cursor position to a specific element
530
- */
531
- setCursorToElement(element) {
532
- const range = document.createRange();
533
- const selection = window.getSelection();
534
-
535
- // Try to set cursor at the beginning of the element
536
- if (element.firstChild && element.firstChild.nodeType === Node.TEXT_NODE) {
537
- range.setStart(element.firstChild, 0);
538
- } else {
539
- range.setStart(element, 0);
540
- }
541
-
542
- range.collapse(true);
543
-
544
- selection.removeAllRanges();
545
- selection.addRange(range);
546
- }
547
-
548
- /**
549
- * Handle selection changes
550
- */
551
- onSelectionChange() {
552
- const selection = window.getSelection();
553
- const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
554
-
555
- // Check if selection is within rich-editor-area
556
- const isInEditableArea = this.isSelectionInEditableArea(selection);
557
-
558
- // Update all modules with selection info
559
- this.modules.forEach(module => {
560
- if (typeof module.onSelectionChange === 'function') {
561
- module.onSelectionChange(range, isInEditableArea);
562
- }
563
- });
564
-
565
- // Update toolbar button states
566
- this.updateToolbarButtonStates();
567
-
568
- // Update toolbar buttons accessibility
569
- this.updateToolbarAccessibility(isInEditableArea);
570
-
571
- // Update statusbar when selection changes
572
- this.updateStatusbar();
573
- }
574
-
575
- /**
576
- * Check if current selection is within the rich-editor-area
577
- */
578
- isSelectionInEditableArea(selection) {
579
- if (!selection || selection.rangeCount === 0) {
580
- return false;
581
- }
582
-
583
- const range = selection.getRangeAt(0);
584
- const startContainer = range.startContainer;
585
- const endContainer = range.endContainer;
586
-
587
- // Check if both start and end containers are within rich-editor-area
588
- const startInEditor = this.isNodeInEditableArea(startContainer);
589
- const endInEditor = this.isNodeInEditableArea(endContainer);
590
-
591
- return startInEditor && endInEditor;
592
- }
593
-
594
- /**
595
- * Check if a node is within the rich-editor-area
596
- */
597
- isNodeInEditableArea(node) {
598
- if (!node) return false;
599
-
600
- // Traverse up the DOM tree to find rich-editor-area
601
- let currentNode = node.nodeType === Node.TEXT_NODE ? node.parentNode : node;
602
-
603
- while (currentNode && currentNode !== document.body) {
604
- if (currentNode === this.editor ||
605
- (currentNode.classList && currentNode.classList.contains('rich-editor-area'))) {
606
- return true;
607
- }
608
- currentNode = currentNode.parentNode;
609
- }
610
-
611
- return false;
612
- }
613
-
614
- /**
615
- * Update toolbar accessibility based on selection location
616
- */
617
- updateToolbarAccessibility(isInEditableArea) {
618
- const toolbar = this.getModule('toolbar');
619
- if (!toolbar) return;
620
-
621
- // List of commands that should be disabled when outside editable area
622
- // Note: undo/redo are NOT in this list - they should always work
623
- const editingCommands = [
624
- 'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript',
625
- 'color', 'background', 'link', 'table', 'heading',
626
- 'font-family', 'line-height', 'capitalization', 'text-align', 'list',
627
- 'indent-increase', 'indent-decrease', 'text-size'
628
- ];
629
-
630
- editingCommands.forEach(command => {
631
- toolbar.setButtonDisabled(command, !isInEditableArea);
632
- });
633
-
634
- // These commands should always be enabled regardless of selection location
635
- const alwaysEnabledCommands = ['more', 'undo', 'redo', 'code-view', 'theme'];
636
- alwaysEnabledCommands.forEach(command => {
637
- toolbar.setButtonDisabled(command, false);
638
- });
639
- }
640
-
641
- /**
642
- * Update statusbar - extracted from EditorCore
643
- * TODO: Copy implementation from EditorCore.updateStatusbar()
644
- */
645
- updateStatusbar() {
646
- if (!this.statusbar) return;
647
-
648
- const sel = window.getSelection();
649
- if (!sel) return;
650
-
651
- // Update breadcrumb
652
- if (this.statusbarEls.breadcrumb && this.options.features.breadcrumb) {
653
- const currentNode = sel.anchorNode;
654
- const path = [];
655
- let element = currentNode?.nodeType === 3 ? currentNode.parentElement : currentNode;
656
-
657
- while (element && element !== this.editor && element !== document.body) {
658
- if (element.tagName) {
659
- let tagInfo = element.tagName.toLowerCase();
660
- if (element.className && typeof element.className === 'string') {
661
- const classes = element.className.trim();
662
- if (classes) {
663
- tagInfo += '.' + classes.split(' ').join('.');
664
- }
665
- }
666
- if (element.id) {
667
- tagInfo += '#' + element.id;
668
- }
669
- path.unshift(tagInfo);
670
- }
671
- element = element.parentElement;
672
- }
673
-
674
- this.statusbarEls.breadcrumb.textContent = path.length > 0 ? path.join(' > ') : 'editor';
675
- }
676
-
677
- // Update word count
678
- if (this.statusbarEls.wordcount && this.options.features.wordCount) {
679
- const text = this.editor.textContent || '';
680
- const words = text.trim() ? text.trim().split(/\s+/).length : 0;
681
- const chars = text.length;
682
- const charsNoSpaces = text.replace(/\s/g, '').length;
683
-
684
- this.statusbarEls.wordcount.textContent = `${words} words, ${chars} chars (${charsNoSpaces} no spaces)`;
685
- }
686
- }
687
-
688
- /**
689
- * Focus editor
690
- */
691
- focus() {
692
- if (this.editor) {
693
- this.editor.focus();
694
- }
695
- }
696
-
697
- /**
698
- * Get editor content
699
- */
700
- getContent() {
701
- return this.editor.innerHTML;
702
- }
703
-
704
- /**
705
- * Set editor content
706
- */
707
- setContent(html) {
708
- // Wrap plain text content in paragraph tag if needed
709
- const processedContent = this.wrapTextInParagraph(html);
710
- this.editor.innerHTML = processedContent;
711
- this.onContentChange();
712
- }
713
-
714
- /**
715
- * Get module instance
716
- */
717
- getModule(name) {
718
- return this.modules.get(name);
719
- }
720
-
721
- /**
722
- * Get format class
723
- */
724
- getFormat(name) {
725
- return this.formats.get(name);
726
- }
727
-
728
- /**
729
- * Register new items
730
- */
731
- register(path, definition, suppressWarning = false) {
732
- this.registry.register(path, definition, suppressWarning);
733
- }
734
-
735
- /**
736
- * Handle toolbar button clicks
737
- */
738
- handleToolbarClick(data) {
739
- const { command, button, value } = data;
740
-
741
- // Set this editor as current instance for the duration of this command
742
- const originalCurrent = Editor.currentInstance;
743
- Editor.currentInstance = this;
744
-
745
- // Emit toolbar-click event for modules to listen
746
- this.emit('toolbar-click', data);
747
-
748
- // Commands that should always work regardless of selection location
749
- const alwaysAllowedCommands = ['more', 'undo', 'redo', 'code-view', 'theme'];
750
-
751
- if (alwaysAllowedCommands.includes(command)) {
752
- // These commands can execute regardless of selection location
753
- switch (command) {
754
- case 'more':
755
- // More command is handled by toolbar module itself
756
- return;
757
- case 'undo':
758
- this.undo();
759
- return;
760
- case 'redo':
761
- this.redo();
762
- return;
763
- case 'code-view':
764
- // Code view command is handled by CodeView module itself
765
- // The module listens to 'toolbar-click' events and handles it internally
766
- return;
767
-
768
-
769
- }
770
- }
771
-
772
- // For all other commands, check if current selection is in editable area
773
- const selection = window.getSelection();
774
- const isInEditableArea = this.isSelectionInEditableArea(selection);
775
-
776
- if (!isInEditableArea) {
777
- console.warn(`Command '${command}' blocked: Selection outside editable area`);
778
- return;
779
- }
780
-
781
- // Handle formatting commands (only when selection is in editable area)
782
- switch (command) {
783
- case 'bold':
784
- case 'italic':
785
- case 'underline':
786
- case 'strike':
787
- case 'subscript':
788
- case 'superscript':
789
- case 'color':
790
- case 'background':
791
- case 'link':
792
- case 'table':
793
- case 'heading':
794
- case 'font-family':
795
- case 'line-height':
796
- case 'capitalization':
797
- case 'text-align':
798
- case 'text-size':
799
- case 'list':
800
- case 'indent-increase':
801
- case 'indent-decrease':
802
- case 'emoji':
803
- case 'image':
804
- case 'video':
805
- case 'tag':
806
-
807
- case 'import':
808
- this.toggleFormat(command);
809
- break;
810
- default:
811
- console.warn(`Unknown command: ${command}`);
812
- }
813
- }
814
-
815
- /**
816
- * Toggle format on current selection
817
- */
818
- toggleFormat(formatName) {
819
- // Save state before applying format
820
- const historyModule = this.getModule('history');
821
- if (historyModule && typeof historyModule.saveBeforeFormat === 'function') {
822
- historyModule.saveBeforeFormat();
823
- }
824
-
825
- // Map format names to registry keys
826
- const formatMap = {
827
- 'bold': 'bold',
828
- 'italic': 'italic',
829
- 'underline': 'underline',
830
- 'strike': 'strike',
831
- 'subscript': 'subscript',
832
- 'superscript': 'superscript',
833
- 'color': 'color',
834
- 'background': 'background',
835
- 'link': 'link',
836
- 'table': 'table',
837
- 'heading': 'heading',
838
- 'font-family': 'font-family',
839
- 'line-height': 'line-height',
840
- 'capitalization': 'capitalization',
841
- 'text-align': 'text-align',
842
- 'text-size': 'text-size',
843
- 'list': 'list',
844
- 'indent-increase': 'indent-increase',
845
- 'indent-decrease': 'indent-decrease',
846
- 'emoji': 'emoji',
847
- 'image': 'image',
848
- 'video': 'video',
849
- 'tag': 'tag',
850
-
851
- 'import': 'import'
852
- };
853
-
854
- const registryKey = formatMap[formatName];
855
- if (!registryKey) {
856
- console.warn(`Unknown format: ${formatName}`);
857
- return;
858
- }
859
-
860
- const FormatClass = this.registry.get(`formats/${registryKey}`);
861
- if (!FormatClass) {
862
- return;
863
- }
864
-
865
- // Create format instance and toggle
866
- const formatInstance = new FormatClass();
867
- formatInstance.toggle();
868
-
869
- // Update button state
870
- this.updateToolbarButtonStates();
871
-
872
- // Trigger content change for formats that modify content immediately
873
- // (like bold, italic, underline, etc. that use execCommand)
874
- const immediateFormats = ['bold', 'italic', 'underline', 'strike', 'subscript', 'superscript'];
875
- if (immediateFormats.includes(formatName)) {
876
- // Use setTimeout to ensure DOM changes are complete
877
- setTimeout(() => {
878
- this.onContentChange();
879
- }, 0);
880
- }
881
- }
882
-
883
- /**
884
- * Update toolbar button states based on current selection
885
- */
886
- updateToolbarButtonStates() {
887
- const toolbar = this.getModule('toolbar');
888
- if (!toolbar) return;
889
-
890
- const selection = window.getSelection();
891
- if (!selection || !selection.rangeCount) return;
892
-
893
- // Check if selection is in editable area
894
- const isInEditableArea = this.isSelectionInEditableArea(selection);
895
-
896
- const formats = ['heading', 'font-family', 'line-height', 'capitalization', 'text-align', 'list', 'indent-increase', 'indent-decrease', 'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript', 'color', 'background', 'link', 'table', 'text-size'];
897
-
898
- formats.forEach(formatName => {
899
- // Only check format state if selection is in editable area
900
- if (isInEditableArea) {
901
- const FormatClass = this.registry.get(`formats/${formatName}`);
902
- if (FormatClass) {
903
- // Create format instance for this specific editor
904
- let formatInstance;
905
- if (FormatClass.createForEditor) {
906
- formatInstance = FormatClass.createForEditor(this.instanceId);
907
- } else {
908
- // For formats that don't have createForEditor, temporarily set this as current instance
909
- const originalCurrent = Editor.currentInstance;
910
- Editor.currentInstance = this;
911
- formatInstance = new FormatClass();
912
- Editor.currentInstance = originalCurrent;
913
- }
914
-
915
- if (formatInstance) {
916
- const isActive = formatInstance.isActive();
917
- toolbar.setButtonActive(formatName, isActive);
918
-
919
- // Special handling for line-height: update button text
920
- if (formatName === 'line-height' && typeof formatInstance.updateButtonText === 'function') {
921
- formatInstance.updateButtonText();
922
- }
923
- }
924
- }
925
- } else {
926
- // Clear active state for buttons when outside editable area
927
- toolbar.setButtonActive(formatName, false);
928
- }
929
- });
930
-
931
- // Special handling for text-size: always update button text to show current size
932
- if (isInEditableArea) {
933
- const TextSizeClass = this.registry.get('formats/text-size');
934
- if (TextSizeClass && typeof TextSizeClass.updateButtonTextStatic === 'function') {
935
- TextSizeClass.updateButtonTextStatic(this.instanceId);
936
- }
937
- }
938
- }
939
-
940
- /**
941
- * Undo last action
942
- */
943
- undo() {
944
- const history = this.getModule('history');
945
- if (history && typeof history.undo === 'function') {
946
- history.undo();
947
- } else {
948
- document.execCommand('undo');
949
- }
950
- }
951
-
952
- /**
953
- * Redo last undone action
954
- */
955
- redo() {
956
- const history = this.getModule('history');
957
- if (history && typeof history.redo === 'function') {
958
- history.redo();
959
- } else {
960
- document.execCommand('redo');
961
- }
962
- }
963
-
964
- /**
965
- * Add event listener
966
- * @param {string} event - Event name
967
- * @param {function} handler - Event handler
968
- */
969
- on(event, handler) {
970
- if (!this.events.has(event)) {
971
- this.events.set(event, []);
972
- }
973
- this.events.get(event).push(handler);
974
- }
975
-
976
- /**
977
- * Remove event listener
978
- * @param {string} event - Event name
979
- * @param {function} handler - Event handler
980
- */
981
- off(event, handler) {
982
- if (this.events.has(event)) {
983
- const handlers = this.events.get(event);
984
- const index = handlers.indexOf(handler);
985
- if (index > -1) {
986
- handlers.splice(index, 1);
987
- }
988
- }
989
- }
990
-
991
- /**
992
- * Emit event
993
- * @param {string} event - Event name
994
- * @param {*} data - Event data
995
- */
996
- emit(event, data) {
997
- if (this.events.has(event)) {
998
- this.events.get(event).forEach(handler => {
999
- try {
1000
- handler(data);
1001
- } catch (error) {
1002
- console.error(`Error in event handler for ${event}:`, error);
1003
- }
1004
- });
1005
- }
1006
- }
1007
-
1008
- /**
1009
- * Prevent focus loss when clicking on UI elements
1010
- * @param {HTMLElement} element - Element to attach listener to
1011
- * @param {string} allowedSelector - CSS selector for elements that should allow normal click behavior
1012
- */
1013
- preventFocusLoss(element, allowedSelector = 'button, input, select, textarea, [contenteditable]') {
1014
- if (!element) return;
1015
-
1016
- element.addEventListener('mousedown', (e) => {
1017
- // Allow normal behavior for interactive elements
1018
- if (e.target.closest(allowedSelector)) {
1019
- return;
1020
- }
1021
-
1022
- // Prevent default behavior for non-interactive areas
1023
- e.preventDefault();
1024
-
1025
- // Restore focus to editor after event processing
1026
- setTimeout(() => {
1027
- this.focus();
1028
- }, 0);
1029
- });
1030
- }
1031
-
1032
- /**
1033
- * Get current editor instance
1034
- * @returns {Editor|null} Current editor instance
1035
- */
1036
- static getCurrentInstance() {
1037
- return Editor.currentInstance;
1038
- }
1039
-
1040
- /**
1041
- * Utility function to maintain editor focus after UI interactions
1042
- * @param {Function} callback - Function to execute before maintaining focus
1043
- * @param {Editor} editor - Editor instance to maintain focus on
1044
- */
1045
- static maintainFocus(callback, editor = null) {
1046
- if (typeof callback === 'function') {
1047
- callback();
1048
- }
1049
- const editorInstance = editor || Editor.getCurrentInstance();
1050
- if (editorInstance) {
1051
- setTimeout(() => editorInstance.focus(), 0);
1052
- }
1053
- }
1054
-
1055
- /**
1056
- * Get popup container for this editor instance
1057
- * @returns {HTMLElement} Popup container element
1058
- */
1059
- getPopupContainer() {
1060
- return this.popupContainer;
1061
- }
1062
-
1063
- /**
1064
- * Get popup container from current editor instance
1065
- * @returns {HTMLElement|null} Popup container element or null if no current instance
1066
- */
1067
- static getPopupContainer() {
1068
- const currentInstance = Editor.getCurrentInstance();
1069
- return currentInstance ? currentInstance.getPopupContainer() : null;
1070
- }
1071
-
1072
- /**
1073
- * Get popup instance for this editor
1074
- * @param {string} popupType - Type of popup (e.g., 'link', 'image', 'table')
1075
- * @returns {Object|null} Popup instance or null if not found
1076
- */
1077
- getPopupInstance(popupType) {
1078
- return this.popupInstances.get(popupType);
1079
- }
1080
-
1081
- /**
1082
- * Set popup instance for this editor
1083
- * @param {string} popupType - Type of popup
1084
- * @param {Object} popupInstance - Popup instance
1085
- */
1086
- setPopupInstance(popupType, popupInstance) {
1087
- this.popupInstances.set(popupType, popupInstance);
1088
- }
1089
-
1090
- /**
1091
- * Get popup instance by editor ID and popup type
1092
- * @param {string} editorId - Editor instance ID
1093
- * @param {string} popupType - Type of popup
1094
- * @returns {Object|null} Popup instance or null if not found
1095
- */
1096
- static getPopupInstanceById(editorId, popupType) {
1097
- const editor = Editor.instances.get(editorId);
1098
- return editor ? editor.getPopupInstance(popupType) : null;
1099
- }
1100
-
1101
- /**
1102
- * Get editor instance by ID
1103
- * @param {string} editorId - Editor instance ID
1104
- * @returns {Editor|null} Editor instance or null if not found
1105
- */
1106
- static getInstanceById(editorId) {
1107
- return Editor.instances.get(editorId);
1108
- }
1109
-
1110
- /**
1111
- * Get all editor instances
1112
- * @returns {Map} Map of all editor instances
1113
- */
1114
- static getAllInstances() {
1115
- return Editor.instances;
1116
- }
1117
-
1118
- /**
1119
- * Destroy popup instances for this editor
1120
- */
1121
- destroyPopupInstances() {
1122
- this.popupInstances.forEach((popupInstance, popupType) => {
1123
- if (popupInstance && typeof popupInstance.destroy === 'function') {
1124
- popupInstance.destroy();
1125
- }
1126
- });
1127
- this.popupInstances.clear();
1128
- }
1129
-
1130
- /**
1131
- * Update placeholder visibility based on editor content
1132
- */
1133
- updatePlaceholderVisibility() {
1134
- const hasContent = this.editor.textContent.trim().length > 0;
1135
-
1136
- if (hasContent) {
1137
- this.editor.classList.remove('placeholder-visible');
1138
- } else {
1139
- this.editor.classList.add('placeholder-visible');
1140
- }
1141
- }
1142
-
1143
- /**
1144
- * Destroy editor
1145
- */
1146
- destroy() {
1147
- // Destroy all modules
1148
- this.modules.forEach(module => {
1149
- if (typeof module.destroy === 'function') {
1150
- module.destroy();
1151
- }
1152
- });
1153
-
1154
- // Destroy popup instances
1155
- this.destroyPopupInstances();
1156
-
1157
- // Remove DOM elements
1158
- if (this.wrapper && this.wrapper.parentNode) {
1159
- this.wrapper.parentNode.removeChild(this.wrapper);
1160
- }
1161
-
1162
- // Clear references
1163
- this.modules.clear();
1164
- this.formats.clear();
1165
- this.events.clear(); // Clear events
1166
-
1167
- // Remove from instances map
1168
- Editor.instances.delete(this.instanceId);
1169
-
1170
- // Clear current instance if this was the current one
1171
- if (Editor.currentInstance === this) {
1172
- Editor.currentInstance = null;
1173
- }
1174
- }
1175
- }