@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,562 @@
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
+ * Line Height Format - Handles line height formatting
8
+ * Now supports multiple editor instances with separate popup instances
9
+ */
10
+ class LineHeight extends InlineFormat {
11
+ static formatName = 'lineHeight';
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 LineHeight format');
21
+ return;
22
+ }
23
+
24
+ this.editorId = currentEditor.instanceId;
25
+
26
+ // Check if this editor already has a line height select instance
27
+ let customSelect = currentEditor.getPopupInstance('line-height');
28
+
29
+ if (!customSelect) {
30
+ // Create new custom select instance for this editor
31
+ const heightMap = LineHeight.getHeightMap();
32
+ const items = Object.values(heightMap).map(heightData => ({
33
+ value: heightData.height,
34
+ label: heightData.element,
35
+ title: heightData.title
36
+ }));
37
+
38
+ customSelect = new CustomSelect({
39
+ items: items,
40
+ displayProperty: 'label',
41
+ valueProperty: 'value',
42
+ className: 'line-height-select',
43
+ onItemSelect: (value, item) => {
44
+ LineHeight.applyLineHeightToCurrentSelection(value, this.editorId);
45
+ },
46
+ editor: currentEditor,
47
+ editorId: this.editorId
48
+ });
49
+
50
+ // Store popup instance in editor
51
+ currentEditor.setPopupInstance('line-height', customSelect);
52
+ }
53
+
54
+ this.customSelect = customSelect;
55
+
56
+ // Set up event listener for selection changes
57
+ this.setupSelectionListener();
58
+ }
59
+
60
+ /**
61
+ * Create a new LineHeight format instance for a specific editor
62
+ * @param {string} editorId - Editor instance ID
63
+ * @returns {LineHeight} LineHeight format instance
64
+ */
65
+ static createForEditor(editorId) {
66
+ const editor = Editor.getInstanceById(editorId);
67
+ if (!editor) {
68
+ console.warn('No editor instance found for ID:', editorId);
69
+ return null;
70
+ }
71
+
72
+ // Temporarily set as current instance
73
+ const originalCurrent = Editor.currentInstance;
74
+ Editor.currentInstance = editor;
75
+
76
+ // Create format instance
77
+ const format = new LineHeight();
78
+
79
+ // Restore original current instance
80
+ Editor.currentInstance = originalCurrent;
81
+
82
+ return format;
83
+ }
84
+
85
+ /**
86
+ * Set up event listener for selection changes to update button text
87
+ */
88
+ setupSelectionListener() {
89
+ // Use a debounced function to avoid too many updates
90
+ let updateTimeout;
91
+ const debouncedUpdate = () => {
92
+ clearTimeout(updateTimeout);
93
+ updateTimeout = setTimeout(() => {
94
+ // Only update if selection is in this editor
95
+ const selection = window.getSelection();
96
+ if (selection && selection.rangeCount > 0) {
97
+ const range = selection.getRangeAt(0);
98
+ const editor = Editor.getInstanceById(this.editorId);
99
+ if (editor && (editor.editor.contains(range.startContainer) || editor.editor.isSameNode(range.startContainer))) {
100
+ this.updateButtonText();
101
+ }
102
+ }
103
+ }, 50); // 50ms delay
104
+ };
105
+
106
+ // Listen for selection changes
107
+ document.addEventListener('selectionchange', debouncedUpdate);
108
+
109
+ // Also listen for mouseup and keyup events for immediate feedback
110
+ document.addEventListener('mouseup', debouncedUpdate);
111
+ document.addEventListener('keyup', debouncedUpdate);
112
+
113
+ // Store the listener for cleanup
114
+ this.selectionListener = debouncedUpdate;
115
+ }
116
+
117
+ /**
118
+ * Get height map with different line heights
119
+ */
120
+ static getHeightMap() {
121
+ return {
122
+ '1.0': {
123
+ height: '1',
124
+ element: '<span>1.0</span>',
125
+ title: '1.0'
126
+ },
127
+ '1.2': {
128
+ height: '1.2',
129
+ element: '<span>1.2</span>',
130
+ title: '1.2'
131
+ },
132
+ '1.5': {
133
+ height: '1.5',
134
+ element: '<span>1.5</span>',
135
+ title: '1.5'
136
+ },
137
+ '1.8': {
138
+ height: '1.8',
139
+ element: '<span>1.8</span>',
140
+ title: '1.8'
141
+ },
142
+ '2.0': {
143
+ height: '2',
144
+ element: '<span>2.0</span>',
145
+ title: '2.0'
146
+ },
147
+ '2.5': {
148
+ height: '2.5',
149
+ element: '<span>2.5</span>',
150
+ title: '2.5'
151
+ },
152
+ '3.0': {
153
+ height: '3',
154
+ element: '<span>3.0</span>',
155
+ title: '3.0'
156
+ }
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Get display name for line height
162
+ * @param {string} height - Line height value
163
+ * @returns {string} Display name
164
+ */
165
+ static getHeightDisplayName(height) {
166
+ const heightMap = this.getHeightMap();
167
+ if (heightMap[height]?.title) return heightMap[height].title;
168
+ // Always show the applied value rather than a placeholder
169
+ const num = parseFloat(height);
170
+ if (!isNaN(num)) return String(num);
171
+ return 'Normal';
172
+ }
173
+
174
+ /**
175
+ * Update custom button text based on current line height
176
+ */
177
+ updateButtonText() {
178
+ const currentHeight = this.getCurrentHeight();
179
+ const displayName = LineHeight.getHeightDisplayName(currentHeight || '1.15');
180
+
181
+ // Find line-height button in the specific editor's toolbar using editorId
182
+ const editor = Editor.getInstanceById(this.editorId);
183
+ if (!editor) return;
184
+
185
+ const toolbar = editor.getModule('toolbar');
186
+ let lineHeightButton = null;
187
+
188
+ if (toolbar) {
189
+ lineHeightButton = toolbar.getButton('line-height');
190
+ }
191
+
192
+ // Fallback: find button by class in the specific editor's toolbar
193
+ if (!lineHeightButton) {
194
+ const toolbarContainer = toolbar?.getContainer();
195
+ if (toolbarContainer) {
196
+ lineHeightButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.line-height-btn');
197
+ }
198
+ }
199
+
200
+ // Final fallback: find any line-height button in the specific editor's wrapper
201
+ if (!lineHeightButton) {
202
+ lineHeightButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.line-height-btn');
203
+ }
204
+
205
+ if (lineHeightButton && lineHeightButton.updateText) {
206
+ lineHeightButton.updateText(displayName);
207
+ } else if (lineHeightButton) {
208
+ lineHeightButton.textContent = displayName;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Create element with specific line height
214
+ * @param {string} height - Line height value
215
+ * @returns {HTMLElement}
216
+ */
217
+ static create(height = '1.15') {
218
+ const node = document.createElement('span');
219
+ node.style.lineHeight = height;
220
+ return node;
221
+ }
222
+
223
+ /**
224
+ * Static method to apply line height to current selection
225
+ * @param {string} height - Line height value
226
+ * @param {string} editorId - Editor instance ID
227
+ */
228
+ static applyLineHeightToCurrentSelection(height, editorId = null) {
229
+ // Get the correct editor instance
230
+ let editor = null;
231
+ if (editorId) {
232
+ editor = Editor.getInstanceById(editorId);
233
+ } else {
234
+ editor = Editor.getCurrentInstance();
235
+ }
236
+
237
+ if (!editor) {
238
+ console.warn('No editor instance found for line height application');
239
+ return;
240
+ }
241
+
242
+ const selection = window.getSelection();
243
+ if (!selection || !selection.rangeCount) return;
244
+
245
+ // Save state before applying format
246
+ saveBeforeFormat();
247
+
248
+ const range = selection.getRangeAt(0);
249
+ const lineHeightFormat = LineHeight.createForEditor(editorId);
250
+ if (lineHeightFormat) {
251
+ lineHeightFormat.apply(height);
252
+
253
+ // Update button text after applying
254
+ lineHeightFormat.updateButtonText();
255
+ }
256
+
257
+ // Trigger content change after applying format
258
+ setTimeout(() => {
259
+ if (editor && typeof editor.onContentChange === 'function') {
260
+ editor.onContentChange();
261
+ }
262
+ }, 0);
263
+ }
264
+
265
+ /**
266
+ * Apply line height format with specified height
267
+ * @param {string} height - Line height value
268
+ */
269
+ apply(height = '1.15') {
270
+ const selection = window.getSelection();
271
+ if (!selection || !selection.rangeCount) return;
272
+ // Hàm đặt caret vào bên trong span mới
273
+ function moveCaretInside(el) {
274
+ const sel = window.getSelection();
275
+ const range = document.createRange();
276
+ const textNode = el.firstChild;
277
+ range.setStart(textNode, textNode.length);
278
+ range.collapse(true);
279
+ sel.removeAllRanges();
280
+ sel.addRange(range);
281
+ }
282
+ // Save state before applying format
283
+ saveBeforeFormat();
284
+
285
+ const range = selection.getRangeAt(0);
286
+
287
+ if (range.collapsed) {
288
+ // No selection - set style for future typing
289
+ let node = range.startContainer;
290
+ if (node.nodeType === Node.TEXT_NODE) {
291
+ node = node.parentNode;
292
+ }
293
+
294
+ // Tìm phần tử block cha gần nhất (div, p, li, ...)
295
+ const blockParent = node.closest('div, p, li, section, article') || node;
296
+ blockParent.style.lineHeight = height;
297
+ moveCaretInside(blockParent);
298
+
299
+ return;
300
+ }
301
+
302
+ // Apply to block elements if possible for better line height effect
303
+ const blockElements = this.getBlockElementsInRange(range);
304
+
305
+ if (blockElements.length > 0) {
306
+ // Apply to block elements
307
+ blockElements.forEach(block => {
308
+ block.style.lineHeight = height;
309
+ });
310
+ } else {
311
+ // Fallback: wrap in span with line-height
312
+ const heightSpan = this.constructor.create(height);
313
+
314
+ try {
315
+ const contents = range.extractContents();
316
+ heightSpan.appendChild(contents);
317
+ range.insertNode(heightSpan);
318
+
319
+ // Select the content in the span
320
+ const newRange = document.createRange();
321
+ newRange.selectNodeContents(heightSpan);
322
+ selection.removeAllRanges();
323
+ selection.addRange(newRange);
324
+ } catch (error) {
325
+ console.warn('Failed to apply line height manually:', error);
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Get block elements within the range
332
+ * @param {Range} range - Selection range
333
+ * @returns {Array} Array of block elements
334
+ */
335
+ getBlockElementsInRange(range) {
336
+ const blockElements = [];
337
+ const blockTags = ['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'UL', 'OL', 'LI'];
338
+
339
+ // Create a fragment of the selection
340
+ const fragment = range.cloneContents();
341
+
342
+ // Get all potential block elements in the fragment
343
+ const walker = document.createTreeWalker(
344
+ fragment,
345
+ NodeFilter.SHOW_ELEMENT,
346
+ {
347
+ acceptNode: (node) => {
348
+ return blockTags.includes(node.tagName) ?
349
+ NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
350
+ }
351
+ }
352
+ );
353
+
354
+ // Get corresponding elements from the actual document
355
+ let node = walker.nextNode();
356
+ while (node) {
357
+ // Find the actual element in the document that corresponds to this fragment node
358
+ const actualNode = range.commonAncestorContainer.querySelector(
359
+ `${node.tagName.toLowerCase()}:not([data-processed])`
360
+ );
361
+ if (actualNode && range.intersectsNode(actualNode)) {
362
+ blockElements.push(actualNode);
363
+ // Mark as processed to avoid duplicates
364
+ actualNode.setAttribute('data-processed', 'true');
365
+ }
366
+ node = walker.nextNode();
367
+ }
368
+
369
+ // Clean up the temporary attribute
370
+ blockElements.forEach(el => el.removeAttribute('data-processed'));
371
+
372
+ // If no block elements found in selection, get the closest parent block element
373
+ if (blockElements.length === 0) {
374
+ let currentNode = range.startContainer;
375
+
376
+ // If text node, get parent element
377
+ if (currentNode.nodeType === Node.TEXT_NODE) {
378
+ currentNode = currentNode.parentElement;
379
+ }
380
+
381
+ // Find parent block element
382
+ while (currentNode && currentNode !== document.body) {
383
+ if (currentNode.nodeType === Node.ELEMENT_NODE &&
384
+ blockTags.includes(currentNode.tagName)) {
385
+ blockElements.push(currentNode);
386
+ break;
387
+ }
388
+ currentNode = currentNode.parentElement;
389
+ }
390
+ }
391
+
392
+ return blockElements;
393
+ }
394
+
395
+ /**
396
+ * Toggle line height format - shows/hides height picker
397
+ */
398
+ async toggle() {
399
+ if (this.customSelect.isVisible) {
400
+ this.customSelect.hide();
401
+ } else {
402
+ await this.showHeightPicker();
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Show custom select positioned relative to line height button on toolbar
408
+ */
409
+ async showHeightPicker() {
410
+ // Find line-height button in the current editor's toolbar
411
+ const editor = Editor.getInstanceById(this.editorId);
412
+ if (!editor) return;
413
+
414
+ const toolbar = editor.getModule('toolbar');
415
+ let lineHeightButton = null;
416
+
417
+ if (toolbar) {
418
+ lineHeightButton = toolbar.getButton('line-height');
419
+ }
420
+
421
+ // Fallback: find button by class in the current editor's toolbar
422
+ if (!lineHeightButton) {
423
+ const toolbarContainer = toolbar?.getContainer();
424
+ if (toolbarContainer) {
425
+ lineHeightButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.line-height-btn');
426
+ }
427
+ }
428
+
429
+ // Final fallback: find any line-height button in the current editor's wrapper
430
+ if (!lineHeightButton) {
431
+ lineHeightButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.line-height-btn');
432
+ }
433
+
434
+ if (!lineHeightButton) {
435
+ console.warn('Line-height button not found for editor:', this.editorId);
436
+ return;
437
+ }
438
+
439
+ // Update current selection before showing
440
+ const currentHeight = this.getCurrentHeight();
441
+ if (currentHeight) {
442
+ this.customSelect.setCurrentValue(currentHeight);
443
+ }
444
+
445
+ await this.customSelect.show(lineHeightButton);
446
+ }
447
+
448
+ /**
449
+ * Check if line height format is active - always return false (no active state)
450
+ * Only update button text to show current height
451
+ * @param {string} height - Optional specific height to check
452
+ * @returns {boolean}
453
+ */
454
+ isActive(height = null) {
455
+ // Always update button text to show current height
456
+ this.updateButtonText();
457
+
458
+ // Never show active state for line height button
459
+ return false;
460
+ }
461
+
462
+ /**
463
+ * Get current line height of the selection
464
+ * @returns {string|null} Current line height or null
465
+ */
466
+ getCurrentHeight() {
467
+ const selection = window.getSelection();
468
+ if (!selection || !selection.rangeCount) return null;
469
+
470
+ const range = selection.getRangeAt(0);
471
+ let currentNode = range.startContainer;
472
+
473
+ // If text node, get parent element
474
+ if (currentNode.nodeType === Node.TEXT_NODE) {
475
+ currentNode = currentNode.parentElement;
476
+ }
477
+
478
+ // Get the specific editor instance
479
+ const editor = Editor.getInstanceById(this.editorId);
480
+ if (!editor) return '1.15';
481
+
482
+ // Check if the selection is within this editor
483
+ if (!editor.editor.contains(currentNode) && !editor.editor.isSameNode(currentNode)) {
484
+ // Selection is not in this editor, return default
485
+ return '1.15';
486
+ }
487
+
488
+ // Find element with line-height style
489
+ while (currentNode && currentNode !== document.body) {
490
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
491
+ const element = currentNode;
492
+
493
+ // Priority 1: Check if this element has explicit inline line-height
494
+ if (element.style.lineHeight) {
495
+ const height = element.style.lineHeight;
496
+ return this.normalizeHeightValue(height);
497
+ }
498
+
499
+ // Priority 2: Check computed line-height
500
+ const computedStyle = window.getComputedStyle(element);
501
+ const lineHeight = computedStyle.lineHeight;
502
+
503
+ if (lineHeight && lineHeight !== 'normal' && lineHeight !== 'initial' && lineHeight !== 'inherit') {
504
+ // Convert pixel values to relative values if possible
505
+ if (lineHeight.endsWith('px')) {
506
+ const fontSize = parseFloat(computedStyle.fontSize);
507
+ const lineHeightPx = parseFloat(lineHeight);
508
+ if (fontSize > 0) {
509
+ const relative = (lineHeightPx / fontSize).toFixed(2);
510
+ return this.normalizeHeightValue(relative);
511
+ }
512
+ }
513
+ return this.normalizeHeightValue(lineHeight);
514
+ }
515
+ }
516
+ currentNode = currentNode.parentElement;
517
+ }
518
+
519
+ // Default fallback
520
+ return '1.15';
521
+ }
522
+
523
+ /**
524
+ * Normalize height value to match heightMap keys
525
+ * @param {string} height - Raw height value
526
+ * @returns {string} Normalized height value
527
+ */
528
+ normalizeHeightValue(height) {
529
+ if (!height) return '1.15';
530
+
531
+ // Convert to number and back to string to normalize
532
+ const numValue = parseFloat(height);
533
+ if (isNaN(numValue)) return '1.15';
534
+
535
+ // Round to 1 decimal place and convert back to string
536
+ const normalized = numValue.toFixed(1);
537
+
538
+ // Check if this normalized value exists in our heightMap
539
+ const heightMap = this.constructor.getHeightMap();
540
+ if (heightMap[normalized]) {
541
+ return normalized;
542
+ }
543
+
544
+ // If not in map, return the original value
545
+ return height;
546
+ }
547
+
548
+
549
+ /**
550
+ * Clean up event listeners to prevent memory leaks
551
+ */
552
+ destroy() {
553
+ if (this.selectionListener) {
554
+ document.removeEventListener('selectionchange', this.selectionListener);
555
+ document.removeEventListener('mouseup', this.selectionListener);
556
+ document.removeEventListener('keyup', this.selectionListener);
557
+ this.selectionListener = null;
558
+ }
559
+ }
560
+ }
561
+
562
+ export default LineHeight;