@oix1987/yjd 1.0.3 → 2.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.
Files changed (70) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +146 -142
  3. package/core.js +77 -0
  4. package/dist/core.esm.js +2 -0
  5. package/dist/core.esm.js.map +1 -0
  6. package/dist/rich-editor.esm.js +1 -1
  7. package/dist/rich-editor.esm.js.map +1 -1
  8. package/dist/rich-editor.min.js +1 -1
  9. package/dist/rich-editor.min.js.map +1 -1
  10. package/index.d.ts +134 -103
  11. package/index.js +227 -0
  12. package/lib/core/editor.js +1806 -0
  13. package/lib/core/format.js +540 -0
  14. package/lib/core/module.js +81 -0
  15. package/lib/core/registry.js +158 -0
  16. package/lib/formats/background.js +213 -0
  17. package/lib/formats/bold.js +49 -0
  18. package/lib/formats/capitalization.js +579 -0
  19. package/lib/formats/color.js +183 -0
  20. package/lib/formats/emoji.js +282 -0
  21. package/lib/formats/font-family.js +548 -0
  22. package/lib/formats/heading.js +502 -0
  23. package/lib/formats/image.js +347 -0
  24. package/lib/formats/import.js +385 -0
  25. package/lib/formats/indent.js +297 -0
  26. package/lib/formats/italic.js +27 -0
  27. package/lib/formats/line-height.js +562 -0
  28. package/lib/formats/link.js +251 -0
  29. package/lib/formats/list.js +635 -0
  30. package/lib/formats/strike.js +31 -0
  31. package/lib/formats/subscript.js +40 -0
  32. package/lib/formats/superscript.js +39 -0
  33. package/lib/formats/table.js +293 -0
  34. package/lib/formats/tag.js +304 -0
  35. package/lib/formats/text-align.js +422 -0
  36. package/lib/formats/text-size.js +498 -0
  37. package/lib/formats/underline.js +30 -0
  38. package/lib/formats/video.js +381 -0
  39. package/lib/modules/block-toolbar.js +639 -0
  40. package/lib/modules/code-view.js +447 -0
  41. package/lib/modules/find-replace.js +273 -0
  42. package/lib/modules/history.js +425 -0
  43. package/lib/modules/resize-handles.js +701 -0
  44. package/lib/modules/slash-menu.js +183 -0
  45. package/lib/modules/table-toolbar.js +635 -0
  46. package/lib/modules/toolbar.js +607 -0
  47. package/lib/styles-loader.js +142 -0
  48. package/{dist → lib}/styles.css +1285 -35
  49. package/lib/styles.css.js +2 -0
  50. package/lib/styles.min.css +1 -0
  51. package/lib/ui/color-picker.js +296 -0
  52. package/lib/ui/customselect.js +351 -0
  53. package/lib/ui/emoji-picker.js +196 -0
  54. package/lib/ui/icons.js +145 -0
  55. package/lib/ui/image-popup.js +435 -0
  56. package/lib/ui/import-popup.js +288 -0
  57. package/lib/ui/link-popup.js +139 -0
  58. package/lib/ui/list-picker.js +307 -0
  59. package/lib/ui/select-button.js +68 -0
  60. package/lib/ui/table-popup.js +171 -0
  61. package/lib/ui/tag-popup.js +249 -0
  62. package/lib/ui/text-align-picker.js +278 -0
  63. package/lib/ui/video-popup.js +413 -0
  64. package/lib/utils/exec-command.js +72 -0
  65. package/lib/utils/history-helper.js +50 -0
  66. package/lib/utils/popup-helper.js +219 -0
  67. package/lib/utils/popup-positioning.js +234 -0
  68. package/lib/utils/sanitize.js +164 -0
  69. package/package.json +51 -32
  70. package/umd-entry.js +18 -0
@@ -0,0 +1,447 @@
1
+ import Module from '../core/module.js';
2
+ import { sanitizeHtml } from '../utils/sanitize.js';
3
+
4
+ /**
5
+ * Code View Module - Toggles between normal editor view and HTML source code view
6
+ */
7
+ class CodeView extends Module {
8
+ constructor(editor, options = {}) {
9
+ super(editor, options);
10
+
11
+ this.isCodeView = false;
12
+ this.originalContent = '';
13
+ this.codeTextarea = null;
14
+ this.disabledModules = new Set(); // Track disabled modules
15
+
16
+ this.init();
17
+ }
18
+
19
+ init() {
20
+
21
+ // Listen for code view toggle events
22
+ this.editor.on('toolbar-click', (data) => {
23
+ if (data.command === 'code-view') {
24
+ this.toggleCodeView();
25
+ }
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Toggle between normal editor view and code view
31
+ */
32
+ toggleCodeView() {
33
+
34
+ if (this.isCodeView) {
35
+ this.showNormalView();
36
+ } else {
37
+ this.showCodeView();
38
+ }
39
+
40
+ this.updateToolbarButton();
41
+ }
42
+
43
+ /**
44
+ * Show code view - display HTML source
45
+ */
46
+ showCodeView() {
47
+ const editorArea = this.editor.editor;
48
+ if (!editorArea) return;
49
+
50
+ // Store original content
51
+ this.originalContent = editorArea.innerHTML;
52
+
53
+ // Create textarea for code editing
54
+ this.codeTextarea = document.createElement('textarea');
55
+ this.codeTextarea.className = 'code-view-textarea';
56
+ this.codeTextarea.value = this.formatHTML(this.originalContent);
57
+
58
+ // Replace editor content with textarea
59
+ editorArea.style.display = 'none';
60
+ editorArea.parentNode.insertBefore(this.codeTextarea, editorArea);
61
+
62
+ // Add CSS class to wrapper for styling
63
+ const wrapper = this.editor.wrapper;
64
+ if (wrapper) {
65
+ wrapper.classList.add('code-view-active');
66
+ }
67
+
68
+ // Focus on textarea
69
+ this.codeTextarea.focus();
70
+
71
+ // Set flag
72
+ this.isCodeView = true;
73
+
74
+ // Reflect the source content in the word/char counter
75
+ if (typeof this.editor.updateStatusbar === 'function') {
76
+ this.editor.updateStatusbar();
77
+ }
78
+
79
+ // Disable other features
80
+ this.disableOtherFeatures();
81
+
82
+ // Add event listener for real-time updates
83
+ this.codeTextarea.addEventListener('input', () => {
84
+ this.updateOriginalContent();
85
+ });
86
+
87
+ }
88
+
89
+ /**
90
+ * Show normal editor view - restore visual editor
91
+ */
92
+ showNormalView() {
93
+ const editorArea = this.editor.editor;
94
+ if (!editorArea || !this.codeTextarea) return;
95
+
96
+ // Get updated content from textarea
97
+ const updatedHTML = this.codeTextarea.value;
98
+
99
+ // Remove textarea
100
+ this.codeTextarea.parentNode.removeChild(this.codeTextarea);
101
+ this.codeTextarea = null;
102
+
103
+ // Remove CSS class from wrapper
104
+ const wrapper = this.editor.wrapper;
105
+ if (wrapper) {
106
+ wrapper.classList.remove('code-view-active');
107
+ }
108
+
109
+ // Show editor area
110
+ editorArea.style.display = '';
111
+
112
+ // Update editor content (sanitize HTML typed in the source view to prevent XSS)
113
+ editorArea.innerHTML = sanitizeHtml(updatedHTML);
114
+
115
+ // Focus on editor
116
+ editorArea.focus();
117
+
118
+ // Set flag
119
+ this.isCodeView = false;
120
+
121
+ // Enable other features
122
+ this.enableOtherFeatures();
123
+
124
+ // Trigger content change event
125
+ this.editor.onContentChange();
126
+
127
+ }
128
+
129
+ /**
130
+ * Disable other features when in code view
131
+ */
132
+ disableOtherFeatures() {
133
+ // Disable toolbar buttons (except code-view and theme)
134
+ const toolbar = this.editor.getModule('toolbar');
135
+ if (toolbar) {
136
+ const allCommands = [
137
+ 'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript',
138
+ 'color', 'background', 'link', 'table', 'heading',
139
+ 'font-family', 'line-height', 'capitalization', 'text-align', 'list',
140
+ 'indent-increase', 'indent-decrease', 'text-size', 'emoji', 'image',
141
+ 'video', 'tag', 'import', 'undo', 'redo',
142
+ 'clear-format', 'horizontal-rule', 'text-direction', 'find', 'more'
143
+ ];
144
+
145
+ allCommands.forEach(command => {
146
+ toolbar.setButtonDisabled(command, true);
147
+ });
148
+
149
+ // Keep code-view and theme enabled
150
+ toolbar.setButtonDisabled('code-view', false);
151
+ toolbar.setButtonDisabled('theme', false);
152
+ }
153
+
154
+ // Disable editor events
155
+ this.disableEditorEvents();
156
+
157
+ // Disable other modules
158
+ this.disableOtherModules();
159
+
160
+ // Hide any open popups
161
+ this.hideAllPopups();
162
+ }
163
+
164
+ /**
165
+ * Enable other features when returning to normal view
166
+ */
167
+ enableOtherFeatures() {
168
+ // Enable toolbar buttons
169
+ const toolbar = this.editor.getModule('toolbar');
170
+ if (toolbar) {
171
+ const allCommands = [
172
+ 'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript',
173
+ 'color', 'background', 'link', 'table', 'heading',
174
+ 'font-family', 'line-height', 'capitalization', 'text-align', 'list',
175
+ 'indent-increase', 'indent-decrease', 'text-size', 'emoji', 'image',
176
+ 'video', 'tag', 'import', 'undo', 'redo',
177
+ 'clear-format', 'horizontal-rule', 'text-direction', 'find', 'more'
178
+ ];
179
+
180
+ allCommands.forEach(command => {
181
+ toolbar.setButtonDisabled(command, false);
182
+ });
183
+ }
184
+
185
+ // Enable editor events
186
+ this.enableEditorEvents();
187
+
188
+ // Enable other modules
189
+ this.enableOtherModules();
190
+ }
191
+
192
+ /**
193
+ * Disable editor events when in code view
194
+ */
195
+ disableEditorEvents() {
196
+ const editorArea = this.editor.editor;
197
+ if (!editorArea) return;
198
+
199
+ // Make editor non-editable to disable all editing functionality
200
+ editorArea.contentEditable = false;
201
+
202
+ // Add a visual indicator that editor is disabled
203
+ editorArea.style.opacity = '0.5';
204
+ editorArea.style.pointerEvents = 'none';
205
+ editorArea.style.cursor = 'not-allowed';
206
+
207
+ // Add a title to indicate the editor is disabled
208
+ editorArea.title = 'Editor is disabled in code view mode. Click "Switch to Visual Editor" to return to normal editing.';
209
+ }
210
+
211
+ /**
212
+ * Enable editor events when returning to normal view
213
+ */
214
+ enableEditorEvents() {
215
+ const editorArea = this.editor.editor;
216
+ if (!editorArea) return;
217
+
218
+ // Restore editor functionality
219
+ editorArea.contentEditable = true;
220
+ editorArea.style.opacity = '';
221
+ editorArea.style.pointerEvents = '';
222
+ editorArea.style.cursor = '';
223
+ editorArea.title = '';
224
+ }
225
+
226
+ /**
227
+ * Disable other modules when in code view
228
+ */
229
+ disableOtherModules() {
230
+ const modulesToDisable = ['history', 'block-toolbar', 'table-toolbar', 'resize-handles'];
231
+
232
+ modulesToDisable.forEach(moduleName => {
233
+ const module = this.editor.getModule(moduleName);
234
+ if (module) {
235
+ // Try to disable module if it has disable method
236
+ if (typeof module.disable === 'function') {
237
+ module.disable();
238
+ this.disabledModules.add(moduleName);
239
+ }
240
+ // For modules without disable method, we can hide their UI elements
241
+ else if (module.getContainer && typeof module.getContainer === 'function') {
242
+ const container = module.getContainer();
243
+ if (container) {
244
+ container.style.display = 'none';
245
+ this.disabledModules.add(moduleName);
246
+ }
247
+ }
248
+ }
249
+ });
250
+ }
251
+
252
+ /**
253
+ * Enable other modules when returning to normal view
254
+ */
255
+ enableOtherModules() {
256
+ this.disabledModules.forEach(moduleName => {
257
+ const module = this.editor.getModule(moduleName);
258
+ if (module) {
259
+ // Try to enable module if it has enable method
260
+ if (typeof module.enable === 'function') {
261
+ module.enable();
262
+ }
263
+ // For modules without enable method, show their UI elements
264
+ else if (module.getContainer && typeof module.getContainer === 'function') {
265
+ const container = module.getContainer();
266
+ if (container) {
267
+ container.style.display = '';
268
+ }
269
+ }
270
+ }
271
+ });
272
+
273
+ this.disabledModules.clear();
274
+ }
275
+
276
+ /**
277
+ * Update original content when user types in textarea
278
+ */
279
+ updateOriginalContent() {
280
+ if (this.codeTextarea) {
281
+ this.originalContent = this.codeTextarea.value;
282
+
283
+ // Trigger content change event to call onChange callback
284
+ // Get the HTML content from textarea
285
+ const content = this.codeTextarea.value;
286
+
287
+ // Call onChange callback if provided
288
+ if (this.editor.options.onChange && typeof this.editor.options.onChange === 'function') {
289
+ this.editor.options.onChange(content);
290
+ }
291
+
292
+ // Emit text-change event
293
+ this.editor.emit('text-change', content);
294
+
295
+ // Keep the word/char counter in sync with the edited source
296
+ if (typeof this.editor.updateStatusbar === 'function') {
297
+ this.editor.updateStatusbar();
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Format HTML for better readability
304
+ */
305
+ formatHTML(html) {
306
+ let formatted = html;
307
+
308
+ // Tách thẻ mở và đóng thành dòng riêng biệt
309
+ formatted = formatted.replace(/></g, '>\n<');
310
+
311
+ // Tách nội dung giữa thẻ mở và đóng thành dòng riêng
312
+ formatted = formatted.replace(/>([^<>\s][^<]*)</g, '>\n$1\n<');
313
+
314
+ const lines = formatted.split('\n');
315
+ const indentSize = 4;
316
+ const formattedLines = [];
317
+ const tagStack = []; // Stack để theo dõi thẻ mở
318
+
319
+ for (let line of lines) {
320
+ const trimmed = line.trim();
321
+ if (!trimmed) continue;
322
+
323
+ // Check for closing tag - xử lý trước khi in
324
+ if (/^<\/(\w+)/.test(trimmed)) {
325
+ const closeTagMatch = trimmed.match(/^<\/(\w+)/);
326
+ if (closeTagMatch) {
327
+ const tagName = closeTagMatch[1];
328
+ // Tìm và loại bỏ thẻ mở tương ứng từ stack
329
+ for (let i = tagStack.length - 1; i >= 0; i--) {
330
+ if (tagStack[i] === tagName) {
331
+ tagStack.splice(i, 1);
332
+ break;
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // Apply indentation based on current stack level
339
+ let currentLevel = tagStack.length;
340
+
341
+ // Nếu là nội dung text (không phải thẻ), nó nằm bên trong thẻ cha nên cần thụt lề thêm
342
+ if (!trimmed.startsWith('<')) {
343
+ currentLevel = tagStack.length;
344
+ }
345
+
346
+ formattedLines.push(' '.repeat(currentLevel * indentSize) + trimmed);
347
+
348
+ // Check for opening tag (not self-closing) - xử lý sau khi in
349
+ const openTagMatch = trimmed.match(/^<(\w+)/);
350
+ if (
351
+ openTagMatch &&
352
+ !trimmed.startsWith('</') &&
353
+ !trimmed.endsWith('/>') &&
354
+ !['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr'].includes(openTagMatch[1].toLowerCase())
355
+ ) {
356
+ const tagName = openTagMatch[1];
357
+ tagStack.push(tagName);
358
+ }
359
+ }
360
+
361
+ return formattedLines.join('\n');
362
+ }
363
+
364
+ /**
365
+ * Update toolbar button state
366
+ */
367
+ updateToolbarButton() {
368
+
369
+ const toolbar = this.editor.getModule('toolbar');
370
+ if (toolbar) {
371
+ toolbar.setButtonActive('code-view', this.isCodeView);
372
+
373
+ // Update button title
374
+ const buttonTitle = this.isCodeView ? 'Switch to Visual Editor' : 'Switch to HTML Editor';
375
+ toolbar.setButtonTitle('code-view', buttonTitle);
376
+
377
+ } else {
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Check if currently in code view
383
+ */
384
+ isInCodeView() {
385
+ return this.isCodeView;
386
+ }
387
+
388
+ /**
389
+ * Get current content (from textarea if in code view, otherwise from editor)
390
+ */
391
+ getCurrentContent() {
392
+ if (this.isCodeView && this.codeTextarea) {
393
+ return this.codeTextarea.value;
394
+ }
395
+ return this.editor.editor.innerHTML;
396
+ }
397
+
398
+ /**
399
+ * Set content programmatically
400
+ */
401
+ setContent(html) {
402
+ if (this.isCodeView && this.codeTextarea) {
403
+ this.codeTextarea.value = this.formatHTML(html);
404
+ this.updateOriginalContent();
405
+ } else {
406
+ this.editor.editor.innerHTML = html;
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Hide all popups when entering code view
412
+ */
413
+ hideAllPopups() {
414
+ // Remove all popup elements from the DOM
415
+ const popups = document.querySelectorAll('.rich-editor-popup, .color-picker-popup, .emoji-picker-popup, .link-popup, .image-popup, .video-popup, .table-popup, .tag-popup, .import-popup');
416
+ popups.forEach(popup => {
417
+ if (popup.parentNode) {
418
+ popup.parentNode.removeChild(popup);
419
+ }
420
+ });
421
+
422
+ // Clear any popup instances from the editor
423
+ if (this.editor.popupInstances) {
424
+ this.editor.popupInstances.clear();
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Clean up when module is destroyed
430
+ */
431
+ destroy() {
432
+ if (this.isCodeView) {
433
+ this.showNormalView();
434
+ }
435
+
436
+ if (this.codeTextarea && this.codeTextarea.parentNode) {
437
+ this.codeTextarea.parentNode.removeChild(this.codeTextarea);
438
+ }
439
+
440
+ this.codeTextarea = null;
441
+ this.originalContent = '';
442
+ this.isCodeView = false;
443
+ this.disabledModules.clear();
444
+ }
445
+ }
446
+
447
+ export default CodeView;