@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,540 @@
1
+ import { execFormat, queryFormatState } from '../utils/exec-command.js';
2
+
3
+ /**
4
+ * Base Format class - Inspired by Quill's architecture
5
+ * All text formats should extend this class
6
+ */
7
+ export class Format {
8
+ static formatName = '';
9
+ static tagName = '';
10
+ static className = '';
11
+
12
+ constructor(domNode) {
13
+ this.domNode = domNode;
14
+ }
15
+
16
+ /**
17
+ * Create a new format node
18
+ * @param {*} value - Format value
19
+ * @returns {HTMLElement}
20
+ */
21
+ static create(value) {
22
+ const node = document.createElement(this.tagName);
23
+ if (this.className) {
24
+ node.className = this.className;
25
+ }
26
+ return node;
27
+ }
28
+
29
+ getOffsetWithin(container, range) {
30
+ let offset = 0;
31
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
32
+ let currentNode;
33
+
34
+ while ((currentNode = walker.nextNode())) {
35
+ if (currentNode === range.startContainer) {
36
+ return offset + range.startOffset;
37
+ }
38
+ offset += currentNode.textContent.length;
39
+ }
40
+
41
+ return offset;
42
+ }
43
+
44
+ /**
45
+ * Check if format is active at current selection
46
+ * @returns {boolean}
47
+ */
48
+ }
49
+
50
+ /**
51
+ * Inline Format - for formats like bold, italic, underline
52
+ * Handles inline formatting that wraps text within the same line/block
53
+ */
54
+ export class InlineFormat extends Format {
55
+ /**
56
+ * Create inline format element
57
+ * @param {*} value - Format value
58
+ * @returns {HTMLElement}
59
+ */
60
+ static create(value) {
61
+ const node = super.create(value);
62
+ return node;
63
+ }
64
+
65
+ /**
66
+ * Apply inline format to selection
67
+ * Wraps selected text or inserts format marker at cursor
68
+ * @param {*} value - Format value
69
+ */
70
+ apply(value) {
71
+ const selection = window.getSelection();
72
+ if (!selection || !selection.rangeCount) return;
73
+
74
+ const range = selection.getRangeAt(0);
75
+ if (range.collapsed) {
76
+ // No selection - insert format marker at cursor
77
+ const formatNode = this.constructor.create(value);
78
+ formatNode.appendChild(document.createTextNode('\u200B')); // Zero-width space
79
+ range.insertNode(formatNode);
80
+
81
+ // Position cursor inside the format node
82
+ const newRange = document.createRange();
83
+ newRange.setStart(formatNode.firstChild, 1);
84
+ newRange.collapse(true);
85
+ selection.removeAllRanges();
86
+ selection.addRange(newRange);
87
+ } else {
88
+ // Has selection - wrap selected content
89
+ const contents = range.extractContents();
90
+ const formatNode = this.constructor.create(value);
91
+ formatNode.appendChild(contents);
92
+ range.insertNode(formatNode);
93
+
94
+ // Select the formatted content
95
+ const newRange = document.createRange();
96
+ newRange.selectNodeContents(formatNode);
97
+ selection.removeAllRanges();
98
+ selection.addRange(newRange);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Remove inline format from selection
104
+ * Unwraps formatted content or removes format at cursor
105
+ */
106
+ remove() {
107
+ const selection = window.getSelection();
108
+ if (!selection || !selection.rangeCount) return;
109
+
110
+ const range = selection.getRangeAt(0);
111
+
112
+ if (range.collapsed) {
113
+ // Handle cursor position
114
+ this.removeAtCursor(range, selection);
115
+ } else {
116
+ // Handle selection
117
+ this.removeFromSelection(range, selection);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Remove format at cursor position
123
+ * @param {Range} range - Current range
124
+ * @param {Selection} selection - Current selection
125
+ */
126
+ removeAtCursor(range, selection) {
127
+ const container = range.startContainer;
128
+ const formatNode = this.findFormatNode(container);
129
+
130
+ if (!formatNode || !formatNode.parentNode) return;
131
+
132
+ const text = formatNode.textContent;
133
+ const absoluteOffset = this.getOffsetWithin(formatNode, range);
134
+
135
+ // Split the format node at cursor position
136
+ const beforeText = text.slice(0, absoluteOffset);
137
+ const afterText = text.slice(absoluteOffset);
138
+
139
+ const fragment = document.createDocumentFragment();
140
+
141
+ if (beforeText) {
142
+ const beforeNode = formatNode.cloneNode(false);
143
+ beforeNode.textContent = beforeText;
144
+ fragment.appendChild(beforeNode);
145
+ }
146
+
147
+ // Insert zero-width space as marker
148
+ const zwspNode = document.createTextNode('\u200B');
149
+ fragment.appendChild(zwspNode);
150
+
151
+ if (afterText) {
152
+ const afterNode = formatNode.cloneNode(false);
153
+ afterNode.textContent = afterText;
154
+ fragment.appendChild(afterNode);
155
+ }
156
+
157
+ formatNode.replaceWith(fragment);
158
+
159
+ // Position cursor after the marker
160
+ const newRange = document.createRange();
161
+ newRange.setStartAfter(zwspNode);
162
+ newRange.collapse(true);
163
+ selection.removeAllRanges();
164
+ selection.addRange(newRange);
165
+ }
166
+
167
+ /**
168
+ * Remove format from selection
169
+ */
170
+ removeFromSelection(range, selection) {
171
+ const formatName = this.constructor.formatName;
172
+ execFormat(formatName);
173
+ if (formatName === 'strike') {
174
+ execFormat('strikeThrough');
175
+ }
176
+ }
177
+
178
+
179
+
180
+ /**
181
+ * Find the format node containing the given node
182
+ * @param {Node} node - DOM node
183
+ * @returns {Element|null} Format node
184
+ */
185
+ findFormatNode(node) {
186
+ while (node && node !== document.body) {
187
+ if (node.nodeType === Node.ELEMENT_NODE &&
188
+ node.tagName === this.constructor.tagName) {
189
+ return node;
190
+ }
191
+ node = node.parentNode;
192
+ }
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Find all format nodes within a range
198
+ * @param {Range} range - Selection range
199
+ * @returns {Element[]} Array of format nodes
200
+ */
201
+ findFormatNodesInRange(range) {
202
+ const nodes = [];
203
+ const walker = document.createTreeWalker(
204
+ range.commonAncestorContainer,
205
+ NodeFilter.SHOW_ELEMENT,
206
+ {
207
+ acceptNode: (node) => {
208
+ if (node.tagName === this.constructor.tagName &&
209
+ range.intersectsNode(node)) {
210
+ return NodeFilter.FILTER_ACCEPT;
211
+ }
212
+ return NodeFilter.FILTER_SKIP;
213
+ }
214
+ }
215
+ );
216
+
217
+ let node;
218
+ while ((node = walker.nextNode())) {
219
+ nodes.push(node);
220
+ }
221
+
222
+ return nodes;
223
+ }
224
+
225
+ /**
226
+ * Check if inline format is active at current selection
227
+ * @returns {boolean}
228
+ */
229
+ isActive() {
230
+ const selection = window.getSelection();
231
+ if (!selection || !selection.rangeCount) return false;
232
+
233
+ const range = selection.getRangeAt(0);
234
+ let node = range.startContainer;
235
+
236
+ const tagName = this.constructor.tagName;
237
+ const altTags = this.constructor.alternativeTagNames || [];
238
+ const formatName = this.constructor.formatName;
239
+
240
+ // Đặc biệt với một số lệnh hỗ trợ execCommand
241
+ const commandSupported = ['bold', 'italic', 'underline'];
242
+ if (commandSupported.includes(formatName?.toLowerCase())) {
243
+ return queryFormatState(formatName);
244
+ }
245
+
246
+ // Kiểm tra DOM tag
247
+ while (node && node !== document.body) {
248
+ if (
249
+ node.nodeType === Node.ELEMENT_NODE &&
250
+ (node.tagName === tagName ||
251
+ altTags.includes(node.tagName))
252
+ ) {
253
+ return true;
254
+ }
255
+ node = node.parentNode;
256
+ }
257
+
258
+ return false;
259
+ }
260
+
261
+ }
262
+
263
+ /**
264
+ * Block Format - for formats like headers, paragraphs, alignment
265
+ * Handles block-level formatting that affects entire blocks/paragraphs
266
+ */
267
+ export class BlockFormat extends Format {
268
+ /**
269
+ * Create block format element
270
+ * @param {*} value - Format value
271
+ * @returns {HTMLElement}
272
+ */
273
+ static create(value) {
274
+ const node = super.create(value);
275
+ return node;
276
+ }
277
+
278
+ /**
279
+ * Apply block format to selection
280
+ * Converts current block(s) or creates new block
281
+ * @param {*} value - Format value
282
+ */
283
+ apply(value) {
284
+ const selection = window.getSelection();
285
+ if (!selection || !selection.rangeCount) return;
286
+
287
+ const range = selection.getRangeAt(0);
288
+ const blocks = this.getBlockElements(range);
289
+
290
+ if (blocks.length === 0) {
291
+ // No block found - create new one
292
+ this.createBlockAtCursor(range, value);
293
+ } else {
294
+ // Convert existing blocks
295
+ blocks.forEach(block => {
296
+ this.convertBlock(block, value);
297
+ });
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Remove block format from selection
303
+ * Converts blocks back to default (paragraph) or removes formatting
304
+ */
305
+ remove() {
306
+ const selection = window.getSelection();
307
+ if (!selection || !selection.rangeCount) return;
308
+
309
+ const range = selection.getRangeAt(0);
310
+ const blocks = this.getBlockElements(range);
311
+
312
+ blocks.forEach(block => {
313
+ this.removeBlockFormat(block);
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Create new block at cursor position
319
+ * @param {Range} range - Current range
320
+ * @param {*} value - Format value
321
+ */
322
+ createBlockAtCursor(range, value) {
323
+ const blockNode = this.constructor.create(value);
324
+
325
+ // Try to preserve style from existing block if cursor is inside one
326
+ const existingBlock = this.getBlockElement(range.startContainer);
327
+ if (existingBlock && existingBlock.style && existingBlock.style.cssText) {
328
+ blockNode.style.cssText = existingBlock.style.cssText;
329
+ }
330
+
331
+ if (range.collapsed) {
332
+ // No selection - create empty block
333
+ blockNode.appendChild(document.createTextNode(''));
334
+ range.insertNode(blockNode);
335
+
336
+ // Position cursor inside the block
337
+ const newRange = document.createRange();
338
+ newRange.setStart(blockNode, 0);
339
+ newRange.collapse(true);
340
+ const selection = window.getSelection();
341
+ selection.removeAllRanges();
342
+ selection.addRange(newRange);
343
+ } else {
344
+ // Has selection - wrap in block
345
+ const contents = range.extractContents();
346
+ blockNode.appendChild(contents);
347
+ range.insertNode(blockNode);
348
+
349
+ // Select the content in the block
350
+ const newRange = document.createRange();
351
+ newRange.selectNodeContents(blockNode);
352
+ const selection = window.getSelection();
353
+ selection.removeAllRanges();
354
+ selection.addRange(newRange);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Convert existing block to new format
360
+ * @param {Element} block - Block element to convert
361
+ * @param {*} value - Format value
362
+ */
363
+ convertBlock(block, value) {
364
+ const newBlock = this.constructor.create(value);
365
+
366
+ // Copy all child nodes
367
+ while (block.firstChild) {
368
+ newBlock.appendChild(block.firstChild);
369
+ }
370
+
371
+ // Copy relevant attributes
372
+ if (block.className && this.shouldPreserveClass(block.className)) {
373
+ newBlock.className = block.className;
374
+ }
375
+
376
+ // Copy style attributes to preserve formatting like text-align
377
+ if (block.style && block.style.cssText) {
378
+ newBlock.style.cssText = block.style.cssText;
379
+ }
380
+
381
+ // Replace the block
382
+ block.parentNode.replaceChild(newBlock, block);
383
+ }
384
+
385
+ /**
386
+ * Remove block format (convert to paragraph)
387
+ * @param {Element} block - Block element
388
+ */
389
+ removeBlockFormat(block) {
390
+ const paragraph = document.createElement('P');
391
+
392
+ // Move all child nodes to paragraph
393
+ while (block.firstChild) {
394
+ paragraph.appendChild(block.firstChild);
395
+ }
396
+
397
+ // Copy style attributes to preserve formatting like text-align
398
+ if (block.style && block.style.cssText) {
399
+ paragraph.style.cssText = block.style.cssText;
400
+ }
401
+
402
+ // Replace the block
403
+ block.parentNode.replaceChild(paragraph, block);
404
+ }
405
+
406
+ /**
407
+ * Get block elements in range
408
+ * @param {Range} range - Selection range
409
+ * @returns {Element[]} Array of block elements
410
+ */
411
+ getBlockElements(range) {
412
+ const blocks = [];
413
+ let startBlock = this.getBlockElement(range.startContainer);
414
+ let endBlock = this.getBlockElement(range.endContainer);
415
+
416
+ // Nếu endBlock ngay sau startBlock và selection kết thúc ở vị trí 0 của endBlock
417
+ if (startBlock && endBlock && startBlock.nextElementSibling === endBlock) {
418
+ const endAtStartOfEndBlock =
419
+ range.endContainer === endBlock &&
420
+ range.endOffset === 0;
421
+ if (endAtStartOfEndBlock) {
422
+ endBlock = startBlock;
423
+ }
424
+ }
425
+
426
+ if (startBlock === endBlock) {
427
+ if (startBlock) blocks.push(startBlock);
428
+ } else {
429
+ // Multiple blocks
430
+ let current = startBlock;
431
+ while (current && current !== endBlock) {
432
+ if (this.isBlockElement(current)) {
433
+ blocks.push(current);
434
+ }
435
+ current = this.getNextBlockElement(current);
436
+ }
437
+ if (endBlock && this.isBlockElement(endBlock)) {
438
+ blocks.push(endBlock);
439
+ }
440
+ }
441
+
442
+ return blocks.filter((block, index, self) =>
443
+ self.indexOf(block) === index
444
+ );
445
+ }
446
+
447
+ /**
448
+ * Get block element containing node
449
+ * @param {Node} node - DOM node
450
+ * @returns {Element|null} Block element
451
+ */
452
+ getBlockElement(node) {
453
+ while (node && node.nodeType !== Node.ELEMENT_NODE) {
454
+ node = node.parentNode;
455
+ }
456
+
457
+ while (node && node !== document.body) {
458
+ if (this.isBlockElement(node)) {
459
+ return node;
460
+ }
461
+ node = node.parentNode;
462
+ }
463
+
464
+ return null;
465
+ }
466
+
467
+ /**
468
+ * Check if element is a block element
469
+ * @param {Element} element - DOM element
470
+ * @returns {boolean}
471
+ */
472
+ isBlockElement(element) {
473
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
474
+
475
+ const blockTags = [
476
+ 'P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
477
+ 'BLOCKQUOTE', 'PRE', 'UL', 'OL', 'LI', 'SECTION', 'ARTICLE'
478
+ ];
479
+ return blockTags.includes(element.tagName.toUpperCase());
480
+ }
481
+
482
+ /**
483
+ * Get next block element
484
+ * @param {Element} element - Current element
485
+ * @returns {Element|null} Next block element
486
+ */
487
+ getNextBlockElement(element) {
488
+ let next = element.nextElementSibling;
489
+ while (next) {
490
+ if (this.isBlockElement(next)) {
491
+ return next;
492
+ }
493
+ next = next.nextElementSibling;
494
+ }
495
+ return null;
496
+ }
497
+
498
+ /**
499
+ * Check if block format is active at current selection
500
+ * @param {*} value - Optional specific value to check
501
+ * @returns {boolean}
502
+ */
503
+ isActive(value = null) {
504
+ const selection = window.getSelection();
505
+ if (!selection || !selection.rangeCount) return false;
506
+
507
+ const range = selection.getRangeAt(0);
508
+ const block = this.getBlockElement(range.startContainer);
509
+
510
+ if (!block) return false;
511
+
512
+ // Check if block matches our format
513
+ if (block.tagName === this.constructor.tagName) {
514
+ return value ? this.hasValue(block, value) : true;
515
+ }
516
+
517
+ return false;
518
+ }
519
+
520
+ /**
521
+ * Check if block has specific value
522
+ * Override in subclasses for specific value checking
523
+ * @param {Element} block - Block element
524
+ * @param {*} value - Value to check
525
+ * @returns {boolean}
526
+ */
527
+ hasValue(block, value) {
528
+ return true; // Default implementation
529
+ }
530
+
531
+ /**
532
+ * Check if class should be preserved during conversion
533
+ * Override in subclasses for specific class handling
534
+ * @param {string} className - Class name
535
+ * @returns {boolean}
536
+ */
537
+ shouldPreserveClass(className) {
538
+ return false; // Default: don't preserve classes
539
+ }
540
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Base Module class - Inspired by Quill's architecture
3
+ * All editor modules should extend this class
4
+ */
5
+ export default class Module {
6
+ static DEFAULTS = {};
7
+
8
+ constructor(editor, options = {}) {
9
+ this.editor = editor;
10
+ this.options = { ...this.constructor.DEFAULTS, ...options };
11
+ this.events = new Map();
12
+ }
13
+
14
+ /**
15
+ * Add event listener
16
+ * @param {string} event - Event name
17
+ * @param {function} handler - Event handler
18
+ */
19
+ on(event, handler) {
20
+ if (!this.events.has(event)) {
21
+ this.events.set(event, []);
22
+ }
23
+ this.events.get(event).push(handler);
24
+ }
25
+
26
+ /**
27
+ * Remove event listener
28
+ * @param {string} event - Event name
29
+ * @param {function} handler - Event handler
30
+ */
31
+ off(event, handler) {
32
+ if (this.events.has(event)) {
33
+ const handlers = this.events.get(event);
34
+ const index = handlers.indexOf(handler);
35
+ if (index > -1) {
36
+ handlers.splice(index, 1);
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Emit event
43
+ * @param {string} event - Event name
44
+ * @param {*} data - Event data
45
+ */
46
+ emit(event, data) {
47
+ if (this.events.has(event)) {
48
+ this.events.get(event).forEach(handler => {
49
+ try {
50
+ handler(data);
51
+ } catch (error) {
52
+ console.error(`Error in event handler for ${event}:`, error);
53
+ }
54
+ });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Called when module is being destroyed
60
+ * Override this method to cleanup resources
61
+ */
62
+ destroy() {
63
+ this.events.clear();
64
+ }
65
+
66
+ /**
67
+ * Called when editor content changes
68
+ * Override this method to respond to content changes
69
+ */
70
+ onContentChange() {
71
+ // Override in subclasses
72
+ }
73
+
74
+ /**
75
+ * Called when selection changes
76
+ * Override this method to respond to selection changes
77
+ */
78
+ onSelectionChange(range) {
79
+ // Override in subclasses
80
+ }
81
+ }