@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,579 @@
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
+
6
+ /**
7
+ * Capitalization Format - Handles text capitalization
8
+ * Now supports multiple editor instances with separate popup instances
9
+ */
10
+ class Capitalization extends InlineFormat {
11
+ static formatName = 'capitalization';
12
+ static tagName = 'SPAN';
13
+
14
+ constructor() {
15
+ super();
16
+
17
+ // Get current editor instance
18
+ const currentEditor = Editor.getCurrentInstance();
19
+ if (!currentEditor) {
20
+ console.warn('No editor instance found for Capitalization format');
21
+ return;
22
+ }
23
+
24
+ this.editorId = currentEditor.instanceId;
25
+
26
+ // Check if this editor already has a capitalization select instance
27
+ let customSelect = currentEditor.getPopupInstance('capitalization');
28
+
29
+ if (!customSelect) {
30
+ // Create new custom select instance for this editor
31
+ const capMap = Capitalization.getCapitalizationMap();
32
+ const items = Object.values(capMap).map(capData => ({
33
+ value: capData.style,
34
+ label: capData.element,
35
+ title: capData.title
36
+ }));
37
+
38
+ customSelect = new CustomSelect({
39
+ items: items,
40
+ displayProperty: 'label',
41
+ valueProperty: 'value',
42
+ className: 'capitalization-select',
43
+ onItemSelect: (value, item) => {
44
+ Capitalization.applyCapitalizationToCurrentSelection(value, this.editorId);
45
+ },
46
+ editor: currentEditor,
47
+ editorId: this.editorId
48
+ });
49
+
50
+ // Store popup instance in editor
51
+ currentEditor.setPopupInstance('capitalization', customSelect);
52
+ }
53
+
54
+ this.customSelect = customSelect;
55
+ }
56
+
57
+ /**
58
+ * Create a new Capitalization format instance for a specific editor
59
+ * @param {string} editorId - Editor instance ID
60
+ * @returns {Capitalization} Capitalization format instance
61
+ */
62
+ static createForEditor(editorId) {
63
+ const editor = Editor.getInstanceById(editorId);
64
+ if (!editor) {
65
+ console.warn('No editor instance found for ID:', editorId);
66
+ return null;
67
+ }
68
+
69
+ // Temporarily set as current instance
70
+ const originalCurrent = Editor.currentInstance;
71
+ Editor.currentInstance = editor;
72
+
73
+ // Create format instance
74
+ const format = new Capitalization();
75
+
76
+ // Restore original current instance
77
+ Editor.currentInstance = originalCurrent;
78
+
79
+ return format;
80
+ }
81
+
82
+ /**
83
+ * Get capitalization map with different text transformations
84
+ */
85
+ static getCapitalizationMap() {
86
+ return {
87
+ 'capitalize': {
88
+ style: 'capitalize',
89
+ element: '<span>Capitalize</span>',
90
+ title: 'Capitalize'
91
+ },
92
+ 'uppercase': {
93
+ style: 'uppercase',
94
+ element: '<span>UPPERCASE</span>',
95
+ title: 'UPPERCASE'
96
+ },
97
+ 'lowercase': {
98
+ style: 'lowercase',
99
+ element: '<span>lowercase</span>',
100
+ title: 'lowercase'
101
+ },
102
+ 'small-caps': {
103
+ style: 'small-caps',
104
+ element: '<span>Small Caps</span>',
105
+ title: 'Small Caps'
106
+ }
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Get display name for capitalization
112
+ * @param {string} style - Text transform value
113
+ * @returns {string} Display name
114
+ */
115
+ static getCapitalizationDisplayName(style) {
116
+ const capMap = this.getCapitalizationMap();
117
+ return capMap[style]?.title || 'Capitalization';
118
+ }
119
+
120
+ /**
121
+ * Update custom button text based on current capitalization
122
+ */
123
+ updateButtonText() {
124
+ const currentCap = this.getCurrentCapitalization();
125
+ const displayName = Capitalization.getCapitalizationDisplayName(currentCap || 'none');
126
+
127
+ // Find capitalization button in the current editor's toolbar
128
+ const editor = Editor.getInstanceById(this.editorId);
129
+ if (!editor) return;
130
+
131
+ const toolbar = editor.getModule('toolbar');
132
+ let capitalizationButton = null;
133
+
134
+ if (toolbar) {
135
+ capitalizationButton = toolbar.getButton('capitalization');
136
+ }
137
+
138
+ // Fallback: find button by class in the current editor's toolbar
139
+ if (!capitalizationButton) {
140
+ const toolbarContainer = toolbar?.getContainer();
141
+ if (toolbarContainer) {
142
+ capitalizationButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.capitalization-btn');
143
+ }
144
+ }
145
+
146
+ // Final fallback: find any capitalization button in the current editor's wrapper
147
+ if (!capitalizationButton) {
148
+ capitalizationButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.capitalization-btn');
149
+ }
150
+
151
+ if (capitalizationButton && capitalizationButton.updateText) {
152
+ capitalizationButton.updateText(displayName);
153
+ } else if (capitalizationButton) {
154
+ capitalizationButton.textContent = displayName;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create element with specific text transformation
160
+ * @param {string} style - Text transform value
161
+ * @returns {HTMLElement}
162
+ */
163
+ static create(style = 'none') {
164
+ const node = document.createElement('span');
165
+ if (style === 'small-caps') {
166
+ node.style.fontVariant = 'small-caps';
167
+ } else {
168
+ node.style.textTransform = style;
169
+ }
170
+ return node;
171
+ }
172
+
173
+ /**
174
+ * Static method to apply capitalization to current selection
175
+ * @param {string} style - Text transform value
176
+ * @param {string} editorId - Editor instance ID
177
+ */
178
+ static applyCapitalizationToCurrentSelection(style, editorId = null) {
179
+ // Get the correct editor instance
180
+ let editor = null;
181
+ if (editorId) {
182
+ editor = Editor.getInstanceById(editorId);
183
+ } else {
184
+ editor = Editor.getCurrentInstance();
185
+ }
186
+
187
+ if (!editor) {
188
+ console.warn('No editor instance found for capitalization application');
189
+ return;
190
+ }
191
+
192
+ const selection = window.getSelection();
193
+ if (!selection || !selection.rangeCount) return;
194
+
195
+ // Save state before applying format
196
+ saveBeforeFormat();
197
+
198
+ const range = selection.getRangeAt(0);
199
+ const capFormat = Capitalization.createForEditor(editorId);
200
+ if (capFormat) {
201
+ capFormat.apply(style);
202
+
203
+ // Update button text after applying
204
+ capFormat.updateButtonText();
205
+ }
206
+
207
+ // Trigger content change after applying format
208
+ setTimeout(() => {
209
+ if (editor && typeof editor.onContentChange === 'function') {
210
+ editor.onContentChange();
211
+ }
212
+ }, 0);
213
+ }
214
+
215
+ /**
216
+ * Check if an element has capitalization-related inline or computed styles
217
+ * @param {Element} element
218
+ * @returns {boolean}
219
+ */
220
+ hasCapitalizationStyling(element) {
221
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
222
+ if (element.style.fontVariant === 'small-caps') return true;
223
+ if (element.style.textTransform && element.style.textTransform !== 'none') return true;
224
+ const computed = window.getComputedStyle(element);
225
+ if (computed.fontVariant === 'small-caps') return true;
226
+ if (computed.textTransform && computed.textTransform !== 'none') return true;
227
+ return false;
228
+ }
229
+
230
+ /**
231
+ * Determine whether an element is our capitalization wrapper (inline styles only)
232
+ * @param {Element} element
233
+ * @returns {boolean}
234
+ */
235
+ isCapitalizationElement(element) {
236
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
237
+ if (element.style.fontVariant === 'small-caps') return true;
238
+ if (element.style.textTransform && element.style.textTransform !== 'none') return true;
239
+ return false;
240
+ }
241
+
242
+ /**
243
+ * Find nearest ancestor element that is a capitalization wrapper
244
+ * @param {Node} node
245
+ * @returns {Element|null}
246
+ */
247
+ findAncestorCapitalizationElement(node) {
248
+ let current = node;
249
+ if (!current) return null;
250
+ if (current.nodeType === Node.TEXT_NODE) current = current.parentElement;
251
+ while (current && current !== document.body) {
252
+ if (this.isCapitalizationElement(current)) return current;
253
+ current = current.parentElement;
254
+ }
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Apply capitalization style directly to an element
260
+ * @param {Element} element
261
+ * @param {string} style
262
+ */
263
+ setElementCapitalizationStyle(element, style) {
264
+ if (!element) return;
265
+ if (style === 'small-caps') {
266
+ element.style.fontVariant = 'small-caps';
267
+ element.style.textTransform = '';
268
+ } else if (style === 'none') {
269
+ element.style.fontVariant = '';
270
+ element.style.textTransform = '';
271
+ } else {
272
+ element.style.fontVariant = '';
273
+ element.style.textTransform = style;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Apply capitalization format with specified style
279
+ * @param {string} style - Text transform value
280
+ */
281
+ apply(style = 'none') {
282
+ const selection = window.getSelection();
283
+ if (!selection || !selection.rangeCount) return;
284
+
285
+ // Lưu trạng thái trước khi format
286
+ saveBeforeFormat();
287
+
288
+ const range = selection.getRangeAt(0);
289
+ if (range.collapsed) return;
290
+
291
+ // Small Caps is a visual style (not a one-way text transform): wrap the
292
+ // selected text in a span so it can render — and be toggled off — properly.
293
+ if (style === 'small-caps') {
294
+ const frag = range.extractContents();
295
+ const span = document.createElement('span');
296
+ span.style.fontVariant = 'small-caps';
297
+ span.appendChild(frag);
298
+ range.insertNode(span);
299
+ const r = document.createRange();
300
+ r.selectNodeContents(span);
301
+ selection.removeAllRanges();
302
+ selection.addRange(r);
303
+ return;
304
+ }
305
+
306
+ // Hàm đổi chữ theo style
307
+ function transformText(text, style) {
308
+ switch (style) {
309
+ case 'uppercase':
310
+ return text.toUpperCase();
311
+ case 'lowercase':
312
+ return text.toLowerCase();
313
+ case 'capitalize':
314
+ text =text.toLowerCase();
315
+ return text.replace(/\b\w/g, char => char.toUpperCase());
316
+ default:
317
+ return text; // 'none' hoặc không đổi
318
+ }
319
+ }
320
+ function removeEmptyElements(node) {
321
+ if (!node) return;
322
+
323
+ // Duyệt cây DOM từ node này xuống
324
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, null);
325
+ const toRemove = [];
326
+
327
+ while (walker.nextNode()) {
328
+ const el = walker.currentNode;
329
+ // Nếu không có text hoặc chỉ toàn khoảng trắng & không có element con
330
+ if (!el.textContent.trim() && el.childElementCount === 0) {
331
+ toRemove.push(el);
332
+ }
333
+ }
334
+
335
+ toRemove.forEach(el => el.remove());
336
+ }
337
+
338
+ // Nếu có selection: đổi text bên trong
339
+ const contents = range.extractContents();
340
+ const walker = document.createTreeWalker(contents, NodeFilter.SHOW_TEXT, null);
341
+
342
+ while (walker.nextNode()) {
343
+ const textNode = walker.currentNode;
344
+ textNode.textContent = transformText(textNode.textContent, style);
345
+ }
346
+
347
+ range.deleteContents();
348
+ range.insertNode(contents);
349
+ removeEmptyElements(range.commonAncestorContainer);
350
+
351
+ // Giữ nguyên selection
352
+ selection.removeAllRanges();
353
+ selection.addRange(range);
354
+ }
355
+
356
+ /**
357
+ * Remove existing capitalization formatting from range
358
+ * @param {Range} range - Selection range
359
+ */
360
+ removeExistingCapitalization(range) {
361
+ const root = range.commonAncestorContainer;
362
+ const elementsToProcess = new Set();
363
+
364
+ // Helper to maybe add element
365
+ const maybeAdd = (el) => {
366
+ if (el && el.nodeType === Node.ELEMENT_NODE && this.hasCapitalizationStyling(el) && range.intersectsNode(el)) {
367
+ elementsToProcess.add(el);
368
+ }
369
+ };
370
+
371
+ // Include the root if applicable
372
+ if (root && root.nodeType === Node.ELEMENT_NODE) {
373
+ maybeAdd(root);
374
+ }
375
+
376
+ // Walk descendants
377
+ const walker = document.createTreeWalker(
378
+ root,
379
+ NodeFilter.SHOW_ELEMENT,
380
+ {
381
+ acceptNode: (node) => (range.intersectsNode(node) && this.hasCapitalizationStyling(node)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
382
+ }
383
+ );
384
+ let node;
385
+ while ((node = walker.nextNode())) {
386
+ elementsToProcess.add(node);
387
+ }
388
+
389
+ // Include ancestors from start and end containers
390
+ const addAncestors = (startNode) => {
391
+ let current = startNode.nodeType === Node.TEXT_NODE ? startNode.parentElement : startNode;
392
+ while (current && current !== document.body) {
393
+ maybeAdd(current);
394
+ current = current.parentElement;
395
+ }
396
+ };
397
+ addAncestors(range.startContainer);
398
+ addAncestors(range.endContainer);
399
+
400
+ Array.from(elementsToProcess).forEach(element => {
401
+ // Clear text transform and font variant styles
402
+ element.style.textTransform = '';
403
+ element.style.fontVariant = '';
404
+
405
+ // If element has no other styles, unwrap it
406
+ if (!element.style.cssText.trim() && !element.className) {
407
+ this.unwrapElement(element);
408
+ }
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Unwrap an element, moving its children to its parent
414
+ * @param {Element} element - Element to unwrap
415
+ */
416
+ unwrapElement(element) {
417
+ const parent = element.parentNode;
418
+ if (!parent) return;
419
+
420
+ while (element.firstChild) {
421
+ parent.insertBefore(element.firstChild, element);
422
+ }
423
+ parent.removeChild(element);
424
+ }
425
+
426
+ /**
427
+ * Toggle capitalization format - shows/hides capitalization picker
428
+ */
429
+ async toggle() {
430
+ if (this.customSelect.isVisible) {
431
+ this.customSelect.hide();
432
+ } else {
433
+ await this.showCapitalizationPicker();
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Show custom select positioned relative to capitalization button on toolbar
439
+ */
440
+ async showCapitalizationPicker() {
441
+ // Find capitalization button in the current editor's toolbar
442
+ const editor = Editor.getInstanceById(this.editorId);
443
+ if (!editor) return;
444
+
445
+ const toolbar = editor.getModule('toolbar');
446
+ let capitalizationButton = null;
447
+
448
+ if (toolbar) {
449
+ capitalizationButton = toolbar.getButton('capitalization');
450
+ }
451
+
452
+ // Fallback: find button by class in the current editor's toolbar
453
+ if (!capitalizationButton) {
454
+ const toolbarContainer = toolbar?.getContainer();
455
+ if (toolbarContainer) {
456
+ capitalizationButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.capitalization-btn');
457
+ }
458
+ }
459
+
460
+ // Final fallback: find any capitalization button in the current editor's wrapper
461
+ if (!capitalizationButton) {
462
+ capitalizationButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.capitalization-btn');
463
+ }
464
+
465
+ if (!capitalizationButton) {
466
+ console.warn('Capitalization button not found for editor:', this.editorId);
467
+ return;
468
+ }
469
+
470
+ // Update current selection before showing
471
+ const currentCap = this.getCurrentCapitalization();
472
+ if (currentCap) {
473
+ this.customSelect.setCurrentValue(currentCap);
474
+ }
475
+
476
+ await this.customSelect.show(capitalizationButton);
477
+ }
478
+
479
+ /**
480
+ * Check if capitalization format is active - always return false (no active state)
481
+ * Only update button text to show current capitalization
482
+ * @param {string} style - Optional specific style to check
483
+ * @returns {boolean}
484
+ */
485
+ isActive(style = null) {
486
+ // Always update button text to show current capitalization
487
+ this.updateButtonText();
488
+
489
+ // Never show active state for capitalization button
490
+ return false;
491
+ }
492
+
493
+ /**
494
+ * Get current capitalization of the selection
495
+ * @returns {string|null} Current text transform or null
496
+ */
497
+ getCurrentCapitalization() {
498
+ const selection = window.getSelection();
499
+ if (!selection || !selection.rangeCount) return null;
500
+
501
+ const range = selection.getRangeAt(0);
502
+ let currentNode = range.startContainer;
503
+
504
+ // If text node, get parent element
505
+ if (currentNode.nodeType === Node.TEXT_NODE) {
506
+ currentNode = currentNode.parentElement;
507
+ }
508
+
509
+ // Find element with text-transform or font-variant style
510
+ while (currentNode && currentNode !== document.body) {
511
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
512
+ const element = currentNode;
513
+
514
+ // Priority 1: Check if this element has explicit inline styles
515
+ if (element.style.fontVariant === 'small-caps') {
516
+ return 'small-caps';
517
+ }
518
+ if (element.style.textTransform && element.style.textTransform !== 'none') {
519
+ return element.style.textTransform;
520
+ }
521
+
522
+ // Priority 2: Check computed styles
523
+ const computedStyle = window.getComputedStyle(element);
524
+ if (computedStyle.fontVariant === 'small-caps') {
525
+ return 'small-caps';
526
+ }
527
+ if (computedStyle.textTransform && computedStyle.textTransform !== 'none') {
528
+ return computedStyle.textTransform;
529
+ }
530
+ }
531
+ currentNode = currentNode.parentElement;
532
+ }
533
+
534
+ // Default fallback
535
+ return 'none';
536
+ }
537
+
538
+ /**
539
+ * Set current capitalization for future typing
540
+ * @param {string} style - Text transform value
541
+ */
542
+ setCurrentCapitalization(style) {
543
+ // Store for future typing operations
544
+ this.currentCapitalization = style;
545
+ }
546
+
547
+ /**
548
+ * Quick toggle methods for common capitalizations
549
+ */
550
+ static toggleUppercase() {
551
+ const cap = new Capitalization();
552
+ const current = cap.getCurrentCapitalization();
553
+ const newStyle = current === 'uppercase' ? 'none' : 'uppercase';
554
+ Capitalization.applyCapitalizationToCurrentSelection(newStyle);
555
+ }
556
+
557
+ static toggleLowercase() {
558
+ const cap = new Capitalization();
559
+ const current = cap.getCurrentCapitalization();
560
+ const newStyle = current === 'lowercase' ? 'none' : 'lowercase';
561
+ Capitalization.applyCapitalizationToCurrentSelection(newStyle);
562
+ }
563
+
564
+ static toggleCapitalize() {
565
+ const cap = new Capitalization();
566
+ const current = cap.getCurrentCapitalization();
567
+ const newStyle = current === 'capitalize' ? 'none' : 'capitalize';
568
+ Capitalization.applyCapitalizationToCurrentSelection(newStyle);
569
+ }
570
+
571
+ static toggleSmallCaps() {
572
+ const cap = new Capitalization();
573
+ const current = cap.getCurrentCapitalization();
574
+ const newStyle = current === 'small-caps' ? 'none' : 'small-caps';
575
+ Capitalization.applyCapitalizationToCurrentSelection(newStyle);
576
+ }
577
+ }
578
+
579
+ export default Capitalization;