@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,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;
|