@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,498 @@
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, queryFormatValue } from '../utils/exec-command.js';
6
+
7
+ /**
8
+ * Text Size Format - Handles font size formatting with 7 levels via execCommand
9
+ * Now supports multiple editor instances with separate popup instances
10
+ */
11
+ class TextSize extends InlineFormat {
12
+ static formatName = 'textSize';
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 TextSize format');
22
+ return;
23
+ }
24
+
25
+ this.editorId = currentEditor.instanceId;
26
+
27
+ // Check if this editor already has a text size select instance
28
+ let customSelect = currentEditor.getPopupInstance('text-size');
29
+
30
+ if (!customSelect) {
31
+ // Create new custom select instance for this editor
32
+ const sizeMap = TextSize.getSizeMap();
33
+ const items = Object.values(sizeMap).map(sizeData => ({
34
+ value: sizeData.size,
35
+ label: sizeData.element,
36
+ title: sizeData.title
37
+ }));
38
+
39
+ customSelect = new CustomSelect({
40
+ items: items,
41
+ displayProperty: 'label',
42
+ valueProperty: 'value',
43
+ className: 'text-size-select',
44
+ onItemSelect: (value) => {
45
+ TextSize.applyTextSizeToCurrentSelection(value, this.editorId);
46
+ },
47
+ editor: currentEditor,
48
+ editorId: this.editorId
49
+ });
50
+
51
+ // Store popup instance in editor
52
+ currentEditor.setPopupInstance('text-size', customSelect);
53
+ }
54
+
55
+ this.customSelect = customSelect;
56
+ }
57
+
58
+ /**
59
+ * Create a new TextSize format instance for a specific editor
60
+ * @param {string} editorId - Editor instance ID
61
+ * @returns {TextSize} TextSize format instance
62
+ */
63
+ static createForEditor(editorId) {
64
+ const editor = Editor.getInstanceById(editorId);
65
+ if (!editor) {
66
+ console.warn('No editor instance found for ID:', editorId);
67
+ return null;
68
+ }
69
+
70
+ // Temporarily set as current instance
71
+ const originalCurrent = Editor.currentInstance;
72
+ Editor.currentInstance = editor;
73
+
74
+ // Create format instance
75
+ const format = new TextSize();
76
+
77
+ // Restore original current instance
78
+ Editor.currentInstance = originalCurrent;
79
+
80
+ return format;
81
+ }
82
+
83
+ /**
84
+ * 7-level text size map aligned with execCommand('fontSize', 1..7)
85
+ */
86
+ static getSizeMap() {
87
+ return {
88
+ '1': { size: '1', element: '<span >XX-Small</span>', title: 'XX-Small' },
89
+ '2': { size: '2', element: '<span >X-Small</span>', title: 'X-Small' },
90
+ '3': { size: '3', element: '<span >Small</span>', title: 'Small' },
91
+ '4': { size: '4', element: '<span >Medium</span>', title: 'Medium' },
92
+ '5': { size: '5', element: '<span >Large</span>', title: 'Large' },
93
+ '6': { size: '6', element: '<span >X-Large</span>', title: 'X-Large' },
94
+ '7': { size: '7', element: '<span >XX-Large</span>', title: 'XX-Large' },
95
+ };
96
+ }
97
+
98
+ static getSizeDisplayName(size) {
99
+ const sizeMap = this.getSizeMap();
100
+ return sizeMap[size]?.title || 'Medium';
101
+ }
102
+
103
+ /**
104
+ * Update button text based on current text size
105
+ */
106
+ updateButtonText() {
107
+ const currentSize = this.getCurrentSize();
108
+ const displayName = TextSize.getSizeDisplayName(currentSize || '4');
109
+
110
+ // Find text-size button in the current editor's toolbar
111
+ const editor = Editor.getInstanceById(this.editorId);
112
+ if (!editor) return;
113
+
114
+ const toolbar = editor.getModule('toolbar');
115
+ let textSizeButton = null;
116
+
117
+ if (toolbar) {
118
+ textSizeButton = toolbar.getButton('text-size');
119
+ }
120
+
121
+ // Fallback: find button by class in the current editor's toolbar
122
+ if (!textSizeButton) {
123
+ const toolbarContainer = toolbar?.getContainer();
124
+ if (toolbarContainer) {
125
+ textSizeButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-size-btn');
126
+ }
127
+ }
128
+
129
+ // Final fallback: find any text-size button in the current editor's wrapper
130
+ if (!textSizeButton) {
131
+ textSizeButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-size-btn');
132
+ }
133
+
134
+ if (textSizeButton && textSizeButton.updateText) {
135
+ textSizeButton.updateText(displayName);
136
+ } else if (textSizeButton) {
137
+ textSizeButton.textContent = displayName;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Static method to update button text for any editor
143
+ * @param {string} editorId - Editor instance ID
144
+ */
145
+ static updateButtonTextStatic(editorId = null) {
146
+ // Get the correct editor instance
147
+ let editor = null;
148
+ if (editorId) {
149
+ editor = Editor.getInstanceById(editorId);
150
+ } else {
151
+ editor = Editor.getCurrentInstance();
152
+ }
153
+
154
+ if (!editor) return;
155
+
156
+ const currentSize = TextSize.getCurrentSizeStatic();
157
+ const displayName = TextSize.getSizeDisplayName(currentSize || '4');
158
+
159
+ const toolbar = editor.getModule('toolbar');
160
+ let textSizeButton = null;
161
+
162
+ if (toolbar) {
163
+ textSizeButton = toolbar.getButton('text-size');
164
+ }
165
+
166
+ // Fallback: find button by class in the current editor's toolbar
167
+ if (!textSizeButton) {
168
+ const toolbarContainer = toolbar?.getContainer();
169
+ if (toolbarContainer) {
170
+ textSizeButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-size-btn');
171
+ }
172
+ }
173
+
174
+ // Final fallback: find any text-size button in the current editor's wrapper
175
+ if (!textSizeButton) {
176
+ textSizeButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-size-btn');
177
+ }
178
+
179
+ if (textSizeButton && textSizeButton.updateText) {
180
+ textSizeButton.updateText(displayName);
181
+ } else if (textSizeButton) {
182
+ textSizeButton.textContent = displayName;
183
+ }
184
+ }
185
+
186
+ static create(size = '4') {
187
+ const node = document.createElement('span');
188
+ // Fallback creation with an approximate CSS size
189
+ node.style.fontSize = TextSize.sizeToCss(size);
190
+ return node;
191
+ }
192
+
193
+ /**
194
+ * Apply text size to current selection
195
+ * @param {string} size - Text size value
196
+ * @param {string} editorId - Editor instance ID
197
+ */
198
+ static applyTextSizeToCurrentSelection(size, editorId = null) {
199
+ // Get the correct editor instance
200
+ let editor = null;
201
+ if (editorId) {
202
+ editor = Editor.getInstanceById(editorId);
203
+ } else {
204
+ editor = Editor.getCurrentInstance();
205
+ }
206
+
207
+ if (!editor) {
208
+ console.warn('No editor instance found for text size application');
209
+ return;
210
+ }
211
+
212
+ const selection = window.getSelection();
213
+ if (!selection || !selection.rangeCount) return;
214
+
215
+ // Save state before applying format
216
+ saveBeforeFormat();
217
+
218
+ const sizeFormat = TextSize.createForEditor(editorId);
219
+ if (sizeFormat) {
220
+ sizeFormat.apply(size);
221
+ sizeFormat.updateButtonText();
222
+ }
223
+
224
+ // Trigger content change after applying format
225
+ setTimeout(() => {
226
+ if (editor && typeof editor.onContentChange === 'function') {
227
+ editor.onContentChange();
228
+ }
229
+ }, 0);
230
+ }
231
+
232
+ /**
233
+ * Map execCommand size (1..7) to CSS font-size for fallback/labels
234
+ */
235
+ static sizeToCss(size) {
236
+ const map = {
237
+ '1': '10px',
238
+ '2': '12px',
239
+ '3': '14px',
240
+ '4': '16px',
241
+ '5': '20px',
242
+ '6': '28px',
243
+ '7': '36px',
244
+ };
245
+ return map[String(size)] || '16px';
246
+ }
247
+
248
+ /**
249
+ * Apply text size using execCommand; works with selection and collapsed caret
250
+ */
251
+ apply(size = '4') {
252
+ const selection = window.getSelection();
253
+ if (!selection || !selection.rangeCount) return;
254
+
255
+ saveBeforeFormat();
256
+
257
+ const range = selection.getRangeAt(0);
258
+
259
+ if (!range.collapsed) {
260
+ // Bạn chưa nói đến xử lý khi bôi đen, nên mình bỏ qua
261
+ execFormat('fontSize', String(size));
262
+
263
+ // Lấy node bao quanh selection hiện tại
264
+ const sel = window.getSelection();
265
+ if (sel.rangeCount > 0) {
266
+ const container = sel.getRangeAt(0).commonAncestorContainer;
267
+ // Nếu container là text node → normalize ở parent
268
+ if (container.nodeType === Node.TEXT_NODE) {
269
+ container.parentNode.normalize();
270
+ } else {
271
+ container.normalize();
272
+ }
273
+ }
274
+
275
+ return;
276
+ }
277
+
278
+ let node = range.startContainer;
279
+ let offset = range.startOffset;
280
+
281
+ // Nếu caret đang trong text node → lấy cha
282
+ if (node.nodeType === Node.TEXT_NODE) {
283
+ node = node.parentNode;
284
+ }
285
+
286
+ // Kiểm tra nếu đang ở trong một <font>
287
+ const currentFont = node.closest && node.closest('font');
288
+
289
+ // ========================
290
+ // Trường hợp 1: caret trong <font> rỗng (chỉ có \u200B)
291
+ // ========================
292
+ if (currentFont && currentFont.textContent === "\u200B") {
293
+ currentFont.setAttribute('size', String(size));
294
+ return;
295
+ }
296
+
297
+ // ========================
298
+ // Trường hợp 2: caret trong <font> có ký tự thực
299
+ // ========================
300
+ if (currentFont && currentFont.firstChild && currentFont.firstChild.nodeType === Node.TEXT_NODE) {
301
+ const textNode = currentFont.firstChild;
302
+ const caretPos = range.startOffset; // vị trí caret trong text node
303
+
304
+ // Loại bỏ ký tự ẩn trong tính toán
305
+
306
+ const textBefore = textNode.data.slice(0, caretPos);
307
+ const textAfter = textNode.data.slice(caretPos);
308
+
309
+ const parent = currentFont.parentNode;
310
+
311
+ if (caretPos === 0) {
312
+ // Đang ở ĐẦU thẻ font → chèn font mới trước
313
+ const newFont = document.createElement('font');
314
+ newFont.setAttribute('size', String(size));
315
+ newFont.appendChild(document.createTextNode("\u200B"));
316
+ parent.insertBefore(newFont, currentFont);
317
+
318
+ moveCaretInside(newFont);
319
+
320
+ } else if (caretPos === textNode.data.length) {
321
+ // Đang ở CUỐI thẻ font → chèn font mới sau
322
+ const newFont = document.createElement('font');
323
+ newFont.setAttribute('size', String(size));
324
+ newFont.appendChild(document.createTextNode("\u200B"));
325
+ parent.insertBefore(newFont, currentFont.nextSibling);
326
+
327
+ moveCaretInside(newFont);
328
+
329
+ } else {
330
+
331
+
332
+ const font1 = document.createElement('font');
333
+ font1.setAttribute('size', currentFont.getAttribute('size'));
334
+ font1.appendChild(document.createTextNode(textBefore));
335
+
336
+ const font2 = document.createElement('font');
337
+ font2.setAttribute('size', String(size));
338
+ font2.appendChild(document.createTextNode("\u200B"));
339
+
340
+ const font3 = document.createElement('font');
341
+ font3.setAttribute('size', currentFont.getAttribute('size'));
342
+ font3.appendChild(document.createTextNode(textAfter));
343
+
344
+ parent.insertBefore(font1, currentFont);
345
+ parent.insertBefore(font2, currentFont);
346
+ parent.insertBefore(font3, currentFont);
347
+
348
+ parent.removeChild(currentFont);
349
+
350
+ moveCaretInside(font2);
351
+ }
352
+ return;
353
+ }
354
+
355
+ // ========================
356
+ // Trường hợp 3: không ở trong <font> nào → tạo mới
357
+ // ========================
358
+ const newFont = document.createElement('font');
359
+ newFont.setAttribute('size', String(size));
360
+ const zwsp = document.createTextNode("\u200B");
361
+ newFont.appendChild(zwsp);
362
+
363
+ range.insertNode(newFont);
364
+ moveCaretInside(newFont);
365
+
366
+ // Hàm phụ để đưa caret vào sau ký tự ẩn
367
+ function moveCaretInside(fontEl) {
368
+ const sel = window.getSelection();
369
+ const range = document.createRange();
370
+ const textNode = fontEl.firstChild;
371
+ range.setStart(textNode, textNode.length);
372
+ range.collapse(true);
373
+ sel.removeAllRanges();
374
+ sel.addRange(range);
375
+ }
376
+
377
+
378
+ }
379
+
380
+ async toggle() {
381
+ if (this.customSelect.isVisible) {
382
+ this.customSelect.hide();
383
+ } else {
384
+ await this.showSizePicker();
385
+ }
386
+ }
387
+
388
+ async showSizePicker() {
389
+ // Find text-size button in the current editor's toolbar
390
+ const editor = Editor.getInstanceById(this.editorId);
391
+ if (!editor) return;
392
+
393
+ const toolbar = editor.getModule('toolbar');
394
+ let textSizeButton = null;
395
+
396
+ if (toolbar) {
397
+ textSizeButton = toolbar.getButton('text-size');
398
+ }
399
+
400
+ // Fallback: find button by class in the current editor's toolbar
401
+ if (!textSizeButton) {
402
+ const toolbarContainer = toolbar?.getContainer();
403
+ if (toolbarContainer) {
404
+ textSizeButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-size-btn');
405
+ }
406
+ }
407
+
408
+ // Final fallback: find any text-size button in the current editor's wrapper
409
+ if (!textSizeButton) {
410
+ textSizeButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-size-btn');
411
+ }
412
+
413
+ if (!textSizeButton) {
414
+ console.warn('Text-size button not found for editor:', this.editorId);
415
+ return;
416
+ }
417
+
418
+ const currentSize = this.getCurrentSize();
419
+ if (currentSize) {
420
+ this.customSelect.setCurrentValue(currentSize);
421
+ }
422
+
423
+ await this.customSelect.show(textSizeButton);
424
+ }
425
+
426
+ isActive(size = null) {
427
+ this.updateButtonText();
428
+ return false;
429
+ }
430
+
431
+ /**
432
+ * Get current text size near caret/selection, return one of '1'..'7'
433
+ */
434
+ getCurrentSize() {
435
+ return TextSize.getCurrentSizeStatic();
436
+ }
437
+
438
+ /**
439
+ * Static method to get current text size
440
+ * @returns {string} Current text size
441
+ */
442
+ static getCurrentSizeStatic() {
443
+ const selection = window.getSelection();
444
+ if (!selection || !selection.rangeCount) return '4';
445
+
446
+ try {
447
+ // Try to use queryCommandValue when available (returns 1..7 in many browsers)
448
+ const val = queryFormatValue('fontSize');
449
+ const num = parseInt(val, 10);
450
+ if (!isNaN(num) && num >= 1 && num <= 7) {
451
+ return String(num);
452
+ }
453
+ } catch (_) {}
454
+
455
+ const range = selection.getRangeAt(0);
456
+ let currentNode = range.startContainer;
457
+ if (currentNode.nodeType === Node.TEXT_NODE) {
458
+ currentNode = currentNode.parentElement;
459
+ }
460
+
461
+ while (currentNode && currentNode !== document.body) {
462
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
463
+ const element = currentNode;
464
+ const inline = element.style?.fontSize;
465
+ if (inline) return this.normalizeCssSizeToExecSize(inline);
466
+
467
+ const computed = window.getComputedStyle(element).fontSize;
468
+ if (computed) return this.normalizeCssSizeToExecSize(computed);
469
+ }
470
+ currentNode = currentNode.parentElement;
471
+ }
472
+
473
+ return '4';
474
+ }
475
+
476
+ /**
477
+ * Normalize CSS px value to closest execCommand size 1..7
478
+ */
479
+ normalizeCssSizeToExecSize(cssSize) {
480
+ const px = parseFloat(cssSize);
481
+ if (isNaN(px)) return '4';
482
+ const steps = [10, 12, 14, 16, 20, 28, 36];
483
+ let closestIndex = 3; // default to '4' (16px)
484
+ let minDiff = Infinity;
485
+ for (let i = 0; i < steps.length; i++) {
486
+ const diff = Math.abs(px - steps[i]);
487
+ if (diff < minDiff) {
488
+ minDiff = diff;
489
+ closestIndex = i;
490
+ }
491
+ }
492
+ return String(closestIndex + 1);
493
+ }
494
+ }
495
+
496
+ export default TextSize;
497
+
498
+
@@ -0,0 +1,30 @@
1
+ import { InlineFormat } from '../core/format.js';
2
+ import { saveBeforeFormat } from '../utils/history-helper.js';
3
+
4
+ /**
5
+ * Underline Format - Handles underline text formatting
6
+ * Extracted from FormatManager.js logic
7
+ */
8
+ class Underline extends InlineFormat {
9
+ static formatName = 'underline';
10
+ static tagName = 'U';
11
+ static alternativeTagNames = ['SPAN'];
12
+
13
+ /**
14
+ * Toggle underline formatting
15
+ */
16
+
17
+ toggle() {
18
+ // Save state before applying format
19
+ saveBeforeFormat();
20
+
21
+ if (this.isActive()) {
22
+ this.remove();
23
+ } else {
24
+ this.apply();
25
+ }
26
+ }
27
+
28
+ }
29
+
30
+ export default Underline;