@oix1987/yjd 1.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +223 -142
  3. package/core.js +82 -0
  4. package/dist/core.esm.js +2 -0
  5. package/dist/core.esm.js.map +1 -0
  6. package/dist/rich-editor.esm.js +1 -1
  7. package/dist/rich-editor.esm.js.map +1 -1
  8. package/dist/rich-editor.min.js +1 -1
  9. package/dist/rich-editor.min.js.map +1 -1
  10. package/index.d.ts +230 -103
  11. package/index.js +297 -0
  12. package/lib/core/editor.js +1885 -0
  13. package/lib/core/format.js +540 -0
  14. package/lib/core/module.js +81 -0
  15. package/lib/core/registry.js +158 -0
  16. package/lib/formats/background.js +213 -0
  17. package/lib/formats/bold.js +49 -0
  18. package/lib/formats/capitalization.js +579 -0
  19. package/lib/formats/color.js +183 -0
  20. package/lib/formats/emoji.js +282 -0
  21. package/lib/formats/font-family.js +548 -0
  22. package/lib/formats/heading.js +502 -0
  23. package/lib/formats/image.js +341 -0
  24. package/lib/formats/import.js +385 -0
  25. package/lib/formats/indent.js +297 -0
  26. package/lib/formats/italic.js +27 -0
  27. package/lib/formats/line-height.js +562 -0
  28. package/lib/formats/link.js +251 -0
  29. package/lib/formats/list.js +635 -0
  30. package/lib/formats/strike.js +31 -0
  31. package/lib/formats/subscript.js +40 -0
  32. package/lib/formats/superscript.js +39 -0
  33. package/lib/formats/table.js +293 -0
  34. package/lib/formats/tag.js +304 -0
  35. package/lib/formats/text-align.js +422 -0
  36. package/lib/formats/text-size.js +498 -0
  37. package/lib/formats/underline.js +30 -0
  38. package/lib/formats/video.js +381 -0
  39. package/lib/modules/block-toolbar.js +639 -0
  40. package/lib/modules/code-view.js +447 -0
  41. package/lib/modules/find-replace.js +273 -0
  42. package/lib/modules/history.js +425 -0
  43. package/lib/modules/mention.js +200 -0
  44. package/lib/modules/resize-handles.js +701 -0
  45. package/lib/modules/slash-menu.js +183 -0
  46. package/lib/modules/table-toolbar.js +635 -0
  47. package/lib/modules/toolbar.js +607 -0
  48. package/lib/serialize.js +241 -0
  49. package/lib/static.js +28 -0
  50. package/lib/styles-loader.js +142 -0
  51. package/{dist → lib}/styles.css +1392 -35
  52. package/lib/styles.css.js +2 -0
  53. package/lib/styles.min.css +1 -0
  54. package/lib/ui/color-picker.js +296 -0
  55. package/lib/ui/customselect.js +351 -0
  56. package/lib/ui/emoji-picker.js +196 -0
  57. package/lib/ui/icons.js +145 -0
  58. package/lib/ui/image-popup.js +435 -0
  59. package/lib/ui/import-popup.js +288 -0
  60. package/lib/ui/link-popup.js +139 -0
  61. package/lib/ui/list-picker.js +307 -0
  62. package/lib/ui/select-button.js +68 -0
  63. package/lib/ui/table-popup.js +171 -0
  64. package/lib/ui/tag-popup.js +249 -0
  65. package/lib/ui/text-align-picker.js +278 -0
  66. package/lib/ui/video-popup.js +413 -0
  67. package/lib/utils/exec-command.js +72 -0
  68. package/lib/utils/history-helper.js +50 -0
  69. package/lib/utils/popup-helper.js +219 -0
  70. package/lib/utils/popup-positioning.js +234 -0
  71. package/lib/utils/sanitize.js +164 -0
  72. package/package.json +51 -32
  73. package/umd-entry.js +19 -0
@@ -0,0 +1,548 @@
1
+ import { InlineFormat } from '../core/format.js';
2
+ import CustomSelect from '../ui/customselect.js';
3
+ import { saveBeforeFormat } from '../utils/history-helper.js';
4
+ import Editor from '../core/editor.js';
5
+ import { execFormat } from '../utils/exec-command.js';
6
+
7
+ /**
8
+ * Font Family Format - Handles font family formatting
9
+ * Now supports multiple editor instances with separate popup instances
10
+ */
11
+ class FontFamily extends InlineFormat {
12
+ static formatName = 'fontFamily';
13
+ static tagName = 'SPAN';
14
+
15
+ constructor() {
16
+ super();
17
+
18
+ // Get current editor instance
19
+ const currentEditor = Editor.getCurrentInstance();
20
+ if (!currentEditor) {
21
+ console.warn('No editor instance found for FontFamily format');
22
+ return;
23
+ }
24
+
25
+ this.editorId = currentEditor.instanceId;
26
+
27
+ // Check if this editor already has a font family select instance
28
+ let customSelect = currentEditor.getPopupInstance('font-family');
29
+
30
+ if (!customSelect) {
31
+ // Create new custom select instance for this editor
32
+ const fontMap = FontFamily.getFontMap();
33
+ const items = Object.values(fontMap).map(fontData => ({
34
+ value: fontData.font,
35
+ label: fontData.element,
36
+ title: fontData.title
37
+ }));
38
+
39
+ customSelect = new CustomSelect({
40
+ items: items,
41
+ displayProperty: 'label',
42
+ valueProperty: 'value',
43
+ className: 'font-family-select',
44
+ onItemSelect: (value, item) => {
45
+ FontFamily.applyFontFamilyToCurrentSelection(value, this.editorId);
46
+ },
47
+ editor: currentEditor,
48
+ editorId: this.editorId
49
+ });
50
+
51
+ // Store popup instance in editor
52
+ currentEditor.setPopupInstance('font-family', customSelect);
53
+ }
54
+
55
+ this.customSelect = customSelect;
56
+
57
+ // Set up event listener for selection changes
58
+ this.setupSelectionListener();
59
+ }
60
+
61
+ /**
62
+ * Create a new FontFamily format instance for a specific editor
63
+ * @param {string} editorId - Editor instance ID
64
+ * @returns {FontFamily} FontFamily format instance
65
+ */
66
+ static createForEditor(editorId) {
67
+ const editor = Editor.getInstanceById(editorId);
68
+ if (!editor) {
69
+ console.warn('No editor instance found for ID:', editorId);
70
+ return null;
71
+ }
72
+
73
+ // Temporarily set as current instance
74
+ const originalCurrent = Editor.currentInstance;
75
+ Editor.currentInstance = editor;
76
+
77
+ // Create format instance
78
+ const format = new FontFamily();
79
+
80
+ // Restore original current instance
81
+ Editor.currentInstance = originalCurrent;
82
+
83
+ return format;
84
+ }
85
+
86
+ /**
87
+ * Set up event listener for selection changes to update button text
88
+ */
89
+ setupSelectionListener() {
90
+ // Use a debounced function to avoid too many updates
91
+ let updateTimeout;
92
+ const debouncedUpdate = () => {
93
+ clearTimeout(updateTimeout);
94
+ updateTimeout = setTimeout(() => {
95
+ // Only update if selection is in this editor
96
+ const selection = window.getSelection();
97
+ if (selection && selection.rangeCount > 0) {
98
+ const range = selection.getRangeAt(0);
99
+ const editor = Editor.getInstanceById(this.editorId);
100
+ if (editor && (editor.editor.contains(range.startContainer) || editor.editor.isSameNode(range.startContainer))) {
101
+ this.updateButtonText();
102
+ }
103
+ }
104
+ }, 50); // 50ms delay
105
+ };
106
+
107
+ // Listen for selection changes
108
+ document.addEventListener('selectionchange', debouncedUpdate);
109
+
110
+ // Also listen for mouseup and keyup events for immediate feedback
111
+ document.addEventListener('mouseup', debouncedUpdate);
112
+ document.addEventListener('keyup', debouncedUpdate);
113
+
114
+ // Store the listener for cleanup
115
+ this.selectionListener = debouncedUpdate;
116
+ }
117
+
118
+ /**
119
+ * Get font map with different font families
120
+ */
121
+ static getFontMap() {
122
+ return {
123
+ 'Arial': {
124
+ font: 'Arial, sans-serif',
125
+ element: '<span style="font-family: Arial, sans-serif">Arial</span>',
126
+ title: 'Arial'
127
+ },
128
+ 'Helvetica': {
129
+ font: 'Helvetica, Arial, sans-serif',
130
+ element: '<span style="font-family: Helvetica, Arial, sans-serif">Helvetica</span>',
131
+ title: 'Helvetica'
132
+ },
133
+ 'Times New Roman': {
134
+ font: '"Times New Roman", Times, serif',
135
+ element: '<span style="font-family: \'Times New Roman\', Times, serif">Times New Roman</span>',
136
+ title: 'Times New Roman'
137
+ },
138
+ 'Georgia': {
139
+ font: 'Georgia, serif',
140
+ element: '<span style="font-family: Georgia, serif">Georgia</span>',
141
+ title: 'Georgia'
142
+ },
143
+ 'Verdana': {
144
+ font: 'Verdana, Geneva, sans-serif',
145
+ element: '<span style="font-family: Verdana, Geneva, sans-serif">Verdana</span>',
146
+ title: 'Verdana'
147
+ },
148
+ 'Courier New': {
149
+ font: '"Courier New", Courier, monospace',
150
+ element: '<span style="font-family: \'Courier New\', Courier, monospace">Courier New</span>',
151
+ title: 'Courier New'
152
+ },
153
+ 'Trebuchet MS': {
154
+ font: '"Trebuchet MS", Helvetica, sans-serif',
155
+ element: '<span style="font-family: \'Trebuchet MS\', Helvetica, sans-serif">Trebuchet MS</span>',
156
+ title: 'Trebuchet MS'
157
+ },
158
+ 'Comic Sans MS': {
159
+ font: '"Comic Sans MS", cursive',
160
+ element: '<span style="font-family: \'Comic Sans MS\', cursive">Comic Sans MS</span>',
161
+ title: 'Comic Sans MS'
162
+ },
163
+ 'Impact': {
164
+ font: 'Impact, Charcoal, sans-serif',
165
+ element: '<span style="font-family: Impact, Charcoal, sans-serif">Impact</span>',
166
+ title: 'Impact'
167
+ },
168
+ 'Lucida Console': {
169
+ font: '"Lucida Console", Monaco, monospace',
170
+ element: '<span style="font-family: \'Lucida Console\', Monaco, monospace">Lucida Console</span>',
171
+ title: 'Lucida Console'
172
+ }
173
+ };
174
+ }
175
+
176
+
177
+ /**
178
+ * Get display name for font
179
+ * @param {string} font - Font family value
180
+ * @returns {string} Display name
181
+ */
182
+ static getFontDisplayName(font) {
183
+ const fontMap = this.getFontMap();
184
+ // Find by font value
185
+ for (const [key, value] of Object.entries(fontMap)) {
186
+ if (value.font === font || key === font) {
187
+ return value.title;
188
+ }
189
+ }
190
+ return 'Arial';
191
+ }
192
+
193
+ /**
194
+ * Update custom button text based on current font
195
+ */
196
+ updateButtonText() {
197
+ const currentFont = this.getCurrentFont();
198
+ const displayName = FontFamily.getFontDisplayName(currentFont || 'Arial, sans-serif');
199
+
200
+ // Find font-family button in the specific editor's toolbar using editorId
201
+ const editor = Editor.getInstanceById(this.editorId);
202
+ if (!editor) return;
203
+
204
+ const toolbar = editor.getModule('toolbar');
205
+ let fontFamilyButton = null;
206
+
207
+ if (toolbar) {
208
+ fontFamilyButton = toolbar.getButton('font-family');
209
+ }
210
+
211
+ // Fallback: find button by class in the specific editor's toolbar
212
+ if (!fontFamilyButton) {
213
+ const toolbarContainer = toolbar?.getContainer();
214
+ if (toolbarContainer) {
215
+ fontFamilyButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.font-family-btn');
216
+ }
217
+ }
218
+
219
+ // Final fallback: find any font-family button in the specific editor's wrapper
220
+ if (!fontFamilyButton) {
221
+ fontFamilyButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.font-family-btn');
222
+ }
223
+
224
+ if (fontFamilyButton && fontFamilyButton.updateText) {
225
+ fontFamilyButton.updateText(displayName);
226
+ } else if (fontFamilyButton) {
227
+ fontFamilyButton.textContent = displayName;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Create element with specific font family
233
+ * @param {string} font - Font family value
234
+ * @returns {HTMLElement}
235
+ */
236
+ static create(font = 'Arial, sans-serif') {
237
+ const node = document.createElement('span');
238
+ node.style.fontFamily = font;
239
+ return node;
240
+ }
241
+
242
+ /**
243
+ * Static method to apply font family to current selection
244
+ * @param {string} font - Font family value
245
+ * @param {string} editorId - Editor instance ID
246
+ */
247
+ static applyFontFamilyToCurrentSelection(font, editorId = null) {
248
+ // Get the correct editor instance
249
+ let editor = null;
250
+ if (editorId) {
251
+ editor = Editor.getInstanceById(editorId);
252
+ } else {
253
+ editor = Editor.getCurrentInstance();
254
+ }
255
+
256
+ if (!editor) {
257
+ console.warn('No editor instance found for font family application');
258
+ return;
259
+ }
260
+
261
+ const selection = window.getSelection();
262
+ if (!selection || !selection.rangeCount) return;
263
+
264
+ // Save state before applying format
265
+ saveBeforeFormat();
266
+
267
+ const range = selection.getRangeAt(0);
268
+ const fontFamilyFormat = FontFamily.createForEditor(editorId);
269
+ if (fontFamilyFormat) {
270
+ fontFamilyFormat.apply(font);
271
+
272
+ // Update button text after applying
273
+ fontFamilyFormat.updateButtonText();
274
+ }
275
+
276
+ // Trigger content change after applying format
277
+ setTimeout(() => {
278
+ if (editor && typeof editor.onContentChange === 'function') {
279
+ editor.onContentChange();
280
+ }
281
+ }, 0);
282
+ }
283
+
284
+ /**
285
+ * Apply font family format with specified font
286
+ * @param {string} font - Font family value
287
+ */
288
+ apply(font = 'Arial, sans-serif') {
289
+ const selection = window.getSelection();
290
+ if (!selection || !selection.rangeCount) return;
291
+ const range = selection.getRangeAt(0);
292
+
293
+ function isCaretInsideFontSpan(selection, font) {
294
+ if (!selection.rangeCount) return false;
295
+ const range = selection.getRangeAt(0);
296
+ let node = range.startContainer;
297
+
298
+ if (node.nodeType === Node.TEXT_NODE) {
299
+ node = node.parentNode;
300
+ }
301
+
302
+ const fontNormalized = font.split(',')[0].trim().toLowerCase();
303
+
304
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
305
+ if (node.tagName === 'SPAN') {
306
+ const styleFont = node.style.fontFamily;
307
+ if (styleFont) {
308
+ const styleFontNormalized = styleFont.split(',')[0].trim().toLowerCase();
309
+ if (styleFontNormalized === fontNormalized) {
310
+ if (
311
+ node.childNodes.length === 1 &&
312
+ node.firstChild.nodeType === Node.TEXT_NODE &&
313
+ node.firstChild.textContent === '\u200B'
314
+ ) {
315
+ return true; // Đang trong span marker rồi
316
+ }
317
+ return true; // Đang trong span font-family đó
318
+ }
319
+ }
320
+ }
321
+ node = node.parentNode;
322
+ }
323
+ return false;
324
+ }
325
+
326
+ // Hàm đặt caret vào bên trong span mới
327
+ function moveCaretInside(el) {
328
+ const sel = window.getSelection();
329
+ const range = document.createRange();
330
+ const textNode = el.firstChild;
331
+ range.setStart(textNode, textNode.length);
332
+ range.collapse(true);
333
+ sel.removeAllRanges();
334
+ sel.addRange(range);
335
+ }
336
+
337
+ if (range.collapsed) {
338
+ if (isCaretInsideFontSpan(selection, font)) {
339
+ // Đã ở trong span font rồi, không cần làm gì thêm
340
+ return;
341
+ }
342
+
343
+ let node = range.startContainer;
344
+ let offset = range.startOffset;
345
+
346
+ if (node.nodeType === Node.TEXT_NODE) {
347
+ node = node.parentNode;
348
+ }
349
+
350
+ const currentSpan = node.closest && node.closest('span');
351
+
352
+ // Trường hợp 1: caret trong span rỗng chứa \u200B
353
+ if (currentSpan && currentSpan.textContent === "\u200B") {
354
+ currentSpan.style.fontFamily = font;
355
+ return;
356
+ }
357
+
358
+ // Trường hợp 2: caret trong span có text thật
359
+ if (currentSpan && currentSpan.firstChild && currentSpan.firstChild.nodeType === Node.TEXT_NODE) {
360
+ const textNode = currentSpan.firstChild;
361
+ const caretPos = range.startOffset;
362
+
363
+ const textBefore = textNode.data.slice(0, caretPos);
364
+ const textAfter = textNode.data.slice(caretPos);
365
+
366
+ const parent = currentSpan.parentNode;
367
+
368
+ if (caretPos === 0) {
369
+ // Chèn span mới trước currentSpan
370
+ const newSpan = document.createElement('span');
371
+ newSpan.style.fontFamily = font;
372
+ newSpan.appendChild(document.createTextNode('\u200B'));
373
+ parent.insertBefore(newSpan, currentSpan);
374
+ moveCaretInside(newSpan);
375
+ } else if (caretPos === textNode.data.length) {
376
+ // Chèn span mới sau currentSpan
377
+ const newSpan = document.createElement('span');
378
+ newSpan.style.fontFamily = font;
379
+ newSpan.appendChild(document.createTextNode('\u200B'));
380
+ parent.insertBefore(newSpan, currentSpan.nextSibling);
381
+ moveCaretInside(newSpan);
382
+ } else {
383
+ // Tách thành 3 span
384
+ const span1 = document.createElement('span');
385
+ span1.style.fontFamily = currentSpan.style.fontFamily;
386
+ span1.appendChild(document.createTextNode(textBefore));
387
+
388
+ const span2 = document.createElement('span');
389
+ span2.style.fontFamily = font;
390
+ span2.appendChild(document.createTextNode('\u200B'));
391
+
392
+ const span3 = document.createElement('span');
393
+ span3.style.fontFamily = currentSpan.style.fontFamily;
394
+ span3.appendChild(document.createTextNode(textAfter));
395
+
396
+ parent.insertBefore(span1, currentSpan);
397
+ parent.insertBefore(span2, currentSpan);
398
+ parent.insertBefore(span3, currentSpan);
399
+ parent.removeChild(currentSpan);
400
+
401
+ moveCaretInside(span2);
402
+ }
403
+ return;
404
+ }
405
+
406
+ // Trường hợp 3: không ở trong span nào → tạo mới
407
+ const newSpan = document.createElement('span');
408
+ newSpan.style.fontFamily = font;
409
+ newSpan.appendChild(document.createTextNode('\u200B'));
410
+ range.insertNode(newSpan);
411
+ moveCaretInside(newSpan);
412
+
413
+ } else {
414
+ // Có selection → áp dụng fontName
415
+ execFormat('fontName', font);
416
+ }
417
+ }
418
+
419
+
420
+ /**
421
+ * Toggle font family format - shows/hides font picker
422
+ */
423
+ async toggle(anchorButton = null) {
424
+ if (this.customSelect.isVisible) {
425
+ this.customSelect.hide();
426
+ } else {
427
+ await this.showFontPicker(anchorButton);
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Show custom select positioned relative to font family button on toolbar
433
+ */
434
+ async showFontPicker(anchorButton = null) {
435
+ // Use provided anchor button or find the default toolbar button
436
+ let fontFamilyButton = anchorButton;
437
+
438
+ if (!fontFamilyButton) {
439
+ // Find font-family button in the current editor's toolbar
440
+ const editor = Editor.getInstanceById(this.editorId);
441
+ if (!editor) return;
442
+
443
+ const toolbar = editor.getModule('toolbar');
444
+
445
+ if (toolbar) {
446
+ fontFamilyButton = toolbar.getButton('font-family');
447
+ }
448
+
449
+ // Fallback: find button by class in the current editor's toolbar
450
+ if (!fontFamilyButton) {
451
+ const toolbarContainer = toolbar?.getContainer();
452
+ if (toolbarContainer) {
453
+ fontFamilyButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.font-family-btn');
454
+ }
455
+ }
456
+
457
+ // Final fallback: find any font-family button in the current editor's wrapper
458
+ if (!fontFamilyButton) {
459
+ fontFamilyButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.font-family-btn');
460
+ }
461
+ }
462
+
463
+ if (!fontFamilyButton) return;
464
+
465
+ // Update current selection before showing
466
+ const currentFont = this.getCurrentFont();
467
+ if (currentFont) {
468
+ this.customSelect.setCurrentValue(currentFont);
469
+ }
470
+
471
+ await this.customSelect.show(fontFamilyButton);
472
+ }
473
+
474
+ /**
475
+ * Check if font family format is active - always return false (no active state)
476
+ * Only update button text to show current font
477
+ * @param {string} font - Optional specific font to check
478
+ * @returns {boolean}
479
+ */
480
+ isActive(font = null) {
481
+ // Always update button text to show current font
482
+ this.updateButtonText();
483
+
484
+ // Never show active state for font family button
485
+ return false;
486
+ }
487
+
488
+ /**
489
+ * Get current font family of the selection
490
+ * @returns {string|null} Current font family or null
491
+ */
492
+ getCurrentFont() {
493
+ const selection = window.getSelection();
494
+ if (!selection || !selection.rangeCount) return null;
495
+
496
+ const range = selection.getRangeAt(0);
497
+ let currentNode = range.startContainer;
498
+
499
+ // If text node, get parent element
500
+ if (currentNode.nodeType === Node.TEXT_NODE) {
501
+ currentNode = currentNode.parentElement;
502
+ }
503
+
504
+ // Get the specific editor instance
505
+ const editor = Editor.getInstanceById(this.editorId);
506
+ if (!editor) return 'Arial, sans-serif';
507
+
508
+ // Check if the selection is within this editor
509
+ if (!editor.editor.contains(currentNode) && !editor.editor.isSameNode(currentNode)) {
510
+ // Selection is not in this editor, return default
511
+ return 'Arial, sans-serif';
512
+ }
513
+
514
+ // Find element with font-family style
515
+ while (currentNode && currentNode !== document.body) {
516
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
517
+ const element = currentNode;
518
+
519
+ // Priority 1: Check if this element has explicit inline font-family
520
+ if (element.style.fontFamily) {
521
+ return element.style.fontFamily;
522
+ }
523
+
524
+ // Priority 2: Check computed font-family
525
+ const computedStyle = window.getComputedStyle(element);
526
+ const fontFamily = computedStyle.fontFamily;
527
+ if (fontFamily && fontFamily !== 'initial' && fontFamily !== 'inherit') {
528
+ return fontFamily;
529
+ }
530
+ }
531
+ currentNode = currentNode.parentElement;
532
+ }
533
+
534
+ // Default fallback
535
+ return 'Arial, sans-serif';
536
+ }
537
+
538
+ /**
539
+ * Set current font for future typing
540
+ * @param {string} font - Font family value
541
+ */
542
+ setCurrentFont(font) {
543
+ // Store for future typing operations
544
+ this.currentFont = font;
545
+ }
546
+ }
547
+
548
+ export default FontFamily;