@oix1987/yjd 1.0.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.
- package/README.md +91 -0
- package/index.d.ts +103 -0
- package/index.js +221 -0
- package/lib/core/editor.js +1175 -0
- package/lib/core/format.js +542 -0
- package/lib/core/module.js +81 -0
- package/lib/core/registry.js +152 -0
- package/lib/formats/background.js +212 -0
- package/lib/formats/bold.js +67 -0
- package/lib/formats/capitalization.js +563 -0
- package/lib/formats/color.js +165 -0
- package/lib/formats/emoji.js +282 -0
- package/lib/formats/font-family.js +547 -0
- package/lib/formats/heading.js +502 -0
- package/lib/formats/image.js +344 -0
- package/lib/formats/import.js +385 -0
- package/lib/formats/indent.js +297 -0
- package/lib/formats/italic.js +27 -0
- package/lib/formats/line-height.js +558 -0
- package/lib/formats/link.js +251 -0
- package/lib/formats/list.js +635 -0
- package/lib/formats/strike.js +31 -0
- package/lib/formats/subscript.js +36 -0
- package/lib/formats/superscript.js +35 -0
- package/lib/formats/table.js +288 -0
- package/lib/formats/tag.js +304 -0
- package/lib/formats/text-align.js +421 -0
- package/lib/formats/text-size.js +497 -0
- package/lib/formats/underline.js +30 -0
- package/lib/formats/video.js +372 -0
- package/lib/modules/block-toolbar.js +628 -0
- package/lib/modules/code-view.js +434 -0
- package/lib/modules/history.js +410 -0
- package/lib/modules/resize-handles.js +677 -0
- package/lib/modules/table-toolbar.js +618 -0
- package/lib/modules/toolbar.js +424 -0
- package/lib/styles-loader.js +144 -0
- package/lib/styles.css +2123 -0
- package/lib/ui/color-picker.js +296 -0
- package/lib/ui/customselect.js +319 -0
- package/lib/ui/emoji-picker.js +196 -0
- package/lib/ui/icons.js +413 -0
- package/lib/ui/image-popup.js +444 -0
- package/lib/ui/import-popup.js +288 -0
- package/lib/ui/link-popup.js +191 -0
- package/lib/ui/list-picker.js +307 -0
- package/lib/ui/select-button.js +61 -0
- package/lib/ui/table-popup.js +171 -0
- package/lib/ui/tag-popup.js +249 -0
- package/lib/ui/text-align-picker.js +281 -0
- package/lib/ui/video-popup.js +422 -0
- package/lib/utils/history-helper.js +50 -0
- package/lib/utils/popup-helper.js +219 -0
- package/lib/utils/popup-positioning.js +231 -0
- package/package.json +26 -0
|
@@ -0,0 +1,424 @@
|
|
|
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
|
+
{ group: 'text-format', items: ['bold', 'italic', 'underline', 'strike'] },
|
|
16
|
+
{ group: 'script', items: ['subscript', 'superscript'] },
|
|
17
|
+
{ group: 'colors', items: ['color', 'background'] },
|
|
18
|
+
{ group: 'link', items: ['link'] },
|
|
19
|
+
{ group: 'structure', items: ['heading', 'text-size'] },
|
|
20
|
+
{ group: 'table', items: ['table'] },
|
|
21
|
+
{ group: 'alignment', items: ['text-align'] },
|
|
22
|
+
{ group: 'actions', items: ['undo', 'redo'] },
|
|
23
|
+
{ group: 'more', items: ['more'] }
|
|
24
|
+
],
|
|
25
|
+
toolbar2: [
|
|
26
|
+
{ group: 'structure', items: ['list'] },
|
|
27
|
+
{ group: 'indent', items: ['indent-increase', 'indent-decrease'] },
|
|
28
|
+
{ group: 'font-family', items: ['font-family'] },
|
|
29
|
+
{ group: 'line-height', items: ['line-height'] },
|
|
30
|
+
{ group: 'capitalization', items: ['capitalization'] },
|
|
31
|
+
{ group: 'media', items: ['emoji', 'image', 'video'] },
|
|
32
|
+
{ group: 'content', items: ['tag'] },
|
|
33
|
+
{ group: 'view', items: ['code-view'] },
|
|
34
|
+
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
constructor(editor, options = {}) {
|
|
39
|
+
super(editor, options);
|
|
40
|
+
this.buttons = new Map();
|
|
41
|
+
this.toolbar2Visible = false;
|
|
42
|
+
this.events = new Map(); // Add event system
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Handle toolbar configuration
|
|
46
|
+
if (Array.isArray(options.toolbar)) {
|
|
47
|
+
// If toolbar array is provided, use only those items - COMPLETELY OVERRIDE DEFAULTS
|
|
48
|
+
this.options = {
|
|
49
|
+
container: null,
|
|
50
|
+
toolbar1: [
|
|
51
|
+
{ group: 'text-format', items: options.toolbar }
|
|
52
|
+
],
|
|
53
|
+
toolbar2: []
|
|
54
|
+
};
|
|
55
|
+
} else if (options.toolbar1 || options.toolbar2) {
|
|
56
|
+
// If specific toolbar1/toolbar2 config is provided, use it - COMPLETELY OVERRIDE DEFAULTS
|
|
57
|
+
this.options = {
|
|
58
|
+
container: null,
|
|
59
|
+
toolbar1: options.toolbar1 || [],
|
|
60
|
+
toolbar2: options.toolbar2 || []
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
// Use full default configuration
|
|
64
|
+
this.options = { ...Toolbar.DEFAULTS, ...options };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
this.init();
|
|
69
|
+
this.preloadIcons();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
init() {
|
|
73
|
+
this.container = this.createToolbarContainer();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Preload icons for better performance
|
|
78
|
+
*/
|
|
79
|
+
async preloadIcons() {
|
|
80
|
+
// Icons are now inline, no need to preload
|
|
81
|
+
// This method is kept for backward compatibility
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create main toolbar container with both toolbars
|
|
86
|
+
*/
|
|
87
|
+
createToolbarContainer() {
|
|
88
|
+
const container = document.createElement('div');
|
|
89
|
+
container.className = 'rich-editor-toolbar-container';
|
|
90
|
+
|
|
91
|
+
// Prevent toolbar from taking focus away from editor
|
|
92
|
+
this.editor.preventFocusLoss(container);
|
|
93
|
+
|
|
94
|
+
// Create toolbar 1
|
|
95
|
+
this.toolbar1 = this.createToolbar('rich-editor-toolbar-1', this.options.toolbar1);
|
|
96
|
+
container.appendChild(this.toolbar1);
|
|
97
|
+
|
|
98
|
+
// Create toolbar 2 (initially hidden)
|
|
99
|
+
this.toolbar2 = this.createToolbar('rich-editor-toolbar-2', this.options.toolbar2);
|
|
100
|
+
this.toolbar2.style.display = 'none';
|
|
101
|
+
container.appendChild(this.toolbar2);
|
|
102
|
+
|
|
103
|
+
return container;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create toolbar element
|
|
108
|
+
*/
|
|
109
|
+
createToolbar(className, toolbarItems) {
|
|
110
|
+
const toolbar = document.createElement('div');
|
|
111
|
+
toolbar.className = className;
|
|
112
|
+
|
|
113
|
+
// Create button groups based on toolbar config
|
|
114
|
+
if (Array.isArray(toolbarItems)) {
|
|
115
|
+
toolbarItems.forEach(group => {
|
|
116
|
+
if (group && group.group && Array.isArray(group.items)) {
|
|
117
|
+
// Create group container
|
|
118
|
+
const groupContainer = document.createElement('div');
|
|
119
|
+
groupContainer.className = `toolbar-group toolbar-group-${group.group}`;
|
|
120
|
+
|
|
121
|
+
// Add buttons to group
|
|
122
|
+
group.items.forEach(item => {
|
|
123
|
+
if (typeof item === 'string') {
|
|
124
|
+
this.addButton(groupContainer, item);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
toolbar.appendChild(groupContainer);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return toolbar;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Add button to toolbar
|
|
138
|
+
*/
|
|
139
|
+
addButton(container, format) {
|
|
140
|
+
// Special handling for more button
|
|
141
|
+
if (format === 'more') {
|
|
142
|
+
return this.addMoreButton(container);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Custom buttons with dropdowns
|
|
146
|
+
const customButtons = {
|
|
147
|
+
'heading': { text: 'Paragraph', width: '110px', title: 'Format (Headings & Paragraphs)' },
|
|
148
|
+
'font-family': { text: 'Font Family', width: '150px', title: 'Font Family' },
|
|
149
|
+
'line-height': { text: 'Line Height', width: '110px', title: 'Line Height' },
|
|
150
|
+
'capitalization': { text: 'Capitalization', width: '130px', title: 'Text Capitalization' },
|
|
151
|
+
'text-size': { text: 'Text Size', width: '100px', title: 'Text Size' }
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (customButtons[format]) {
|
|
155
|
+
const config = customButtons[format];
|
|
156
|
+
const customButton = createCustomButton(config.text, { width: config.width });
|
|
157
|
+
customButton.dataset.command = format;
|
|
158
|
+
customButton.classList.add('rich-editor-toolbar-btn', `${format}-btn`);
|
|
159
|
+
customButton.title = config.title;
|
|
160
|
+
|
|
161
|
+
customButton.addEventListener('click', (e) => {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
this.emit('toolbar-click', { command: format, button: customButton });
|
|
164
|
+
// Maintain editor focus after button click
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
this.editor.focus();
|
|
167
|
+
}, 0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
this.buttons.set(format, customButton);
|
|
171
|
+
container.appendChild(customButton);
|
|
172
|
+
return customButton;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Icon buttons with popups
|
|
176
|
+
const iconButtons = {
|
|
177
|
+
'text-align': { icon: 'align-left', title: 'Align Left' },
|
|
178
|
+
'list': { icon: 'list', title: 'List' }
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (iconButtons[format]) {
|
|
182
|
+
const config = iconButtons[format];
|
|
183
|
+
const button = document.createElement('button');
|
|
184
|
+
button.type = 'button';
|
|
185
|
+
button.className = `rich-editor-toolbar-btn ${format}-btn`;
|
|
186
|
+
button.dataset.command = format;
|
|
187
|
+
button.title = config.title;
|
|
188
|
+
|
|
189
|
+
const svgContent = IconUtils.getIcon(config.icon);
|
|
190
|
+
if (svgContent) {
|
|
191
|
+
button.innerHTML = `<span class="icon">${svgContent}</span>`;
|
|
192
|
+
} else {
|
|
193
|
+
button.textContent = format === 'text-align' ? '≡' : '•';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
button.addEventListener('click', (e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
this.emit('toolbar-click', { command: format, button: button });
|
|
199
|
+
// Maintain editor focus after button click
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
this.editor.focus();
|
|
202
|
+
}, 0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
this.buttons.set(format, button);
|
|
206
|
+
container.appendChild(button);
|
|
207
|
+
return button;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Regular icon buttons
|
|
211
|
+
const button = document.createElement('button');
|
|
212
|
+
button.type = 'button';
|
|
213
|
+
button.className = `rich-editor-toolbar-btn ${format}-btn`;
|
|
214
|
+
button.dataset.command = format;
|
|
215
|
+
|
|
216
|
+
// Add icon
|
|
217
|
+
const iconElement = IconUtils.createIconElement(format, {
|
|
218
|
+
width: '16px',
|
|
219
|
+
height: '16px'
|
|
220
|
+
});
|
|
221
|
+
button.appendChild(iconElement);
|
|
222
|
+
|
|
223
|
+
// Set title based on format
|
|
224
|
+
const titles = {
|
|
225
|
+
'bold': 'Bold (Ctrl+B)',
|
|
226
|
+
'italic': 'Italic (Ctrl+I)',
|
|
227
|
+
'underline': 'Underline (Ctrl+U)',
|
|
228
|
+
'strike': 'Strikethrough',
|
|
229
|
+
'subscript': 'Subscript',
|
|
230
|
+
'superscript': 'Superscript',
|
|
231
|
+
'color': 'Text Color',
|
|
232
|
+
'background': 'Background Color',
|
|
233
|
+
'link': 'Insert/Edit Link',
|
|
234
|
+
'table': 'Insert Table',
|
|
235
|
+
'undo': 'Undo (Ctrl+Z)',
|
|
236
|
+
'redo': 'Redo (Ctrl+Y)',
|
|
237
|
+
'indent-increase': 'Increase Indent',
|
|
238
|
+
'indent-decrease': 'Decrease Indent',
|
|
239
|
+
'emoji': 'Insert Emoji',
|
|
240
|
+
'image': 'Insert Image',
|
|
241
|
+
'video': 'Insert Video',
|
|
242
|
+
'tag': 'Insert Tag',
|
|
243
|
+
|
|
244
|
+
'import': 'Import Files',
|
|
245
|
+
'code-view': 'Switch to HTML Editor',
|
|
246
|
+
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
button.title = titles[format] || format;
|
|
250
|
+
|
|
251
|
+
// Add fallback for code-view
|
|
252
|
+
if (format === 'code-view') {
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
if (!iconElement.innerHTML.trim()) {
|
|
255
|
+
iconElement.innerHTML = '</>';
|
|
256
|
+
iconElement.style.fontSize = '12px';
|
|
257
|
+
iconElement.style.fontWeight = 'bold';
|
|
258
|
+
}
|
|
259
|
+
}, 1000);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
button.addEventListener('click', (e) => {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
this.emit('toolbar-click', { command: format, button });
|
|
265
|
+
// Maintain editor focus after button click
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
this.editor.focus();
|
|
268
|
+
}, 0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
this.buttons.set(format, button);
|
|
272
|
+
container.appendChild(button);
|
|
273
|
+
return button;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Add more button to toggle toolbar 2
|
|
278
|
+
*/
|
|
279
|
+
addMoreButton(container) {
|
|
280
|
+
const button = document.createElement('button');
|
|
281
|
+
button.type = 'button';
|
|
282
|
+
button.className = 'rich-editor-toolbar-btn more-btn';
|
|
283
|
+
button.dataset.command = 'more';
|
|
284
|
+
|
|
285
|
+
const iconElement = IconUtils.createIconElement('more', {
|
|
286
|
+
width: '16px',
|
|
287
|
+
height: '16px'
|
|
288
|
+
});
|
|
289
|
+
button.appendChild(iconElement);
|
|
290
|
+
button.title = 'More Options';
|
|
291
|
+
|
|
292
|
+
button.addEventListener('click', (e) => {
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
this.toggleToolbar2();
|
|
295
|
+
// Maintain editor focus after button click
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
this.editor.focus();
|
|
298
|
+
}, 0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
this.buttons.set('more', button);
|
|
302
|
+
container.appendChild(button);
|
|
303
|
+
return button;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Toggle toolbar 2 visibility
|
|
308
|
+
*/
|
|
309
|
+
toggleToolbar2() {
|
|
310
|
+
this.toolbar2Visible = !this.toolbar2Visible;
|
|
311
|
+
|
|
312
|
+
if (this.toolbar2Visible) {
|
|
313
|
+
this.toolbar2.style.display = 'flex';
|
|
314
|
+
this.toolbar2.style.borderTop = '1px solid #d1d5db';
|
|
315
|
+
} else {
|
|
316
|
+
this.toolbar2.style.display = 'none';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Update more button appearance
|
|
320
|
+
const moreButton = this.buttons.get('more');
|
|
321
|
+
if (moreButton) {
|
|
322
|
+
if (this.toolbar2Visible) {
|
|
323
|
+
moreButton.classList.add('active');
|
|
324
|
+
moreButton.title = 'Hide More Options';
|
|
325
|
+
} else {
|
|
326
|
+
moreButton.classList.remove('active');
|
|
327
|
+
moreButton.title = 'More Options';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get toolbar container element
|
|
334
|
+
*/
|
|
335
|
+
getContainer() {
|
|
336
|
+
return this.container;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get button by command
|
|
341
|
+
*/
|
|
342
|
+
getButton(command) {
|
|
343
|
+
return this.buttons.get(command);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Set button active state
|
|
348
|
+
*/
|
|
349
|
+
setButtonActive(command, isActive) {
|
|
350
|
+
const button = this.buttons.get(command);
|
|
351
|
+
if (button && button.classList) {
|
|
352
|
+
if (isActive) {
|
|
353
|
+
button.classList.add('active');
|
|
354
|
+
} else {
|
|
355
|
+
button.classList.remove('active');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Set button disabled state
|
|
362
|
+
*/
|
|
363
|
+
setButtonDisabled(command, isDisabled) {
|
|
364
|
+
const button = this.buttons.get(command);
|
|
365
|
+
if (button) {
|
|
366
|
+
button.disabled = isDisabled;
|
|
367
|
+
button.style.opacity = isDisabled ? '0.5' : '1';
|
|
368
|
+
button.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Set button title
|
|
374
|
+
*/
|
|
375
|
+
setButtonTitle(command, title) {
|
|
376
|
+
const button = this.buttons.get(command);
|
|
377
|
+
if (button) {
|
|
378
|
+
button.title = title;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Check if toolbar 2 is visible
|
|
384
|
+
*/
|
|
385
|
+
isToolbar2Visible() {
|
|
386
|
+
return this.toolbar2Visible;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Event system methods
|
|
391
|
+
*/
|
|
392
|
+
on(event, callback) {
|
|
393
|
+
if (!this.events.has(event)) {
|
|
394
|
+
this.events.set(event, []);
|
|
395
|
+
}
|
|
396
|
+
this.events.get(event).push(callback);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
emit(event, data) {
|
|
400
|
+
const callbacks = this.events.get(event);
|
|
401
|
+
if (callbacks) {
|
|
402
|
+
callbacks.forEach(callback => {
|
|
403
|
+
try {
|
|
404
|
+
callback(data);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(`Error in toolbar event ${event}:`, error);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Destroy toolbar
|
|
414
|
+
*/
|
|
415
|
+
destroy() {
|
|
416
|
+
if (this.container && this.container.parentNode) {
|
|
417
|
+
this.container.parentNode.removeChild(this.container);
|
|
418
|
+
}
|
|
419
|
+
this.buttons.clear();
|
|
420
|
+
this.events.clear();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export default Toolbar;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Loader - Load và inject CSS styles vào DOM
|
|
3
|
+
* Thay thế cho việc sử dụng inline styles
|
|
4
|
+
*/
|
|
5
|
+
class StylesLoader {
|
|
6
|
+
static loaded = false;
|
|
7
|
+
static styleElement = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load CSS từ file hoặc string
|
|
11
|
+
*/
|
|
12
|
+
static async loadStyles() {
|
|
13
|
+
if (this.loaded) return;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Tạo style element
|
|
17
|
+
this.styleElement = document.createElement('style');
|
|
18
|
+
this.styleElement.id = 'rich-editor-styles';
|
|
19
|
+
|
|
20
|
+
// Load CSS từ file
|
|
21
|
+
const response = await fetch(new URL('./styles.css', import.meta.url));
|
|
22
|
+
const cssText = await response.text();
|
|
23
|
+
|
|
24
|
+
this.styleElement.textContent = cssText;
|
|
25
|
+
|
|
26
|
+
// Inject vào head
|
|
27
|
+
document.head.appendChild(this.styleElement);
|
|
28
|
+
|
|
29
|
+
this.loaded = true;
|
|
30
|
+
|
|
31
|
+
} catch (error) {
|
|
32
|
+
|
|
33
|
+
// Fallback: load minimal styles
|
|
34
|
+
this.loadFallbackStyles();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load minimal fallback styles nếu không thể load từ file
|
|
40
|
+
*/
|
|
41
|
+
static loadFallbackStyles() {
|
|
42
|
+
const fallbackCSS = `
|
|
43
|
+
.yjd-rich-editor {
|
|
44
|
+
position: relative;
|
|
45
|
+
background: #fff;
|
|
46
|
+
border: 1px solid #ddd;
|
|
47
|
+
border-radius: 4px;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
font-family: system-ui, sans-serif;
|
|
51
|
+
}
|
|
52
|
+
.yjd-rich-editor .rich-editor-area {
|
|
53
|
+
flex: 1;
|
|
54
|
+
padding: 20px;
|
|
55
|
+
outline: none;
|
|
56
|
+
min-height: 100px;
|
|
57
|
+
}
|
|
58
|
+
.yjd-rich-editor .rich-editor-toolbar {
|
|
59
|
+
display: flex;
|
|
60
|
+
gap: 4px;
|
|
61
|
+
padding: 8px;
|
|
62
|
+
border-bottom: 1px solid #ddd;
|
|
63
|
+
background: #f9f9f9;
|
|
64
|
+
}
|
|
65
|
+
.yjd-rich-editor .rich-editor-toolbar-btn {
|
|
66
|
+
padding: 4px 8px;
|
|
67
|
+
border: 1px solid #ccc;
|
|
68
|
+
border-radius: 3px;
|
|
69
|
+
background: #fff;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
}
|
|
72
|
+
.yjd-rich-editor .table-grid-selector {
|
|
73
|
+
position: absolute;
|
|
74
|
+
background: white;
|
|
75
|
+
border: 1px solid #ccc;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
padding: 10px;
|
|
78
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
79
|
+
z-index: 1000;
|
|
80
|
+
display: none;
|
|
81
|
+
}
|
|
82
|
+
.yjd-rich-editor .table-grid-cell {
|
|
83
|
+
width: 20px;
|
|
84
|
+
height: 20px;
|
|
85
|
+
border: 1px solid #ddd;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
background: white;
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
this.styleElement = document.createElement('style');
|
|
92
|
+
this.styleElement.id = 'rich-editor-styles-fallback';
|
|
93
|
+
this.styleElement.textContent = fallbackCSS;
|
|
94
|
+
document.head.appendChild(this.styleElement);
|
|
95
|
+
|
|
96
|
+
this.loaded = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Unload styles
|
|
101
|
+
*/
|
|
102
|
+
static unloadStyles() {
|
|
103
|
+
if (this.styleElement && this.styleElement.parentNode) {
|
|
104
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
105
|
+
this.styleElement = null;
|
|
106
|
+
this.loaded = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if styles are loaded
|
|
112
|
+
*/
|
|
113
|
+
static isLoaded() {
|
|
114
|
+
return this.loaded;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reload styles
|
|
119
|
+
*/
|
|
120
|
+
static async reloadStyles() {
|
|
121
|
+
this.unloadStyles();
|
|
122
|
+
await this.loadStyles();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add custom CSS
|
|
127
|
+
*/
|
|
128
|
+
static addCustomCSS(css, id = 'rich-editor-custom') {
|
|
129
|
+
// Remove existing custom styles
|
|
130
|
+
const existing = document.getElementById(id);
|
|
131
|
+
if (existing) {
|
|
132
|
+
existing.remove();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add new custom styles
|
|
136
|
+
const style = document.createElement('style');
|
|
137
|
+
style.id = id;
|
|
138
|
+
style.textContent = css;
|
|
139
|
+
document.head.appendChild(style);
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default StylesLoader;
|