@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,422 @@
1
+ import { BlockFormat } from '../core/format.js';
2
+ import TextAlignPicker from '../ui/text-align-picker.js';
3
+ import IconUtils from '../ui/icons.js';
4
+ import { saveBeforeFormat } from '../utils/history-helper.js';
5
+ import Editor from '../core/editor.js';
6
+ import { execFormat } from '../utils/exec-command.js';
7
+
8
+ /**
9
+ * Text Align Format - Handles text alignment formatting
10
+ * Now supports multiple editor instances with separate popup instances
11
+ */
12
+ class TextAlign extends BlockFormat {
13
+ static formatName = 'text-align';
14
+ static tagName = 'P';
15
+ static attribute = 'style';
16
+
17
+ constructor() {
18
+ super();
19
+
20
+ // Get current editor instance
21
+ const currentEditor = Editor.getCurrentInstance();
22
+ if (!currentEditor) {
23
+ console.warn('No editor instance found for TextAlign format');
24
+ return;
25
+ }
26
+
27
+ this.editorId = currentEditor.instanceId;
28
+
29
+ // Check if this editor already has a text align picker instance
30
+ let alignPicker = currentEditor.getPopupInstance('text-align');
31
+
32
+ if (!alignPicker) {
33
+ // Create new text align picker instance for this editor
34
+ alignPicker = new TextAlignPicker({
35
+ onAlignSelect: (alignment) => {
36
+ TextAlign.applyAlignToCurrentSelection(alignment, this.editorId);
37
+ },
38
+ editor: currentEditor,
39
+ editorId: this.editorId
40
+ });
41
+
42
+ // Store popup instance in editor
43
+ currentEditor.setPopupInstance('text-align', alignPicker);
44
+ }
45
+
46
+ this.alignPicker = alignPicker;
47
+ }
48
+
49
+ /**
50
+ * Create a new TextAlign format instance for a specific editor
51
+ * @param {string} editorId - Editor instance ID
52
+ * @returns {TextAlign} TextAlign format instance
53
+ */
54
+ static createForEditor(editorId) {
55
+ const editor = Editor.getInstanceById(editorId);
56
+ if (!editor) {
57
+ console.warn('No editor instance found for ID:', editorId);
58
+ return null;
59
+ }
60
+
61
+ // Temporarily set as current instance
62
+ const originalCurrent = Editor.currentInstance;
63
+ Editor.currentInstance = editor;
64
+
65
+ // Create format instance
66
+ const format = new TextAlign();
67
+
68
+ // Restore original current instance
69
+ Editor.currentInstance = originalCurrent;
70
+
71
+ return format;
72
+ }
73
+
74
+ /**
75
+ * Create block element with text alignment
76
+ * @param {string} value - Alignment value (left, center, right, justify)
77
+ * @returns {HTMLElement}
78
+ */
79
+ static create(value) {
80
+ const node = document.createElement(this.tagName);
81
+ if (value && value !== 'left') {
82
+ node.style.textAlign = value;
83
+ }
84
+ return node;
85
+ }
86
+
87
+ /**
88
+ * Static method to apply alignment to current selection or cursor position
89
+ * @param {string} alignment - Alignment value
90
+ * @param {string} editorId - Editor instance ID
91
+ */
92
+ static applyAlignToCurrentSelection(alignment, editorId = null) {
93
+ // Get the correct editor instance
94
+ let editor = null;
95
+ if (editorId) {
96
+ editor = Editor.getInstanceById(editorId);
97
+ } else {
98
+ editor = Editor.getCurrentInstance();
99
+ }
100
+
101
+ if (!editor) {
102
+ console.warn('No editor instance found for text alignment application');
103
+ return;
104
+ }
105
+
106
+ const selection = window.getSelection();
107
+ if (!selection || !selection.rangeCount) return;
108
+
109
+ // Save state before applying format
110
+ saveBeforeFormat();
111
+ // Lưu vị trí caret trước khi thay đổi
112
+ const originalRange = selection.getRangeAt(0);
113
+ const caretContainer = originalRange.endContainer;
114
+ const caretOffset = originalRange.endOffset;
115
+ try {
116
+ const range = selection.getRangeAt(0);
117
+ const blockElements = TextAlign.getSelectedBlockElements(range);
118
+
119
+ if (blockElements.length === 0) {
120
+ // If no block elements found, create one
121
+ execFormat('formatBlock', 'p');
122
+ const newRange = selection.getRangeAt(0);
123
+ const newBlocks = TextAlign.getSelectedBlockElements(newRange);
124
+ newBlocks.forEach(block => {
125
+ TextAlign.applyAlignmentToBlock(block, alignment);
126
+ });
127
+ } else {
128
+ // Apply alignment to existing blocks
129
+ blockElements.forEach(block => {
130
+ TextAlign.applyAlignmentToBlock(block, alignment);
131
+ });
132
+ }
133
+
134
+ // Update toolbar button icon after applying alignment
135
+ TextAlign.updateToolbarButtonIcon(alignment, editorId);
136
+ // Khôi phục caret
137
+ selection.removeAllRanges();
138
+ const newCaretRange = document.createRange();
139
+ newCaretRange.setStart(caretContainer, caretOffset);
140
+ newCaretRange.collapse(true);
141
+ selection.addRange(newCaretRange);
142
+
143
+ } catch (error) {
144
+ console.error('Error applying text alignment:', error);
145
+ }
146
+
147
+ // Trigger content change after applying format
148
+ setTimeout(() => {
149
+ if (editor && typeof editor.onContentChange === 'function') {
150
+ editor.onContentChange();
151
+ }
152
+ }, 0);
153
+ }
154
+
155
+ /**
156
+ * Apply alignment to a specific block element
157
+ */
158
+ static applyAlignmentToBlock(block, alignment) {
159
+ if (alignment === 'left') {
160
+ block.style.textAlign = '';
161
+ } else {
162
+ block.style.textAlign = alignment;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Get icon name for alignment value
168
+ * @param {string} alignment - Alignment value
169
+ * @returns {string} Icon name
170
+ */
171
+ static getIconNameForAlignment(alignment) {
172
+ const iconMap = {
173
+ 'left': 'align-left',
174
+ 'center': 'align-center',
175
+ 'right': 'align-right',
176
+ 'justify': 'align-justify'
177
+ };
178
+ return iconMap[alignment] || 'align-center';
179
+ }
180
+
181
+ /**
182
+ * Update toolbar button icon based on alignment
183
+ * @param {string} alignment - Current alignment
184
+ * @param {string} editorId - Editor instance ID
185
+ */
186
+ static updateToolbarButtonIcon(alignment, editorId = null) {
187
+ // Get the correct editor instance
188
+ let editor = null;
189
+ if (editorId) {
190
+ editor = Editor.getInstanceById(editorId);
191
+ } else {
192
+ editor = Editor.getCurrentInstance();
193
+ }
194
+
195
+ if (!editor) return;
196
+
197
+ const toolbar = editor.getModule('toolbar');
198
+ let button = null;
199
+
200
+ if (toolbar) {
201
+ button = toolbar.getButton('text-align');
202
+ }
203
+
204
+ // Fallback: find button by class in the current editor's toolbar
205
+ if (!button) {
206
+ const toolbarContainer = toolbar?.getContainer();
207
+ if (toolbarContainer) {
208
+ button = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-align-btn');
209
+ }
210
+ }
211
+
212
+ // Final fallback: find any text-align button in the current editor's wrapper
213
+ if (!button) {
214
+ button = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-align-btn');
215
+ }
216
+
217
+ if (!button) return;
218
+
219
+ const iconName = TextAlign.getIconNameForAlignment(alignment);
220
+ const titleMap = {
221
+ 'left': 'Align Left',
222
+ 'center': 'Align Center',
223
+ 'right': 'Align Right',
224
+ 'justify': 'Justify'
225
+ };
226
+
227
+ // Update button title
228
+ button.title = titleMap[alignment] || 'Text Alignment';
229
+
230
+ // Update icon
231
+ const svgContent = IconUtils.getIcon(iconName);
232
+ if (svgContent) {
233
+ const iconSpan = button.querySelector('.icon');
234
+ if (iconSpan) {
235
+ iconSpan.innerHTML = svgContent;
236
+ } else {
237
+ button.innerHTML = `<span class="icon">${svgContent}</span>`;
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Get all selected block elements
244
+ */
245
+ static getSelectedBlockElements(range) {
246
+ const blocks = [];
247
+
248
+ // Xác định block chứa điểm bắt đầu và kết thúc
249
+ const startBlock = TextAlign.getBlockElement(range.startContainer);
250
+ const endBlock = TextAlign.getBlockElement(range.endContainer);
251
+
252
+ if (!startBlock || !endBlock) return blocks;
253
+
254
+ // Nếu chỉ trong 1 block
255
+ if (startBlock === endBlock) {
256
+ blocks.push(startBlock);
257
+ return blocks;
258
+ }
259
+
260
+ // Duyệt từ startBlock tới endBlock
261
+ let currentBlock = startBlock;
262
+ while (currentBlock) {
263
+ // Chỉ thêm block nếu nó giao với range
264
+ const blockRange = document.createRange();
265
+ blockRange.selectNodeContents(currentBlock);
266
+
267
+ if (range.compareBoundaryPoints(Range.END_TO_START, blockRange) < 0 &&
268
+ range.compareBoundaryPoints(Range.START_TO_END, blockRange) > 0) {
269
+ blocks.push(currentBlock);
270
+ }
271
+
272
+ if (currentBlock === endBlock) break;
273
+ currentBlock = TextAlign.getNextBlockElement(currentBlock);
274
+ }
275
+
276
+ return blocks;
277
+ }
278
+
279
+ /**
280
+ * Get the block element containing the given node
281
+ */
282
+ static getBlockElement(node) {
283
+ if (!node) return null;
284
+
285
+ let currentNode = node;
286
+ while (currentNode && currentNode !== document.body) {
287
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
288
+ const tagName = currentNode.tagName;
289
+ if (['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI'].includes(tagName)) {
290
+ return currentNode;
291
+ }
292
+ }
293
+ currentNode = currentNode.parentNode;
294
+ }
295
+ return null;
296
+ }
297
+
298
+ /**
299
+ * Get next block element in document order
300
+ */
301
+ static getNextBlockElement(element) {
302
+ let currentNode = element.nextSibling;
303
+
304
+ while (currentNode) {
305
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
306
+ const tagName = currentNode.tagName;
307
+ if (['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI'].includes(tagName)) {
308
+ return currentNode;
309
+ }
310
+ }
311
+ currentNode = currentNode.nextSibling;
312
+ }
313
+
314
+ return null;
315
+ }
316
+
317
+ /**
318
+ * Apply alignment formatting with specified value
319
+ * @param {string} value - Alignment value (left, center, right, justify)
320
+ */
321
+ apply(value = 'left') {
322
+ TextAlign.applyAlignToCurrentSelection(value, this.editorId);
323
+ }
324
+
325
+ /**
326
+ * Remove alignment formatting (reset to left)
327
+ */
328
+ remove() {
329
+ TextAlign.applyAlignToCurrentSelection('left', this.editorId);
330
+ }
331
+
332
+ /**
333
+ * Toggle alignment formatting - shows/hides alignment picker
334
+ */
335
+ toggle() {
336
+ if (this.alignPicker.isVisible) {
337
+ this.alignPicker.hide();
338
+ } else {
339
+ this.showAlignPicker();
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Show alignment picker positioned relative to align button on toolbar
345
+ */
346
+ showAlignPicker() {
347
+ // Find text-align button in the current editor's toolbar
348
+ const editor = Editor.getInstanceById(this.editorId);
349
+ if (!editor) return;
350
+
351
+ const toolbar = editor.getModule('toolbar');
352
+ let alignButton = null;
353
+
354
+ if (toolbar) {
355
+ alignButton = toolbar.getButton('text-align');
356
+ }
357
+
358
+ // Fallback: find button by class in the current editor's toolbar
359
+ if (!alignButton) {
360
+ const toolbarContainer = toolbar?.getContainer();
361
+ if (toolbarContainer) {
362
+ alignButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-align-btn');
363
+ }
364
+ }
365
+
366
+ // Final fallback: find any text-align button in the current editor's wrapper
367
+ if (!alignButton) {
368
+ alignButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-align-btn');
369
+ }
370
+
371
+ if (!alignButton) {
372
+ console.warn('Text-align button not found for editor:', this.editorId);
373
+ return;
374
+ }
375
+
376
+ this.alignPicker.show(alignButton);
377
+ }
378
+
379
+ /**
380
+ * Check if specific alignment is active in current selection
381
+ * Always returns false because text-align button should not have active state
382
+ * Instead, the button icon changes to reflect current alignment
383
+ */
384
+ isActive(alignment = null) {
385
+ // Update button icon based on current alignment
386
+ const currentAlignment = TextAlign.getCurrentAlignment();
387
+ TextAlign.updateToolbarButtonIcon(currentAlignment, this.editorId);
388
+
389
+ // Highlight when a non-default alignment (center / right / justify) is set.
390
+ return !!currentAlignment && currentAlignment !== 'left' && currentAlignment !== 'start';
391
+ }
392
+
393
+ /**
394
+ * Get current alignment of selection
395
+ */
396
+ static getCurrentAlignment() {
397
+ const selection = window.getSelection();
398
+ if (!selection || !selection.rangeCount) return 'left';
399
+
400
+ try {
401
+ const range = selection.getRangeAt(0);
402
+ // Lấy tất cả block trong vùng chọn
403
+ const blocks = TextAlign.getSelectedBlockElements(range);
404
+
405
+ // Nếu có nhiều block -> lấy block đầu tiên
406
+ const firstBlock = blocks.length > 0
407
+ ? blocks[0]
408
+ : TextAlign.getBlockElement(range.commonAncestorContainer);
409
+
410
+ if (!firstBlock) return 'left';
411
+
412
+ const textAlign = window.getComputedStyle(firstBlock).textAlign;
413
+ return textAlign === 'left' || textAlign === 'start' || !textAlign ? 'left' : textAlign;
414
+ } catch (error) {
415
+ console.error('Error getting current alignment:', error);
416
+ return 'left';
417
+ }
418
+ }
419
+
420
+ }
421
+
422
+ export default TextAlign;