@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,347 @@
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 = 'image/*';
169
+ input.style.display = 'none';
170
+ input.addEventListener('change', async () => {
171
+ const file = input.files && input.files[0];
172
+ if (file) {
173
+ try {
174
+ const src = await Image.handleFileUpload(file);
175
+ editor.focus();
176
+ const sel = window.getSelection();
177
+ // Restore the caret we had before the file dialog stole focus. If we
178
+ // never had one (clicking the toolbar can collapse the selection),
179
+ // drop the caret at the end of the editor so insertion still works.
180
+ if (savedRange) {
181
+ sel.removeAllRanges();
182
+ sel.addRange(savedRange);
183
+ } else if (!sel.rangeCount || !editor.editor.contains(sel.anchorNode)) {
184
+ const r = document.createRange();
185
+ r.selectNodeContents(editor.editor);
186
+ r.collapse(false);
187
+ sel.removeAllRanges();
188
+ sel.addRange(r);
189
+ }
190
+ Image.insertImageAtCurrentPosition(src, file.name || '', this.editorId);
191
+ } catch (err) {
192
+ console.warn('Image upload failed:', err.message);
193
+ }
194
+ }
195
+ input.remove();
196
+ });
197
+ document.body.appendChild(input);
198
+ input.click();
199
+ }
200
+
201
+ /**
202
+ * Remove image formatting
203
+ */
204
+ remove() {
205
+ const selection = window.getSelection();
206
+ if (!selection || !selection.rangeCount) return;
207
+
208
+ const range = selection.getRangeAt(0);
209
+ const imageElement = this.getImageElement(range);
210
+
211
+ if (imageElement) {
212
+ imageElement.remove();
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Toggle image — opens the native file picker.
218
+ */
219
+ toggle() {
220
+ this.openFilePicker();
221
+ }
222
+
223
+ /**
224
+ * Check if image formatting is active
225
+ */
226
+ isActive() {
227
+ const selection = window.getSelection();
228
+ if (!selection || !selection.rangeCount) return false;
229
+
230
+ const range = selection.getRangeAt(0);
231
+ const imageElement = this.getImageElement(range);
232
+
233
+ return imageElement !== null;
234
+ }
235
+
236
+ /**
237
+ * Get image element from selection
238
+ * @param {Range} range - Selection range
239
+ * @returns {HTMLElement|null}
240
+ */
241
+ getImageElement(range) {
242
+ let node = range.commonAncestorContainer;
243
+
244
+ // If it's a text node, get its parent
245
+ if (node.nodeType === Node.TEXT_NODE) {
246
+ node = node.parentNode;
247
+ }
248
+
249
+ // Check if current node is an image
250
+ if (node.tagName === 'IMG' && node.classList && node.classList.contains('inserted-image')) {
251
+ return node;
252
+ }
253
+
254
+ // Check if selection contains an image
255
+ const imageInSelection = range.cloneContents().querySelector('.inserted-image');
256
+ if (imageInSelection) {
257
+ return imageInSelection;
258
+ }
259
+
260
+ return null;
261
+ }
262
+
263
+ /**
264
+ * Handle file upload
265
+ * @param {File} file - Image file
266
+ * @returns {Promise<string>} - Promise that resolves to image URL
267
+ */
268
+ static async handleFileUpload(file) {
269
+ return new Promise((resolve, reject) => {
270
+ if (!file || !file.type.startsWith('image/')) {
271
+ reject(new Error('Please select a valid image file'));
272
+ return;
273
+ }
274
+
275
+ const reader = new FileReader();
276
+ reader.onload = (e) => {
277
+ resolve(e.target.result);
278
+ };
279
+ reader.onerror = () => {
280
+ reject(new Error('Failed to read file'));
281
+ };
282
+ reader.readAsDataURL(file);
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Validate image URL
288
+ * @param {string} url - Image URL
289
+ * @returns {Promise<boolean>} - Promise that resolves to validation result
290
+ */
291
+ static validateImageUrl(url) {
292
+ return new Promise((resolve) => {
293
+ // Check if it's a valid image URL format
294
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
295
+ const hasValidExtension = imageExtensions.some(ext =>
296
+ url.toLowerCase().includes(`.${ext}`)
297
+ );
298
+
299
+ // Check if it's a data URL
300
+ if (url.startsWith('data:image/')) {
301
+ resolve(true);
302
+ return;
303
+ }
304
+
305
+ // Check if it's a valid HTTP(S) URL
306
+ if (!/^https?:\/\//.test(url)) {
307
+ resolve(false);
308
+ return;
309
+ }
310
+
311
+ // If it has a valid extension, assume it's valid
312
+ if (hasValidExtension) {
313
+ resolve(true);
314
+ return;
315
+ }
316
+
317
+ // Try to load the image (fallback)
318
+ const img = new Image();
319
+ img.onload = () => {
320
+ resolve(true);
321
+ };
322
+ img.onerror = () => {
323
+ // If loading fails, but URL looks like an image, still allow it
324
+ // This handles cases where CORS blocks loading but the URL is valid
325
+ if (url.includes('imgur.com') || url.includes('drive.google.com') || hasValidExtension) {
326
+ resolve(true);
327
+ } else {
328
+ resolve(false);
329
+ }
330
+ };
331
+
332
+ // Set timeout to avoid hanging
333
+ setTimeout(() => {
334
+ // If no response after 5 seconds, still allow if URL looks valid
335
+ if (hasValidExtension || url.includes('imgur.com') || url.includes('drive.google.com')) {
336
+ resolve(true);
337
+ } else {
338
+ resolve(false);
339
+ }
340
+ }, 5000);
341
+
342
+ img.src = url;
343
+ });
344
+ }
345
+ }
346
+
347
+ export default Image;