@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.
- package/LICENSE +15 -0
- package/README.md +146 -142
- package/core.js +77 -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 +134 -103
- package/index.js +227 -0
- package/lib/core/editor.js +1806 -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 +347 -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/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/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1285 -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 +18 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Popup Component - Popup for inserting videos
|
|
3
|
+
*/
|
|
4
|
+
import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
|
|
5
|
+
|
|
6
|
+
class VideoPopup {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = {
|
|
9
|
+
onVideoInsert: null,
|
|
10
|
+
editor: null,
|
|
11
|
+
...options
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
this.popup = null;
|
|
15
|
+
this.isVisible = false;
|
|
16
|
+
this.clickOutsideHandler = null;
|
|
17
|
+
this.selectedVideoSrc = null;
|
|
18
|
+
this.savedSelection = null; // Save editor selection
|
|
19
|
+
|
|
20
|
+
this.createVideoPopup();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create video popup
|
|
25
|
+
*/
|
|
26
|
+
createVideoPopup() {
|
|
27
|
+
this.popup = document.createElement('div');
|
|
28
|
+
this.popup.className = 'video-popup';
|
|
29
|
+
|
|
30
|
+
const content = document.createElement('div');
|
|
31
|
+
content.className = 'video-popup-content';
|
|
32
|
+
|
|
33
|
+
// Title
|
|
34
|
+
const title = document.createElement('h3');
|
|
35
|
+
title.textContent = 'Insert video';
|
|
36
|
+
title.className = 'yjd-input-title';
|
|
37
|
+
content.appendChild(title);
|
|
38
|
+
|
|
39
|
+
// Container
|
|
40
|
+
const uploadContainer = document.createElement('div');
|
|
41
|
+
uploadContainer.className = 'video-input-container';
|
|
42
|
+
|
|
43
|
+
const textLabel = document.createElement('p');
|
|
44
|
+
textLabel.textContent = 'Your video url';
|
|
45
|
+
textLabel.className = 'yjd-input-label';
|
|
46
|
+
|
|
47
|
+
const inputgroup1 = document.createElement('div');
|
|
48
|
+
inputgroup1.className = 'yjd-input-upload-group';
|
|
49
|
+
this.inputGroup = inputgroup1; // Store reference
|
|
50
|
+
|
|
51
|
+
// input url
|
|
52
|
+
this.urlInput = document.createElement('input');
|
|
53
|
+
this.urlInput.type = 'url';
|
|
54
|
+
this.urlInput.className = 'yjd-input';
|
|
55
|
+
this.urlInput.placeholder = 'Please enter your video URL';
|
|
56
|
+
this.urlInput.addEventListener('input', () => {
|
|
57
|
+
this.updateInsertButton();
|
|
58
|
+
// Show preview if URL is valid
|
|
59
|
+
const url = this.urlInput.value.trim();
|
|
60
|
+
if (url && this.isValidVideoUrl(url)) {
|
|
61
|
+
this.showPreview(url);
|
|
62
|
+
} else {
|
|
63
|
+
this.removePreview();
|
|
64
|
+
}
|
|
65
|
+
if(this.urlInput.value.trim()){
|
|
66
|
+
this.customButton.style.display = 'none';
|
|
67
|
+
}else{
|
|
68
|
+
this.customButton.style.display = 'block';
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Hidden file input
|
|
73
|
+
this.fileInput = document.createElement('input');
|
|
74
|
+
this.fileInput.type = 'file';
|
|
75
|
+
this.fileInput.accept = 'video/*';
|
|
76
|
+
this.fileInput.className = 'image-input-hidden'; // ẩn bằng CSS
|
|
77
|
+
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
|
78
|
+
|
|
79
|
+
// Custom button
|
|
80
|
+
const customButton = document.createElement('button');
|
|
81
|
+
customButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>`;
|
|
82
|
+
customButton.className = 'yjd-custom-upload-button';
|
|
83
|
+
this.customButton = customButton;
|
|
84
|
+
customButton.addEventListener('click', () => this.fileInput.click());
|
|
85
|
+
|
|
86
|
+
// Create preview container
|
|
87
|
+
this.createPreviewContainer();
|
|
88
|
+
|
|
89
|
+
// Append elements
|
|
90
|
+
inputgroup1.appendChild(this.urlInput);
|
|
91
|
+
inputgroup1.appendChild(this.fileInput);
|
|
92
|
+
inputgroup1.appendChild(customButton);
|
|
93
|
+
uploadContainer.appendChild(textLabel);
|
|
94
|
+
uploadContainer.appendChild(inputgroup1);
|
|
95
|
+
uploadContainer.appendChild(this.previewContainer);
|
|
96
|
+
content.appendChild(uploadContainer);
|
|
97
|
+
|
|
98
|
+
// Buttons
|
|
99
|
+
const buttonContainer = document.createElement('div');
|
|
100
|
+
buttonContainer.className = 'yjd-button-container';
|
|
101
|
+
|
|
102
|
+
const cancelButton = document.createElement('button');
|
|
103
|
+
cancelButton.type = 'button';
|
|
104
|
+
cancelButton.className = 'image-button yjd-button-cancel';
|
|
105
|
+
cancelButton.textContent = 'Cancel';
|
|
106
|
+
cancelButton.addEventListener('click', () => {
|
|
107
|
+
this.hide();
|
|
108
|
+
// Maintain editor focus after popup close
|
|
109
|
+
if (this.options.editor) {
|
|
110
|
+
setTimeout(() => this.options.editor.focus(), 0);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.insertButton = document.createElement('button');
|
|
115
|
+
this.insertButton.type = 'button';
|
|
116
|
+
this.insertButton.className = 'image-button yjd-button-confirm button-disable';
|
|
117
|
+
this.insertButton.textContent = 'Add video';
|
|
118
|
+
this.insertButton.disabled = true;
|
|
119
|
+
this.insertButton.addEventListener('click', () => {
|
|
120
|
+
this.insertVideo();
|
|
121
|
+
// Maintain editor focus after insert
|
|
122
|
+
if (this.options.editor) {
|
|
123
|
+
setTimeout(() => this.options.editor.focus(), 0);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
buttonContainer.appendChild(cancelButton);
|
|
128
|
+
buttonContainer.appendChild(this.insertButton);
|
|
129
|
+
content.appendChild(buttonContainer);
|
|
130
|
+
|
|
131
|
+
this.popup.appendChild(content);
|
|
132
|
+
appendPopup(this.popup);
|
|
133
|
+
|
|
134
|
+
// Prevent focus loss when clicking on popup
|
|
135
|
+
if (this.options.editor && typeof this.options.editor.preventFocusLoss === 'function') {
|
|
136
|
+
this.options.editor.preventFocusLoss(this.popup);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async handleFileSelect(e) {
|
|
141
|
+
const file = e.target.files[0];
|
|
142
|
+
if (!file) return;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const { default: Video } = await import('../formats/video.js');
|
|
146
|
+
this.selectedVideoSrc = await Video.handleFileUpload(file);
|
|
147
|
+
this.urlInput.value = '';
|
|
148
|
+
this.showPreview(this.selectedVideoSrc);
|
|
149
|
+
this.updateInsertButton();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
alert(error.message);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
updateInsertButton() {
|
|
156
|
+
const hasVideo = this.selectedVideoSrc || this.urlInput.value.trim();
|
|
157
|
+
this.insertButton.disabled = !hasVideo;
|
|
158
|
+
this.insertButton.classList.toggle('button-disable', !hasVideo);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Show video preview
|
|
163
|
+
*/
|
|
164
|
+
showPreview(videoSrc) {
|
|
165
|
+
if (!videoSrc) return;
|
|
166
|
+
|
|
167
|
+
this.videoPreview.src = videoSrc;
|
|
168
|
+
this.previewContainer.style.display = 'block';
|
|
169
|
+
this.selectedVideoSrc = videoSrc;
|
|
170
|
+
|
|
171
|
+
// Hide input group
|
|
172
|
+
this.toggleInputGroup(false);
|
|
173
|
+
|
|
174
|
+
// Recalculate position after preview is shown to ensure buttons remain visible
|
|
175
|
+
this.recalculatePosition();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Remove video preview and show input again
|
|
180
|
+
*/
|
|
181
|
+
removePreview() {
|
|
182
|
+
this.selectedVideoSrc = null;
|
|
183
|
+
this.previewContainer.style.display = 'none';
|
|
184
|
+
this.videoPreview.src = '';
|
|
185
|
+
|
|
186
|
+
// Show input group and reset file input
|
|
187
|
+
this.toggleInputGroup(true);
|
|
188
|
+
if (this.fileInput) {
|
|
189
|
+
this.fileInput.value = '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.updateInsertButton();
|
|
193
|
+
|
|
194
|
+
// Recalculate position after preview is removed
|
|
195
|
+
this.recalculatePosition();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Toggle input group visibility
|
|
200
|
+
*/
|
|
201
|
+
toggleInputGroup(show) {
|
|
202
|
+
if (!this.inputGroup) return;
|
|
203
|
+
|
|
204
|
+
if (show) {
|
|
205
|
+
this.inputGroup.style.display = 'flex';
|
|
206
|
+
this.inputGroup.style.visibility = 'visible';
|
|
207
|
+
if (this.customButton) {
|
|
208
|
+
this.customButton.style.pointerEvents = 'auto';
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
this.inputGroup.style.display = 'none';
|
|
212
|
+
this.inputGroup.style.visibility = 'hidden';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create preview container with video and remove button
|
|
218
|
+
*/
|
|
219
|
+
createPreviewContainer() {
|
|
220
|
+
this.previewContainer = document.createElement('div');
|
|
221
|
+
this.previewContainer.className = 'video-preview-container';
|
|
222
|
+
this.previewContainer.style.cssText = 'display: none; position: relative;';
|
|
223
|
+
|
|
224
|
+
// Video preview
|
|
225
|
+
this.videoPreview = document.createElement('video');
|
|
226
|
+
this.videoPreview.className = 'video-preview';
|
|
227
|
+
this.videoPreview.style.cssText = 'max-width: 100%; max-height: 200px; border-radius: 8px; object-fit: contain;';
|
|
228
|
+
this.videoPreview.controls = true;
|
|
229
|
+
this.videoPreview.muted = true;
|
|
230
|
+
|
|
231
|
+
// Remove button
|
|
232
|
+
this.removeButton = document.createElement('button');
|
|
233
|
+
this.removeButton.className = 'video-remove-button';
|
|
234
|
+
this.removeButton.innerHTML = '×';
|
|
235
|
+
this.removeButton.style.cssText = `
|
|
236
|
+
position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.7);
|
|
237
|
+
color: white; border: none; border-radius: 50%; width: 24px; height: 24px;
|
|
238
|
+
cursor: pointer; font-size: 16px; font-weight: bold;
|
|
239
|
+
`;
|
|
240
|
+
this.removeButton.addEventListener('click', () => this.removePreview());
|
|
241
|
+
|
|
242
|
+
this.previewContainer.appendChild(this.videoPreview);
|
|
243
|
+
this.previewContainer.appendChild(this.removeButton);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if URL is a valid video URL
|
|
248
|
+
*/
|
|
249
|
+
isValidVideoUrl(url) {
|
|
250
|
+
try {
|
|
251
|
+
const urlObj = new URL(url);
|
|
252
|
+
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'];
|
|
253
|
+
const videoHosts = ['youtube.com', 'youtu.be', 'vimeo.com', 'dailymotion.com'];
|
|
254
|
+
|
|
255
|
+
const pathname = urlObj.pathname.toLowerCase();
|
|
256
|
+
const hasVideoExtension = videoExtensions.some(ext => pathname.endsWith(ext));
|
|
257
|
+
const isFromVideoHost = videoHosts.some(host => urlObj.hostname.includes(host));
|
|
258
|
+
|
|
259
|
+
return hasVideoExtension || isFromVideoHost;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async insertVideo() {
|
|
266
|
+
let src = this.selectedVideoSrc || this.urlInput.value.trim();
|
|
267
|
+
|
|
268
|
+
if (!src) return;
|
|
269
|
+
|
|
270
|
+
// Always validate URL (both file upload and URL input)
|
|
271
|
+
try {
|
|
272
|
+
const { default: Video } = await import('../formats/video.js');
|
|
273
|
+
const isValid = await Video.validateVideoUrl(src);
|
|
274
|
+
if (!isValid) {
|
|
275
|
+
alert('Invalid video URL. Please check the URL and try again.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
alert('Error validating video URL.');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Restore editor selection before inserting
|
|
284
|
+
this.restoreSelection();
|
|
285
|
+
|
|
286
|
+
if (this.options.onVideoInsert) {
|
|
287
|
+
this.options.onVideoInsert(src);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.hide();
|
|
291
|
+
this.reset();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
reset() {
|
|
295
|
+
this.fileInput.value = '';
|
|
296
|
+
this.urlInput.value = '';
|
|
297
|
+
this.selectedVideoSrc = null;
|
|
298
|
+
|
|
299
|
+
// Hide preview and show input
|
|
300
|
+
this.previewContainer.style.display = 'none';
|
|
301
|
+
this.videoPreview.src = '';
|
|
302
|
+
this.toggleInputGroup(true);
|
|
303
|
+
|
|
304
|
+
this.updateInsertButton();
|
|
305
|
+
this.customButton.style.display = 'block';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Save current editor selection
|
|
310
|
+
*/
|
|
311
|
+
saveSelection() {
|
|
312
|
+
const selection = window.getSelection();
|
|
313
|
+
if (selection && selection.rangeCount > 0) {
|
|
314
|
+
this.savedSelection = selection.getRangeAt(0).cloneRange();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Restore editor selection
|
|
320
|
+
*/
|
|
321
|
+
restoreSelection() {
|
|
322
|
+
if (this.savedSelection) {
|
|
323
|
+
const selection = window.getSelection();
|
|
324
|
+
selection.removeAllRanges();
|
|
325
|
+
selection.addRange(this.savedSelection);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
setupClickOutside() {
|
|
330
|
+
if (this.clickOutsideHandler) {
|
|
331
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.clickOutsideHandler = (e) => {
|
|
335
|
+
if (!this.popup.contains(e.target)) {
|
|
336
|
+
this.hide();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
setTimeout(() => {
|
|
341
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
342
|
+
}, 100);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
removeClickOutside() {
|
|
346
|
+
if (this.clickOutsideHandler) {
|
|
347
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
348
|
+
this.clickOutsideHandler = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
show(anchor) {
|
|
353
|
+
if (!anchor) return;
|
|
354
|
+
|
|
355
|
+
// Save current editor selection before showing popup
|
|
356
|
+
this.saveSelection();
|
|
357
|
+
|
|
358
|
+
// Reset state when showing popup
|
|
359
|
+
this.reset();
|
|
360
|
+
|
|
361
|
+
// Store anchor for recalculation
|
|
362
|
+
this.currentAnchor = anchor;
|
|
363
|
+
|
|
364
|
+
// Calculate and set popup position
|
|
365
|
+
const position = calculatePopupPosition(anchor, this.popup, {
|
|
366
|
+
offsetY: 5,
|
|
367
|
+
offsetX: 0
|
|
368
|
+
});
|
|
369
|
+
setPopupPosition(this.popup, position);
|
|
370
|
+
|
|
371
|
+
this.popup.classList.add('visible');
|
|
372
|
+
this.isVisible = true;
|
|
373
|
+
|
|
374
|
+
this.setupClickOutside();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Recalculate popup position to ensure it stays within viewport
|
|
379
|
+
*/
|
|
380
|
+
recalculatePosition() {
|
|
381
|
+
if (!this.currentAnchor || !this.isVisible) return;
|
|
382
|
+
|
|
383
|
+
// Small delay to ensure DOM updates are complete
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
const position = calculatePopupPosition(this.currentAnchor, this.popup, {
|
|
386
|
+
offsetY: 5,
|
|
387
|
+
offsetX: 0
|
|
388
|
+
});
|
|
389
|
+
setPopupPosition(this.popup, position);
|
|
390
|
+
}, 10);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
hide() {
|
|
394
|
+
this.popup.classList.remove('visible');
|
|
395
|
+
this.isVisible = false;
|
|
396
|
+
this.removeClickOutside();
|
|
397
|
+
// Clear saved selection to avoid memory leaks
|
|
398
|
+
this.savedSelection = null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
destroy() {
|
|
402
|
+
this.removeClickOutside();
|
|
403
|
+
|
|
404
|
+
if (this.popup && this.popup.parentNode) {
|
|
405
|
+
this.popup.parentNode.removeChild(this.popup);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.popup = null;
|
|
409
|
+
this.isVisible = false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export default VideoPopup;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execCommand wrapper — single migration point for the deprecated
|
|
3
|
+
* document.execCommand / queryCommand* family.
|
|
4
|
+
*
|
|
5
|
+
* document.execCommand is deprecated. It still works in every current browser
|
|
6
|
+
* and remains the most reliable way to toggle inline formatting across complex
|
|
7
|
+
* selections, so we keep using it for now — but ONLY through this module.
|
|
8
|
+
* Centralizing it here means:
|
|
9
|
+
* - consistent try/catch (these APIs throw in detached/edge cases),
|
|
10
|
+
* - one place to add feature detection or a fallback,
|
|
11
|
+
* - one place to perform a future Range-API migration without touching every
|
|
12
|
+
* format file.
|
|
13
|
+
*
|
|
14
|
+
* Prefer these helpers over calling document.execCommand directly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute a formatting command.
|
|
19
|
+
* @param {string} command - execCommand command name (e.g. 'bold', 'foreColor')
|
|
20
|
+
* @param {string|null} [value] - command value, when applicable
|
|
21
|
+
* @returns {boolean} true if the command ran without throwing
|
|
22
|
+
*/
|
|
23
|
+
export function execFormat(command, value = null) {
|
|
24
|
+
try {
|
|
25
|
+
// Omit the value argument when none is given. Passing null explicitly makes
|
|
26
|
+
// some commands stringify it (e.g. insertHorizontalRule would set id="null").
|
|
27
|
+
return value == null
|
|
28
|
+
? document.execCommand(command, false)
|
|
29
|
+
: document.execCommand(command, false, value);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn(`execCommand('${command}') failed:`, e);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enable/disable styleWithCSS (so commands emit inline styles instead of
|
|
38
|
+
* deprecated presentational tags like <font>). Safe to call before each
|
|
39
|
+
* styling command.
|
|
40
|
+
* @param {boolean} [enabled=true]
|
|
41
|
+
*/
|
|
42
|
+
export function setStyleWithCSS(enabled = true) {
|
|
43
|
+
return execFormat('styleWithCSS', enabled);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Query whether a command is currently active for the selection.
|
|
48
|
+
* @param {string} command
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function queryFormatState(command) {
|
|
52
|
+
try {
|
|
53
|
+
return document.queryCommandState(command);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Query the current value of a command for the selection.
|
|
61
|
+
* @param {string} command
|
|
62
|
+
* @returns {string} empty string if unsupported/unavailable
|
|
63
|
+
*/
|
|
64
|
+
export function queryFormatValue(command) {
|
|
65
|
+
try {
|
|
66
|
+
return document.queryCommandValue(command) || '';
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default { execFormat, setStyleWithCSS, queryFormatState, queryFormatValue };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Editor from '../core/editor.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper function to save history state before applying format
|
|
5
|
+
* This should be called by all format operations to ensure proper undo/redo functionality
|
|
6
|
+
*/
|
|
7
|
+
export function saveBeforeFormat() {
|
|
8
|
+
const editor = Editor.getCurrentInstance();
|
|
9
|
+
if (editor) {
|
|
10
|
+
const historyModule = editor.getModule('history');
|
|
11
|
+
if (historyModule && typeof historyModule.saveBeforeFormat === 'function') {
|
|
12
|
+
historyModule.saveBeforeFormat();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to trigger content change after format operations
|
|
19
|
+
* This ensures onChange callback is called when formatting is applied
|
|
20
|
+
*/
|
|
21
|
+
export function triggerContentChange() {
|
|
22
|
+
const editor = Editor.getCurrentInstance();
|
|
23
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
24
|
+
// Use setTimeout to ensure the DOM changes are complete
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
editor.onContentChange();
|
|
27
|
+
}, 0);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Helper function to save before format and trigger content change
|
|
33
|
+
* This is a convenience function that combines both operations
|
|
34
|
+
*/
|
|
35
|
+
export function saveBeforeFormatAndTriggerChange() {
|
|
36
|
+
saveBeforeFormat();
|
|
37
|
+
triggerContentChange();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper function to check if history module is available
|
|
42
|
+
*/
|
|
43
|
+
export function hasHistoryModule() {
|
|
44
|
+
const editor = Editor.getCurrentInstance();
|
|
45
|
+
if (editor) {
|
|
46
|
+
const historyModule = editor.getModule('history');
|
|
47
|
+
return historyModule && typeof historyModule.saveBeforeFormat === 'function';
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|