@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,341 @@
1
+ import { InlineFormat } from '../core/format.js';
2
+ import Editor from '../core/editor.js';
3
+ import { isSafeUrl } from '../utils/sanitize.js';
4
+
5
+ /**
6
+ * Image Format - Handles image insertion
7
+ * Now supports multiple editor instances with separate popup instances
8
+ */
9
+ class Image extends InlineFormat {
10
+ static formatName = 'image';
11
+ static tagName = 'IMG';
12
+ static className = 'inserted-image';
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 Image format');
21
+ return;
22
+ }
23
+
24
+ this.editorId = currentEditor.instanceId;
25
+ }
26
+
27
+ /**
28
+ * Create a new Image format instance for a specific editor
29
+ * @param {string} editorId - Editor instance ID
30
+ * @returns {Image} Image format instance
31
+ */
32
+ static createForEditor(editorId) {
33
+ const editor = Editor.getInstanceById(editorId);
34
+ if (!editor) {
35
+ console.warn('No editor instance found for ID:', editorId);
36
+ return null;
37
+ }
38
+
39
+ // Temporarily set as current instance
40
+ const originalCurrent = Editor.currentInstance;
41
+ Editor.currentInstance = editor;
42
+
43
+ // Create format instance
44
+ const format = new Image();
45
+
46
+ // Restore original current instance
47
+ Editor.currentInstance = originalCurrent;
48
+
49
+ return format;
50
+ }
51
+
52
+ /**
53
+ * Create image element
54
+ * @param {string} src - Image source URL
55
+ * @param {string} alt - Alt text
56
+ * @returns {HTMLElement}
57
+ */
58
+ static create(src, alt = '') {
59
+ // Allow http(s)/relative URLs and raster data: image URIs; reject the rest.
60
+ if (!isSafeUrl(src, { allowDataImage: true })) {
61
+ console.warn('Blocked unsafe image URL:', src);
62
+ return null;
63
+ }
64
+ const img = document.createElement('IMG');
65
+ img.src = src;
66
+ img.alt = alt || 'Inserted image';
67
+ img.className = 'inserted-image';
68
+ img.style.maxWidth = '100%';
69
+ img.style.height = 'auto';
70
+ img.setAttribute('contenteditable', 'false');
71
+ return img;
72
+ }
73
+
74
+ /**
75
+ * Insert image at current cursor position
76
+ * @param {string} src - Image source URL
77
+ * @param {string} alt - Alt text
78
+ * @param {string} editorId - Editor instance ID
79
+ */
80
+ static insertImageAtCurrentPosition(src, alt = '', editorId = null) {
81
+ // Get the correct editor instance
82
+ let editor = null;
83
+ if (editorId) {
84
+ editor = Editor.getInstanceById(editorId);
85
+ } else {
86
+ editor = Editor.getCurrentInstance();
87
+ }
88
+
89
+ if (!editor) {
90
+ console.warn('No editor instance found for image insertion');
91
+ return;
92
+ }
93
+
94
+ const selection = window.getSelection();
95
+ if (!selection) return;
96
+ // No caret (or caret outside this editor) → append at the end of the editor.
97
+ if (!selection.rangeCount || !editor.editor.contains(selection.anchorNode)) {
98
+ const r = document.createRange();
99
+ r.selectNodeContents(editor.editor);
100
+ r.collapse(false);
101
+ selection.removeAllRanges();
102
+ selection.addRange(r);
103
+ }
104
+
105
+ try {
106
+ const range = selection.getRangeAt(0);
107
+ // Create image element
108
+ const imageElement = Image.create(src, alt);
109
+ // Abort if the URL was rejected as unsafe
110
+ if (!imageElement) return;
111
+ // Insert image at cursor position
112
+ range.deleteContents();
113
+ range.insertNode(imageElement);
114
+ // Add a space after the image for easier editing
115
+ const spaceNode = document.createTextNode(' ');
116
+ range.setStartAfter(imageElement);
117
+ range.insertNode(spaceNode);
118
+ // Position cursor after the space
119
+ range.setStartAfter(spaceNode);
120
+ range.collapse(true);
121
+ selection.removeAllRanges();
122
+ selection.addRange(range);
123
+
124
+ // Trigger content change event
125
+ if (editor && typeof editor.onContentChange === 'function') {
126
+ editor.onContentChange();
127
+ }
128
+ } catch (error) {
129
+ console.error('Error inserting image:', error);
130
+ }
131
+
132
+ // Trigger content change after applying format
133
+ setTimeout(() => {
134
+ const currentEditor = Editor.getCurrentInstance();
135
+ if (currentEditor && typeof currentEditor.onContentChange === 'function') {
136
+ currentEditor.onContentChange();
137
+ }
138
+ }, 0);
139
+ }
140
+
141
+ /**
142
+ * Apply image formatting — insert a known src, or open the file picker.
143
+ */
144
+ apply(src, alt) {
145
+ if (src) {
146
+ Image.insertImageAtCurrentPosition(src, alt, this.editorId);
147
+ } else {
148
+ this.openFilePicker();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Open the native file browser, then insert the chosen image straight into
154
+ * the editor (visible immediately). The selection is captured before the
155
+ * dialog steals focus and restored before insertion.
156
+ */
157
+ openFilePicker() {
158
+ const editor = Editor.getInstanceById(this.editorId);
159
+ if (!editor) return;
160
+
161
+ const selection = window.getSelection();
162
+ const savedRange = selection && selection.rangeCount
163
+ ? selection.getRangeAt(0).cloneRange()
164
+ : null;
165
+
166
+ const input = document.createElement('input');
167
+ input.type = 'file';
168
+ input.accept = (editor.options.image && editor.options.image.accept) || 'image/*';
169
+ input.style.display = 'none';
170
+ input.addEventListener('change', () => {
171
+ const file = input.files && input.files[0];
172
+ if (file) {
173
+ // Restore the caret captured before the file dialog stole focus.
174
+ editor.focus();
175
+ const sel = window.getSelection();
176
+ if (savedRange) {
177
+ sel.removeAllRanges();
178
+ sel.addRange(savedRange);
179
+ } else if (!sel.rangeCount || !editor.editor.contains(sel.anchorNode)) {
180
+ const r = document.createRange();
181
+ r.selectNodeContents(editor.editor);
182
+ r.collapse(false);
183
+ sel.removeAllRanges();
184
+ sel.addRange(r);
185
+ }
186
+ // Single insertion path → honours the image.upload hook + validation.
187
+ editor.insertImageFile(file);
188
+ }
189
+ input.remove();
190
+ });
191
+ document.body.appendChild(input);
192
+ input.click();
193
+ }
194
+
195
+ /**
196
+ * Remove image formatting
197
+ */
198
+ remove() {
199
+ const selection = window.getSelection();
200
+ if (!selection || !selection.rangeCount) return;
201
+
202
+ const range = selection.getRangeAt(0);
203
+ const imageElement = this.getImageElement(range);
204
+
205
+ if (imageElement) {
206
+ imageElement.remove();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Toggle image — opens the native file picker.
212
+ */
213
+ toggle() {
214
+ this.openFilePicker();
215
+ }
216
+
217
+ /**
218
+ * Check if image formatting is active
219
+ */
220
+ isActive() {
221
+ const selection = window.getSelection();
222
+ if (!selection || !selection.rangeCount) return false;
223
+
224
+ const range = selection.getRangeAt(0);
225
+ const imageElement = this.getImageElement(range);
226
+
227
+ return imageElement !== null;
228
+ }
229
+
230
+ /**
231
+ * Get image element from selection
232
+ * @param {Range} range - Selection range
233
+ * @returns {HTMLElement|null}
234
+ */
235
+ getImageElement(range) {
236
+ let node = range.commonAncestorContainer;
237
+
238
+ // If it's a text node, get its parent
239
+ if (node.nodeType === Node.TEXT_NODE) {
240
+ node = node.parentNode;
241
+ }
242
+
243
+ // Check if current node is an image
244
+ if (node.tagName === 'IMG' && node.classList && node.classList.contains('inserted-image')) {
245
+ return node;
246
+ }
247
+
248
+ // Check if selection contains an image
249
+ const imageInSelection = range.cloneContents().querySelector('.inserted-image');
250
+ if (imageInSelection) {
251
+ return imageInSelection;
252
+ }
253
+
254
+ return null;
255
+ }
256
+
257
+ /**
258
+ * Handle file upload
259
+ * @param {File} file - Image file
260
+ * @returns {Promise<string>} - Promise that resolves to image URL
261
+ */
262
+ static async handleFileUpload(file) {
263
+ return new Promise((resolve, reject) => {
264
+ if (!file || !file.type.startsWith('image/')) {
265
+ reject(new Error('Please select a valid image file'));
266
+ return;
267
+ }
268
+
269
+ const reader = new FileReader();
270
+ reader.onload = (e) => {
271
+ resolve(e.target.result);
272
+ };
273
+ reader.onerror = () => {
274
+ reject(new Error('Failed to read file'));
275
+ };
276
+ reader.readAsDataURL(file);
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Validate image URL
282
+ * @param {string} url - Image URL
283
+ * @returns {Promise<boolean>} - Promise that resolves to validation result
284
+ */
285
+ static validateImageUrl(url) {
286
+ return new Promise((resolve) => {
287
+ // Check if it's a valid image URL format
288
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
289
+ const hasValidExtension = imageExtensions.some(ext =>
290
+ url.toLowerCase().includes(`.${ext}`)
291
+ );
292
+
293
+ // Check if it's a data URL
294
+ if (url.startsWith('data:image/')) {
295
+ resolve(true);
296
+ return;
297
+ }
298
+
299
+ // Check if it's a valid HTTP(S) URL
300
+ if (!/^https?:\/\//.test(url)) {
301
+ resolve(false);
302
+ return;
303
+ }
304
+
305
+ // If it has a valid extension, assume it's valid
306
+ if (hasValidExtension) {
307
+ resolve(true);
308
+ return;
309
+ }
310
+
311
+ // Try to load the image (fallback)
312
+ const img = new Image();
313
+ img.onload = () => {
314
+ resolve(true);
315
+ };
316
+ img.onerror = () => {
317
+ // If loading fails, but URL looks like an image, still allow it
318
+ // This handles cases where CORS blocks loading but the URL is valid
319
+ if (url.includes('imgur.com') || url.includes('drive.google.com') || hasValidExtension) {
320
+ resolve(true);
321
+ } else {
322
+ resolve(false);
323
+ }
324
+ };
325
+
326
+ // Set timeout to avoid hanging
327
+ setTimeout(() => {
328
+ // If no response after 5 seconds, still allow if URL looks valid
329
+ if (hasValidExtension || url.includes('imgur.com') || url.includes('drive.google.com')) {
330
+ resolve(true);
331
+ } else {
332
+ resolve(false);
333
+ }
334
+ }, 5000);
335
+
336
+ img.src = url;
337
+ });
338
+ }
339
+ }
340
+
341
+ export default Image;