@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.
- package/README.md +73 -22
- package/dist/rich-editor.esm.js +2 -0
- package/dist/rich-editor.esm.js.map +1 -0
- package/dist/rich-editor.min.js +2 -0
- package/dist/rich-editor.min.js.map +1 -0
- package/package.json +12 -7
- package/index.js +0 -221
- package/lib/core/editor.js +0 -1175
- package/lib/core/format.js +0 -542
- package/lib/core/module.js +0 -81
- package/lib/core/registry.js +0 -152
- package/lib/formats/background.js +0 -212
- package/lib/formats/bold.js +0 -67
- package/lib/formats/capitalization.js +0 -563
- package/lib/formats/color.js +0 -165
- package/lib/formats/emoji.js +0 -282
- package/lib/formats/font-family.js +0 -547
- package/lib/formats/heading.js +0 -502
- package/lib/formats/image.js +0 -344
- package/lib/formats/import.js +0 -385
- package/lib/formats/indent.js +0 -297
- package/lib/formats/italic.js +0 -27
- package/lib/formats/line-height.js +0 -558
- package/lib/formats/link.js +0 -251
- package/lib/formats/list.js +0 -635
- package/lib/formats/strike.js +0 -31
- package/lib/formats/subscript.js +0 -36
- package/lib/formats/superscript.js +0 -35
- package/lib/formats/table.js +0 -288
- package/lib/formats/tag.js +0 -304
- package/lib/formats/text-align.js +0 -421
- package/lib/formats/text-size.js +0 -497
- package/lib/formats/underline.js +0 -30
- package/lib/formats/video.js +0 -372
- package/lib/modules/block-toolbar.js +0 -628
- package/lib/modules/code-view.js +0 -434
- package/lib/modules/history.js +0 -410
- package/lib/modules/resize-handles.js +0 -677
- package/lib/modules/table-toolbar.js +0 -618
- package/lib/modules/toolbar.js +0 -424
- package/lib/styles-loader.js +0 -144
- package/lib/styles.css +0 -2123
- package/lib/ui/color-picker.js +0 -296
- package/lib/ui/customselect.js +0 -319
- package/lib/ui/emoji-picker.js +0 -196
- package/lib/ui/icons.js +0 -413
- package/lib/ui/image-popup.js +0 -444
- package/lib/ui/import-popup.js +0 -288
- package/lib/ui/link-popup.js +0 -191
- package/lib/ui/list-picker.js +0 -307
- package/lib/ui/select-button.js +0 -61
- package/lib/ui/table-popup.js +0 -171
- package/lib/ui/tag-popup.js +0 -249
- package/lib/ui/text-align-picker.js +0 -281
- package/lib/ui/video-popup.js +0 -422
- package/lib/utils/history-helper.js +0 -50
- package/lib/utils/popup-helper.js +0 -219
- package/lib/utils/popup-positioning.js +0 -231
package/lib/core/editor.js
DELETED
|
@@ -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> </p>',
|
|
501
|
-
'<div> </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 !== ' ' &&
|
|
519
|
-
childContent !== '<br> ' &&
|
|
520
|
-
childContent !== ' <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
|
-
}
|