@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.
- package/LICENSE +15 -0
- package/README.md +223 -142
- package/core.js +82 -0
- package/dist/core.esm.js +2 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +230 -103
- package/index.js +297 -0
- package/lib/core/editor.js +1885 -0
- package/lib/core/format.js +540 -0
- package/lib/core/module.js +81 -0
- package/lib/core/registry.js +158 -0
- package/lib/formats/background.js +213 -0
- package/lib/formats/bold.js +49 -0
- package/lib/formats/capitalization.js +579 -0
- package/lib/formats/color.js +183 -0
- package/lib/formats/emoji.js +282 -0
- package/lib/formats/font-family.js +548 -0
- package/lib/formats/heading.js +502 -0
- package/lib/formats/image.js +341 -0
- package/lib/formats/import.js +385 -0
- package/lib/formats/indent.js +297 -0
- package/lib/formats/italic.js +27 -0
- package/lib/formats/line-height.js +562 -0
- package/lib/formats/link.js +251 -0
- package/lib/formats/list.js +635 -0
- package/lib/formats/strike.js +31 -0
- package/lib/formats/subscript.js +40 -0
- package/lib/formats/superscript.js +39 -0
- package/lib/formats/table.js +293 -0
- package/lib/formats/tag.js +304 -0
- package/lib/formats/text-align.js +422 -0
- package/lib/formats/text-size.js +498 -0
- package/lib/formats/underline.js +30 -0
- package/lib/formats/video.js +381 -0
- package/lib/modules/block-toolbar.js +639 -0
- package/lib/modules/code-view.js +447 -0
- package/lib/modules/find-replace.js +273 -0
- package/lib/modules/history.js +425 -0
- package/lib/modules/mention.js +200 -0
- package/lib/modules/resize-handles.js +701 -0
- package/lib/modules/slash-menu.js +183 -0
- package/lib/modules/table-toolbar.js +635 -0
- package/lib/modules/toolbar.js +607 -0
- package/lib/serialize.js +241 -0
- package/lib/static.js +28 -0
- package/lib/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1392 -35
- package/lib/styles.css.js +2 -0
- package/lib/styles.min.css +1 -0
- package/lib/ui/color-picker.js +296 -0
- package/lib/ui/customselect.js +351 -0
- package/lib/ui/emoji-picker.js +196 -0
- package/lib/ui/icons.js +145 -0
- package/lib/ui/image-popup.js +435 -0
- package/lib/ui/import-popup.js +288 -0
- package/lib/ui/link-popup.js +139 -0
- package/lib/ui/list-picker.js +307 -0
- package/lib/ui/select-button.js +68 -0
- package/lib/ui/table-popup.js +171 -0
- package/lib/ui/tag-popup.js +249 -0
- package/lib/ui/text-align-picker.js +278 -0
- package/lib/ui/video-popup.js +413 -0
- package/lib/utils/exec-command.js +72 -0
- package/lib/utils/history-helper.js +50 -0
- package/lib/utils/popup-helper.js +219 -0
- package/lib/utils/popup-positioning.js +234 -0
- package/lib/utils/sanitize.js +164 -0
- package/package.json +51 -32
- 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;
|