@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,183 @@
1
+ import { InlineFormat } from '../core/format.js';
2
+ import ColorPicker from '../ui/color-picker.js';
3
+ import { saveBeforeFormat } from '../utils/history-helper.js';
4
+ import Editor from '../core/editor.js';
5
+ import { execFormat, setStyleWithCSS } from '../utils/exec-command.js';
6
+
7
+ /**
8
+ * Color Format - Handles text color formatting
9
+ */
10
+ class Color extends InlineFormat {
11
+ static formatName = 'color';
12
+ static tagName = 'SPAN';
13
+ static attribute = 'color';
14
+
15
+ // Selection saved when the picker opens, restored before applying — so the
16
+ // colour still lands on the right text after a tap clears the live selection
17
+ // (mobile/touch) or focus moves to the picker.
18
+ static savedRanges = new Map();
19
+
20
+ constructor() {
21
+ super();
22
+
23
+ // Get current editor instance
24
+ const currentEditor = Editor.getCurrentInstance();
25
+ if (!currentEditor) {
26
+ console.warn('No editor instance found for Color format');
27
+ return;
28
+ }
29
+
30
+ this.editorId = currentEditor.instanceId;
31
+
32
+ // Check if this editor already has a color picker instance
33
+ let colorPicker = currentEditor.getPopupInstance('color');
34
+
35
+ if (!colorPicker) {
36
+ // Create new color picker instance for this editor
37
+ const editorId = this.editorId;
38
+ colorPicker = new ColorPicker({
39
+ onColorSelect: (color) => {
40
+ Color.applyColorToCurrentSelection(color, editorId);
41
+ },
42
+ editor: Editor.getCurrentInstance()
43
+ });
44
+
45
+ // Store popup instance in editor
46
+ currentEditor.setPopupInstance('color', colorPicker);
47
+ }
48
+
49
+ this.colorPicker = colorPicker;
50
+ }
51
+
52
+ /**
53
+ * Static method to apply color to current selection
54
+ */
55
+ static applyColorToCurrentSelection(color, editorId = null) {
56
+ const selection = window.getSelection();
57
+ // Restore the selection captured when the picker opened (a tap on the
58
+ // picker may have collapsed the live selection, especially on mobile).
59
+ const saved = editorId != null ? Color.savedRanges.get(editorId) : null;
60
+ if (saved) {
61
+ selection.removeAllRanges();
62
+ selection.addRange(saved);
63
+ }
64
+ if (editorId != null) Color.savedRanges.delete(editorId);
65
+
66
+ if (!selection || !selection.rangeCount || selection.isCollapsed) return;
67
+
68
+ // Save state before applying format
69
+ saveBeforeFormat();
70
+
71
+ setStyleWithCSS(true);
72
+ // 'transparent' is the picker's "reset to default" entry — clear the colour
73
+ // back to the editor default rather than painting an explicit colour.
74
+ execFormat('foreColor', color === 'transparent' ? 'inherit' : color);
75
+
76
+ // Refresh toolbar state (active highlight + swatch) and notify listeners.
77
+ setTimeout(() => {
78
+ const currentEditor = Editor.getCurrentInstance();
79
+ if (currentEditor) {
80
+ if (typeof currentEditor.updateToolbarButtonStates === 'function') {
81
+ currentEditor.updateToolbarButtonStates();
82
+ }
83
+ if (typeof currentEditor.onContentChange === 'function') {
84
+ currentEditor.onContentChange();
85
+ }
86
+ }
87
+ }, 0);
88
+ }
89
+
90
+ /**
91
+ * Toggle color formatting - shows/hides color picker
92
+ */
93
+ toggle() {
94
+ if (this.colorPicker.isVisible) {
95
+ this.colorPicker.hide();
96
+ } else {
97
+ this.showColorPicker();
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Show color picker positioned relative to color button on toolbar
103
+ */
104
+ showColorPicker() {
105
+ // Find color button in the current editor's toolbar
106
+ const editor = Editor.getInstanceById(this.editorId);
107
+ if (!editor) return;
108
+
109
+ // Capture the current selection so we can apply the colour to it even if a
110
+ // tap on the picker clears the live selection. Fall back to the editor's
111
+ // last non-collapsed range (mobile clears the selection on touchstart).
112
+ const sel = window.getSelection();
113
+ if (sel && sel.rangeCount && !sel.isCollapsed) {
114
+ Color.savedRanges.set(this.editorId, sel.getRangeAt(0).cloneRange());
115
+ } else if (editor._lastRange) {
116
+ Color.savedRanges.set(this.editorId, editor._lastRange.cloneRange());
117
+ }
118
+
119
+ const toolbar = editor.getModule('toolbar');
120
+ let colorButton = null;
121
+
122
+ if (toolbar) {
123
+ colorButton = toolbar.getButton('color');
124
+ }
125
+
126
+ // Fallback: find button by class in the current editor's toolbar
127
+ if (!colorButton) {
128
+ const toolbarContainer = toolbar?.getContainer();
129
+ if (toolbarContainer) {
130
+ colorButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.color-btn');
131
+ }
132
+ }
133
+
134
+ // Final fallback: find any color button in the current editor's wrapper
135
+ if (!colorButton) {
136
+ colorButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.color-btn');
137
+ }
138
+
139
+ if (!colorButton) {
140
+ console.warn('Color button not found for editor:', this.editorId);
141
+ return;
142
+ }
143
+
144
+ this.colorPicker.show(colorButton);
145
+ }
146
+
147
+ /**
148
+ * Check if color formatting is active in current selection
149
+ */
150
+ isActive() {
151
+ return !!Color.getCurrentColor();
152
+ }
153
+
154
+ /**
155
+ * Return the explicit text colour applied at the current selection, or null
156
+ * when the text uses the editor's default colour. We look for an EXPLICIT
157
+ * inline colour (the `<span style="color:…">` / `<font color>` that the
158
+ * editor inserts) rather than comparing the computed colour to a hardcoded
159
+ * default — the default depends on the active theme, so a hardcoded compare
160
+ * made the button look permanently "active".
161
+ */
162
+ static getCurrentColor() {
163
+ const selection = window.getSelection();
164
+ if (!selection || !selection.rangeCount) return null;
165
+
166
+ let node = selection.getRangeAt(0).startContainer;
167
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
168
+
169
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
170
+ if (node.classList && node.classList.contains('rich-editor-area')) break;
171
+ if (node.style && node.style.color && node.style.color !== 'inherit') {
172
+ return node.style.color;
173
+ }
174
+ if (node.tagName === 'FONT' && node.getAttribute('color')) {
175
+ return node.getAttribute('color');
176
+ }
177
+ node = node.parentNode;
178
+ }
179
+ return null;
180
+ }
181
+ }
182
+
183
+ export default Color;
@@ -0,0 +1,282 @@
1
+ import { InlineFormat } from '../core/format.js';
2
+ import EmojiPicker from '../ui/emoji-picker.js';
3
+ import Editor from '../core/editor.js';
4
+
5
+ /**
6
+ * Emoji Format - Handles emoji insertion
7
+ * Now supports multiple editor instances with separate popup instances
8
+ */
9
+ class Emoji extends InlineFormat {
10
+ static formatName = 'emoji';
11
+ static tagName = 'SPAN';
12
+ static className = 'emoji';
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 Emoji format');
21
+ return;
22
+ }
23
+
24
+ this.editorId = currentEditor.instanceId;
25
+
26
+ // Check if this editor already has an emoji picker instance
27
+ let emojiPicker = currentEditor.getPopupInstance('emoji');
28
+
29
+ if (!emojiPicker) {
30
+ // Create new emoji picker instance for this editor
31
+ emojiPicker = new EmojiPicker({
32
+ onEmojiSelect: (emoji) => {
33
+ Emoji.insertEmojiAtCurrentPosition(emoji, this.editorId);
34
+ },
35
+ editor: currentEditor,
36
+ editorId: this.editorId
37
+ });
38
+
39
+ // Store popup instance in editor
40
+ currentEditor.setPopupInstance('emoji', emojiPicker);
41
+ }
42
+
43
+ this.emojiPicker = emojiPicker;
44
+ }
45
+
46
+ /**
47
+ * Create a new Emoji format instance for a specific editor
48
+ * @param {string} editorId - Editor instance ID
49
+ * @returns {Emoji} Emoji format instance
50
+ */
51
+ static createForEditor(editorId) {
52
+ const editor = Editor.getInstanceById(editorId);
53
+ if (!editor) {
54
+ console.warn('No editor instance found for ID:', editorId);
55
+ return null;
56
+ }
57
+
58
+ // Temporarily set as current instance
59
+ const originalCurrent = Editor.currentInstance;
60
+ Editor.currentInstance = editor;
61
+
62
+ // Create format instance
63
+ const format = new Emoji();
64
+
65
+ // Restore original current instance
66
+ Editor.currentInstance = originalCurrent;
67
+
68
+ return format;
69
+ }
70
+
71
+ /**
72
+ * Create emoji element
73
+ * @param {string} value - Emoji character
74
+ * @returns {HTMLElement}
75
+ */
76
+ static create(value) {
77
+ const span = document.createElement('SPAN');
78
+ span.className = 'emoji';
79
+ span.textContent = value;
80
+ span.setAttribute('data-emoji', value);
81
+ return span;
82
+ }
83
+
84
+ /**
85
+ * Insert emoji at current cursor position
86
+ * @param {string} emoji - Emoji character to insert
87
+ * @param {string} editorId - Editor instance ID
88
+ */
89
+ static insertEmojiAtCurrentPosition(emoji, editorId = null) {
90
+ // Get the correct editor instance
91
+ let editor = null;
92
+ if (editorId) {
93
+ editor = Editor.getInstanceById(editorId);
94
+ } else {
95
+ editor = Editor.getCurrentInstance();
96
+ }
97
+
98
+ if (!editor) {
99
+ console.warn('No editor instance found for emoji insertion');
100
+ return;
101
+ }
102
+
103
+ const selection = window.getSelection();
104
+ if (!selection || !selection.rangeCount) return;
105
+
106
+ try {
107
+ const range = selection.getRangeAt(0);
108
+
109
+ // Check if cursor is inside an existing emoji span
110
+ let currentNode = range.startContainer;
111
+ let emojiParent = null;
112
+
113
+ // If cursor is in a text node, check its parent
114
+ if (currentNode.nodeType === Node.TEXT_NODE) {
115
+ currentNode = currentNode.parentNode;
116
+ }
117
+
118
+ // Find if we're inside an emoji span
119
+ while (currentNode && currentNode !== editor.element) {
120
+ if (currentNode.classList && currentNode.classList.contains('emoji')) {
121
+ emojiParent = currentNode;
122
+ break;
123
+ }
124
+ currentNode = currentNode.parentNode;
125
+ }
126
+
127
+ // If cursor is inside an emoji span, move it outside
128
+ if (emojiParent) {
129
+ // Move cursor after the emoji span
130
+ range.setStartAfter(emojiParent);
131
+ range.collapse(true);
132
+ selection.removeAllRanges();
133
+ selection.addRange(range);
134
+ }
135
+
136
+ // Create emoji element
137
+ const emojiElement = Emoji.create(emoji);
138
+
139
+ // Insert emoji at cursor position
140
+ range.deleteContents();
141
+ range.insertNode(emojiElement);
142
+
143
+ // Create a zero-width space character after the emoji
144
+ const zeroWidthSpace = document.createTextNode('\u200B'); // Zero-width space
145
+
146
+ // Insert zero-width space after emoji
147
+ range.setStartAfter(emojiElement);
148
+ range.insertNode(zeroWidthSpace);
149
+
150
+ // Position cursor after the zero-width space
151
+ range.setStartAfter(zeroWidthSpace);
152
+ range.collapse(true);
153
+ selection.removeAllRanges();
154
+ selection.addRange(range);
155
+
156
+ // Trigger content change event
157
+ if (editor && typeof editor.onContentChange === 'function') {
158
+ editor.onContentChange();
159
+ }
160
+
161
+ } catch (error) {
162
+ console.error('Error inserting emoji:', error);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Apply emoji formatting - shows emoji picker
168
+ */
169
+ apply(value) {
170
+ if (value) {
171
+ Emoji.insertEmojiAtCurrentPosition(value, this.editorId);
172
+ } else {
173
+ this.showEmojiPicker();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Remove emoji formatting
179
+ */
180
+ remove() {
181
+ const selection = window.getSelection();
182
+ if (!selection || !selection.rangeCount) return;
183
+
184
+ const range = selection.getRangeAt(0);
185
+ const emojiElement = this.getEmojiElement(range);
186
+
187
+ if (emojiElement) {
188
+ // Replace emoji element with its text content
189
+ const textNode = document.createTextNode(emojiElement.textContent);
190
+ emojiElement.parentNode.replaceChild(textNode, emojiElement);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Toggle emoji formatting - shows emoji picker
196
+ */
197
+ toggle() {
198
+ if (this.emojiPicker.isVisible) {
199
+ this.emojiPicker.hide();
200
+ } else {
201
+ this.showEmojiPicker();
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Show emoji picker popup
207
+ */
208
+ showEmojiPicker() {
209
+ // Find emoji button in the current editor's toolbar
210
+ const editor = Editor.getInstanceById(this.editorId);
211
+ if (!editor) return;
212
+
213
+ const toolbar = editor.getModule('toolbar');
214
+ let emojiButton = null;
215
+
216
+ if (toolbar) {
217
+ emojiButton = toolbar.getButton('emoji');
218
+ }
219
+
220
+ // Fallback: find button by class in the current editor's toolbar
221
+ if (!emojiButton) {
222
+ const toolbarContainer = toolbar?.getContainer();
223
+ if (toolbarContainer) {
224
+ emojiButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.emoji-btn');
225
+ }
226
+ }
227
+
228
+ // Final fallback: find any emoji button in the current editor's wrapper
229
+ if (!emojiButton) {
230
+ emojiButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.emoji-btn');
231
+ }
232
+
233
+ if (!emojiButton) {
234
+ console.warn('Emoji button not found for editor:', this.editorId);
235
+ return;
236
+ }
237
+
238
+ this.emojiPicker.show(emojiButton);
239
+ }
240
+
241
+ /**
242
+ * Check if emoji formatting is active
243
+ */
244
+ isActive() {
245
+ const selection = window.getSelection();
246
+ if (!selection || !selection.rangeCount) return false;
247
+
248
+ const range = selection.getRangeAt(0);
249
+ const emojiElement = this.getEmojiElement(range);
250
+
251
+ return emojiElement !== null;
252
+ }
253
+
254
+ /**
255
+ * Get emoji element from selection
256
+ * @param {Range} range - Selection range
257
+ * @returns {HTMLElement|null}
258
+ */
259
+ getEmojiElement(range) {
260
+ let node = range.commonAncestorContainer;
261
+
262
+ // If it's a text node, get its parent
263
+ if (node.nodeType === Node.TEXT_NODE) {
264
+ node = node.parentNode;
265
+ }
266
+
267
+ // Check if current node is an emoji
268
+ if (node.classList && node.classList.contains('emoji')) {
269
+ return node;
270
+ }
271
+
272
+ // Check if selection contains an emoji
273
+ const emojiInSelection = range.cloneContents().querySelector('.emoji');
274
+ if (emojiInSelection) {
275
+ return emojiInSelection;
276
+ }
277
+
278
+ return null;
279
+ }
280
+ }
281
+
282
+ export default Emoji;