@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,381 @@
1
+ import { InlineFormat } from '../core/format.js';
2
+ import VideoPopup from '../ui/video-popup.js';
3
+ import Editor from '../core/editor.js';
4
+ import { isSafeUrl } from '../utils/sanitize.js';
5
+
6
+ /**
7
+ * Video Format - Handles video insertion
8
+ * Now supports multiple editor instances with separate popup instances
9
+ */
10
+ class Video extends InlineFormat {
11
+ static formatName = 'video';
12
+ static tagName = 'VIDEO';
13
+ static className = 'inserted-video';
14
+
15
+ constructor() {
16
+ super();
17
+
18
+ // Get current editor instance
19
+ const currentEditor = Editor.getCurrentInstance();
20
+ if (!currentEditor) {
21
+ console.warn('No editor instance found for Video format');
22
+ return;
23
+ }
24
+
25
+ this.editorId = currentEditor.instanceId;
26
+
27
+ // Check if this editor already has a video popup instance
28
+ let videoPopup = currentEditor.getPopupInstance('video');
29
+
30
+ if (!videoPopup) {
31
+ // Create new video popup instance for this editor
32
+ videoPopup = new VideoPopup({
33
+ onVideoInsert: (src) => {
34
+ Video.insertVideoAtCurrentPosition(src, this.editorId);
35
+ },
36
+ editor: currentEditor,
37
+ editorId: this.editorId
38
+ });
39
+
40
+ // Store popup instance in editor
41
+ currentEditor.setPopupInstance('video', videoPopup);
42
+ }
43
+
44
+ this.videoPopup = videoPopup;
45
+ }
46
+
47
+ /**
48
+ * Create a new Video format instance for a specific editor
49
+ * @param {string} editorId - Editor instance ID
50
+ * @returns {Video} Video format instance
51
+ */
52
+ static createForEditor(editorId) {
53
+ const editor = Editor.getInstanceById(editorId);
54
+ if (!editor) {
55
+ console.warn('No editor instance found for ID:', editorId);
56
+ return null;
57
+ }
58
+
59
+ // Temporarily set as current instance
60
+ const originalCurrent = Editor.currentInstance;
61
+ Editor.currentInstance = editor;
62
+
63
+ // Create format instance
64
+ const format = new Video();
65
+
66
+ // Restore original current instance
67
+ Editor.currentInstance = originalCurrent;
68
+
69
+ return format;
70
+ }
71
+
72
+ /**
73
+ * Create video element
74
+ * @param {string} src - Video source URL
75
+ * @returns {HTMLElement}
76
+ */
77
+ static create(src) {
78
+ // Check if it's a YouTube URL
79
+ if (Video.isYouTubeUrl(src)) {
80
+ return Video.createYouTubeEmbed(src);
81
+ }
82
+
83
+ // Reject unsafe URL schemes for direct (non-YouTube) video sources.
84
+ if (!isSafeUrl(src)) {
85
+ console.warn('Blocked unsafe video URL:', src);
86
+ return null;
87
+ }
88
+
89
+ // Create regular video element for direct video URLs
90
+ const video = document.createElement('VIDEO');
91
+ video.src = src;
92
+ video.className = 'inserted-video';
93
+ video.controls = true;
94
+ video.style.maxWidth = '100%';
95
+ video.style.height = 'auto';
96
+ video.setAttribute('contenteditable', 'false');
97
+ return video;
98
+ }
99
+
100
+ /**
101
+ * Create YouTube embedded iframe
102
+ * @param {string} url - YouTube URL
103
+ * @returns {HTMLElement}
104
+ */
105
+ static createYouTubeEmbed(url) {
106
+ const videoId = Video.getYouTubeVideoId(url);
107
+ if (!videoId) {
108
+ throw new Error('Invalid YouTube URL');
109
+ }
110
+
111
+ const iframe = document.createElement('IFRAME');
112
+ iframe.src = `https://www.youtube.com/embed/${videoId}`;
113
+ iframe.className = 'inserted-video youtube-video';
114
+ iframe.width = '560';
115
+ iframe.height = '315';
116
+ iframe.style.maxWidth = '100%';
117
+ iframe.style.width = '560px'; // Set explicit width
118
+ iframe.style.height = '315px'; // Set explicit height
119
+ iframe.style.position = 'relative'; // Add position relative
120
+ iframe.style.display = 'block'; // Make it block level
121
+ iframe.setAttribute('frameborder', '0');
122
+ iframe.setAttribute('allowfullscreen', '');
123
+ iframe.setAttribute('contenteditable', 'false');
124
+
125
+ return iframe;
126
+ }
127
+
128
+ /**
129
+ * Insert video at current cursor position
130
+ * @param {string} src - Video source URL
131
+ * @param {string} editorId - Editor instance ID
132
+ */
133
+ static insertVideoAtCurrentPosition(src, editorId = null) {
134
+ // Get the correct editor instance
135
+ let editor = null;
136
+ if (editorId) {
137
+ editor = Editor.getInstanceById(editorId);
138
+ } else {
139
+ editor = Editor.getCurrentInstance();
140
+ }
141
+
142
+ if (!editor) {
143
+ console.warn('No editor instance found for video insertion');
144
+ return;
145
+ }
146
+
147
+ const selection = window.getSelection();
148
+ if (!selection || !selection.rangeCount) {
149
+ return;
150
+ }
151
+
152
+ try {
153
+ const range = selection.getRangeAt(0);
154
+ // Create video element
155
+ const videoElement = Video.create(src);
156
+ // Abort if the URL was rejected as unsafe
157
+ if (!videoElement) return;
158
+ // Insert video at cursor position
159
+ range.deleteContents();
160
+ range.insertNode(videoElement);
161
+ // Add a space after the video for easier editing
162
+ const spaceNode = document.createTextNode(' ');
163
+ range.setStartAfter(videoElement);
164
+ range.insertNode(spaceNode);
165
+ // Position cursor after the space
166
+ range.setStartAfter(spaceNode);
167
+ range.collapse(true);
168
+ selection.removeAllRanges();
169
+ selection.addRange(range);
170
+
171
+ // Trigger content change event
172
+ if (editor && typeof editor.onContentChange === 'function') {
173
+ editor.onContentChange();
174
+ }
175
+ } catch (error) {
176
+ console.error('Error inserting video:', error);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Apply video formatting - shows video popup
182
+ */
183
+ apply(src) {
184
+ if (src) {
185
+ Video.insertVideoAtCurrentPosition(src, this.editorId);
186
+ } else {
187
+ this.showVideoPopup();
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Remove video formatting
193
+ */
194
+ remove() {
195
+ const selection = window.getSelection();
196
+ if (!selection || !selection.rangeCount) return;
197
+
198
+ const range = selection.getRangeAt(0);
199
+ const videoElement = this.getVideoElement(range);
200
+
201
+ if (videoElement) {
202
+ videoElement.remove();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Toggle video formatting - shows video popup
208
+ */
209
+ toggle() {
210
+ if (this.videoPopup.isVisible) {
211
+ this.videoPopup.hide();
212
+ } else {
213
+ this.showVideoPopup();
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Show video popup
219
+ */
220
+ showVideoPopup() {
221
+ // Find video button in the current editor's toolbar
222
+ const editor = Editor.getInstanceById(this.editorId);
223
+ if (!editor) return;
224
+
225
+ const toolbar = editor.getModule('toolbar');
226
+ let videoButton = null;
227
+
228
+ if (toolbar) {
229
+ videoButton = toolbar.getButton('video');
230
+ }
231
+
232
+ // Fallback: find button by class in the current editor's toolbar
233
+ if (!videoButton) {
234
+ const toolbarContainer = toolbar?.getContainer();
235
+ if (toolbarContainer) {
236
+ videoButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.video-btn');
237
+ }
238
+ }
239
+
240
+ // Final fallback: find any video button in the current editor's wrapper
241
+ if (!videoButton) {
242
+ videoButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.video-btn');
243
+ }
244
+
245
+ if (!videoButton) {
246
+ console.warn('Video button not found for editor:', this.editorId);
247
+ return;
248
+ }
249
+
250
+ this.videoPopup.show(videoButton);
251
+ }
252
+
253
+ /**
254
+ * Check if video formatting is active
255
+ */
256
+ isActive() {
257
+ const selection = window.getSelection();
258
+ if (!selection || !selection.rangeCount) return false;
259
+
260
+ const range = selection.getRangeAt(0);
261
+ const videoElement = this.getVideoElement(range);
262
+
263
+ return videoElement !== null;
264
+ }
265
+
266
+ /**
267
+ * Get video element from selection
268
+ * @param {Range} range - Selection range
269
+ * @returns {HTMLElement|null}
270
+ */
271
+ getVideoElement(range) {
272
+ let node = range.commonAncestorContainer;
273
+
274
+ // If it's a text node, get its parent
275
+ if (node.nodeType === Node.TEXT_NODE) {
276
+ node = node.parentNode;
277
+ }
278
+
279
+ // Check if current node is a video or iframe
280
+ if ((node.tagName === 'VIDEO' || node.tagName === 'IFRAME') &&
281
+ node.classList && node.classList.contains('inserted-video')) {
282
+ return node;
283
+ }
284
+
285
+ // Check if selection contains a video or iframe
286
+ const videoInSelection = range.cloneContents().querySelector('.inserted-video');
287
+ if (videoInSelection) {
288
+ return videoInSelection;
289
+ }
290
+
291
+ return null;
292
+ }
293
+
294
+ /**
295
+ * Handle file upload
296
+ * @param {File} file - Video file
297
+ * @returns {Promise<string>} - Promise that resolves to video URL
298
+ */
299
+ static async handleFileUpload(file) {
300
+ return new Promise((resolve, reject) => {
301
+ if (!file || !file.type.startsWith('video/')) {
302
+ reject(new Error('Please select a valid video file'));
303
+ return;
304
+ }
305
+
306
+ const reader = new FileReader();
307
+ reader.onload = (e) => {
308
+ resolve(e.target.result);
309
+ };
310
+ reader.onerror = () => {
311
+ reject(new Error('Failed to read file'));
312
+ };
313
+ reader.readAsDataURL(file);
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Validate video URL
319
+ * @param {string} url - Video URL
320
+ * @returns {Promise<boolean>} - Promise that resolves to validation result
321
+ */
322
+ static validateVideoUrl(url) {
323
+ return new Promise((resolve) => {
324
+ // Check if it's a YouTube URL
325
+ if (Video.isYouTubeUrl(url)) {
326
+ resolve(true);
327
+ return;
328
+ }
329
+
330
+ // Check if it's a valid video URL format
331
+ const videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'];
332
+ const hasValidExtension = videoExtensions.some(ext =>
333
+ url.toLowerCase().includes(`.${ext}`)
334
+ );
335
+
336
+ if (hasValidExtension) {
337
+ resolve(true);
338
+ return;
339
+ }
340
+
341
+ // Try to load as video element (for direct video URLs)
342
+ const video = document.createElement('video');
343
+ video.onloadedmetadata = () => {
344
+ resolve(true);
345
+ };
346
+ video.onerror = () => {
347
+ resolve(false);
348
+ };
349
+
350
+ // Set timeout to avoid hanging
351
+ setTimeout(() => {
352
+ resolve(false);
353
+ }, 5000);
354
+
355
+ video.src = url;
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Check if URL is a YouTube URL
361
+ * @param {string} url - URL to check
362
+ * @returns {boolean} - Whether it's a YouTube URL
363
+ */
364
+ static isYouTubeUrl(url) {
365
+ const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/;
366
+ return youtubeRegex.test(url);
367
+ }
368
+
369
+ /**
370
+ * Extract YouTube video ID from URL
371
+ * @param {string} url - YouTube URL
372
+ * @returns {string|null} - Video ID or null if not found
373
+ */
374
+ static getYouTubeVideoId(url) {
375
+ const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/;
376
+ const match = url.match(youtubeRegex);
377
+ return match ? match[1] : null;
378
+ }
379
+ }
380
+
381
+ export default Video;