@oix1987/yjd 1.0.0 → 1.0.2
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/README.md +73 -22
- package/dist/rich-editor.esm.js +2 -0
- package/dist/rich-editor.esm.js.map +1 -0
- package/dist/rich-editor.min.js +2 -0
- package/dist/rich-editor.min.js.map +1 -0
- package/package.json +12 -7
- package/index.js +0 -221
- package/lib/core/editor.js +0 -1175
- package/lib/core/format.js +0 -542
- package/lib/core/module.js +0 -81
- package/lib/core/registry.js +0 -152
- package/lib/formats/background.js +0 -212
- package/lib/formats/bold.js +0 -67
- package/lib/formats/capitalization.js +0 -563
- package/lib/formats/color.js +0 -165
- package/lib/formats/emoji.js +0 -282
- package/lib/formats/font-family.js +0 -547
- package/lib/formats/heading.js +0 -502
- package/lib/formats/image.js +0 -344
- package/lib/formats/import.js +0 -385
- package/lib/formats/indent.js +0 -297
- package/lib/formats/italic.js +0 -27
- package/lib/formats/line-height.js +0 -558
- package/lib/formats/link.js +0 -251
- package/lib/formats/list.js +0 -635
- package/lib/formats/strike.js +0 -31
- package/lib/formats/subscript.js +0 -36
- package/lib/formats/superscript.js +0 -35
- package/lib/formats/table.js +0 -288
- package/lib/formats/tag.js +0 -304
- package/lib/formats/text-align.js +0 -421
- package/lib/formats/text-size.js +0 -497
- package/lib/formats/underline.js +0 -30
- package/lib/formats/video.js +0 -372
- package/lib/modules/block-toolbar.js +0 -628
- package/lib/modules/code-view.js +0 -434
- package/lib/modules/history.js +0 -410
- package/lib/modules/resize-handles.js +0 -677
- package/lib/modules/table-toolbar.js +0 -618
- package/lib/modules/toolbar.js +0 -424
- package/lib/styles-loader.js +0 -144
- package/lib/styles.css +0 -2123
- package/lib/ui/color-picker.js +0 -296
- package/lib/ui/customselect.js +0 -319
- package/lib/ui/emoji-picker.js +0 -196
- package/lib/ui/icons.js +0 -413
- package/lib/ui/image-popup.js +0 -444
- package/lib/ui/import-popup.js +0 -288
- package/lib/ui/link-popup.js +0 -191
- package/lib/ui/list-picker.js +0 -307
- package/lib/ui/select-button.js +0 -61
- package/lib/ui/table-popup.js +0 -171
- package/lib/ui/tag-popup.js +0 -249
- package/lib/ui/text-align-picker.js +0 -281
- package/lib/ui/video-popup.js +0 -422
- package/lib/utils/history-helper.js +0 -50
- package/lib/utils/popup-helper.js +0 -219
- package/lib/utils/popup-positioning.js +0 -231
package/lib/ui/video-popup.js
DELETED
|
@@ -1,422 +0,0 @@
|
|
|
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 = 'Upload 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" width="17" height="17" viewBox="0 0 17 17" fill="none">
|
|
82
|
-
<g clip-path="url(#clip0_243_650)">
|
|
83
|
-
<path d="M9.45721 4.06101V11.4287C9.45721 11.966 9.02311 12.4001 8.48578 12.4001C7.94846 12.4001 7.51436 11.966 7.51436 11.4287V4.06101L5.28614 6.28923C4.90668 6.66869 4.29043 6.66869 3.91096 6.28923C3.5315 5.90976 3.5315 5.29351 3.91096 4.91405L7.79668 1.02833C8.17614 0.64887 8.79239 0.64887 9.17186 1.02833L13.0576 4.91405C13.437 5.29351 13.437 5.90976 13.0576 6.28923C12.6781 6.66869 12.0619 6.66869 11.6824 6.28923L9.45721 4.06101ZM2.65721 11.4287H6.54293C6.54293 12.5003 7.41418 13.3715 8.48578 13.3715C9.55739 13.3715 10.4286 12.5003 10.4286 11.4287H14.3144C15.386 11.4287 16.2572 12.2999 16.2572 13.3715V14.343C16.2572 15.4146 15.386 16.2858 14.3144 16.2858H2.65721C1.58561 16.2858 0.714355 15.4146 0.714355 14.343V13.3715C0.714355 12.2999 1.58561 11.4287 2.65721 11.4287ZM13.8286 14.5858C14.0219 14.5858 14.2072 14.5091 14.3438 14.3724C14.4805 14.2358 14.5572 14.0505 14.5572 13.8573C14.5572 13.664 14.4805 13.4787 14.3438 13.3421C14.2072 13.2055 14.0219 13.1287 13.8286 13.1287C13.6354 13.1287 13.4501 13.2055 13.3135 13.3421C13.1768 13.4787 13.1001 13.664 13.1001 13.8573C13.1001 14.0505 13.1768 14.2358 13.3135 14.3724C13.4501 14.5091 13.6354 14.5858 13.8286 14.5858Z" fill="#252424"/>
|
|
84
|
-
</g>
|
|
85
|
-
<defs>
|
|
86
|
-
<clipPath id="clip0_243_650">
|
|
87
|
-
<rect width="15.5429" height="15.5429" fill="white" transform="translate(0.714355 0.742859)"/>
|
|
88
|
-
</clipPath>
|
|
89
|
-
</defs>
|
|
90
|
-
</svg>`;
|
|
91
|
-
customButton.className = 'yjd-custom-upload-button';
|
|
92
|
-
this.customButton = customButton;
|
|
93
|
-
customButton.addEventListener('click', () => this.fileInput.click());
|
|
94
|
-
|
|
95
|
-
// Create preview container
|
|
96
|
-
this.createPreviewContainer();
|
|
97
|
-
|
|
98
|
-
// Append elements
|
|
99
|
-
inputgroup1.appendChild(this.urlInput);
|
|
100
|
-
inputgroup1.appendChild(this.fileInput);
|
|
101
|
-
inputgroup1.appendChild(customButton);
|
|
102
|
-
uploadContainer.appendChild(textLabel);
|
|
103
|
-
uploadContainer.appendChild(inputgroup1);
|
|
104
|
-
uploadContainer.appendChild(this.previewContainer);
|
|
105
|
-
content.appendChild(uploadContainer);
|
|
106
|
-
|
|
107
|
-
// Buttons
|
|
108
|
-
const buttonContainer = document.createElement('div');
|
|
109
|
-
buttonContainer.className = 'yjd-button-container';
|
|
110
|
-
|
|
111
|
-
const cancelButton = document.createElement('button');
|
|
112
|
-
cancelButton.type = 'button';
|
|
113
|
-
cancelButton.className = 'image-button yjd-button-cancel';
|
|
114
|
-
cancelButton.textContent = 'Cancel';
|
|
115
|
-
cancelButton.addEventListener('click', () => {
|
|
116
|
-
this.hide();
|
|
117
|
-
// Maintain editor focus after popup close
|
|
118
|
-
if (this.options.editor) {
|
|
119
|
-
setTimeout(() => this.options.editor.focus(), 0);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
this.insertButton = document.createElement('button');
|
|
124
|
-
this.insertButton.type = 'button';
|
|
125
|
-
this.insertButton.className = 'image-button yjd-button-confirm button-disable';
|
|
126
|
-
this.insertButton.textContent = 'Add video';
|
|
127
|
-
this.insertButton.disabled = true;
|
|
128
|
-
this.insertButton.addEventListener('click', () => {
|
|
129
|
-
this.insertVideo();
|
|
130
|
-
// Maintain editor focus after insert
|
|
131
|
-
if (this.options.editor) {
|
|
132
|
-
setTimeout(() => this.options.editor.focus(), 0);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
buttonContainer.appendChild(cancelButton);
|
|
137
|
-
buttonContainer.appendChild(this.insertButton);
|
|
138
|
-
content.appendChild(buttonContainer);
|
|
139
|
-
|
|
140
|
-
this.popup.appendChild(content);
|
|
141
|
-
appendPopup(this.popup);
|
|
142
|
-
|
|
143
|
-
// Prevent focus loss when clicking on popup
|
|
144
|
-
if (this.options.editor && typeof this.options.editor.preventFocusLoss === 'function') {
|
|
145
|
-
this.options.editor.preventFocusLoss(this.popup);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async handleFileSelect(e) {
|
|
150
|
-
const file = e.target.files[0];
|
|
151
|
-
if (!file) return;
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const { default: Video } = await import('../formats/video.js');
|
|
155
|
-
this.selectedVideoSrc = await Video.handleFileUpload(file);
|
|
156
|
-
this.urlInput.value = '';
|
|
157
|
-
this.showPreview(this.selectedVideoSrc);
|
|
158
|
-
this.updateInsertButton();
|
|
159
|
-
} catch (error) {
|
|
160
|
-
alert(error.message);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
updateInsertButton() {
|
|
165
|
-
const hasVideo = this.selectedVideoSrc || this.urlInput.value.trim();
|
|
166
|
-
this.insertButton.disabled = !hasVideo;
|
|
167
|
-
this.insertButton.classList.toggle('button-disable', !hasVideo);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Show video preview
|
|
172
|
-
*/
|
|
173
|
-
showPreview(videoSrc) {
|
|
174
|
-
if (!videoSrc) return;
|
|
175
|
-
|
|
176
|
-
this.videoPreview.src = videoSrc;
|
|
177
|
-
this.previewContainer.style.display = 'block';
|
|
178
|
-
this.selectedVideoSrc = videoSrc;
|
|
179
|
-
|
|
180
|
-
// Hide input group
|
|
181
|
-
this.toggleInputGroup(false);
|
|
182
|
-
|
|
183
|
-
// Recalculate position after preview is shown to ensure buttons remain visible
|
|
184
|
-
this.recalculatePosition();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Remove video preview and show input again
|
|
189
|
-
*/
|
|
190
|
-
removePreview() {
|
|
191
|
-
this.selectedVideoSrc = null;
|
|
192
|
-
this.previewContainer.style.display = 'none';
|
|
193
|
-
this.videoPreview.src = '';
|
|
194
|
-
|
|
195
|
-
// Show input group and reset file input
|
|
196
|
-
this.toggleInputGroup(true);
|
|
197
|
-
if (this.fileInput) {
|
|
198
|
-
this.fileInput.value = '';
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.updateInsertButton();
|
|
202
|
-
|
|
203
|
-
// Recalculate position after preview is removed
|
|
204
|
-
this.recalculatePosition();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Toggle input group visibility
|
|
209
|
-
*/
|
|
210
|
-
toggleInputGroup(show) {
|
|
211
|
-
if (!this.inputGroup) return;
|
|
212
|
-
|
|
213
|
-
if (show) {
|
|
214
|
-
this.inputGroup.style.display = 'flex';
|
|
215
|
-
this.inputGroup.style.visibility = 'visible';
|
|
216
|
-
if (this.customButton) {
|
|
217
|
-
this.customButton.style.pointerEvents = 'auto';
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
this.inputGroup.style.display = 'none';
|
|
221
|
-
this.inputGroup.style.visibility = 'hidden';
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Create preview container with video and remove button
|
|
227
|
-
*/
|
|
228
|
-
createPreviewContainer() {
|
|
229
|
-
this.previewContainer = document.createElement('div');
|
|
230
|
-
this.previewContainer.className = 'video-preview-container';
|
|
231
|
-
this.previewContainer.style.cssText = 'display: none; position: relative;';
|
|
232
|
-
|
|
233
|
-
// Video preview
|
|
234
|
-
this.videoPreview = document.createElement('video');
|
|
235
|
-
this.videoPreview.className = 'video-preview';
|
|
236
|
-
this.videoPreview.style.cssText = 'max-width: 100%; max-height: 200px; border-radius: 8px; object-fit: contain;';
|
|
237
|
-
this.videoPreview.controls = true;
|
|
238
|
-
this.videoPreview.muted = true;
|
|
239
|
-
|
|
240
|
-
// Remove button
|
|
241
|
-
this.removeButton = document.createElement('button');
|
|
242
|
-
this.removeButton.className = 'video-remove-button';
|
|
243
|
-
this.removeButton.innerHTML = '×';
|
|
244
|
-
this.removeButton.style.cssText = `
|
|
245
|
-
position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.7);
|
|
246
|
-
color: white; border: none; border-radius: 50%; width: 24px; height: 24px;
|
|
247
|
-
cursor: pointer; font-size: 16px; font-weight: bold;
|
|
248
|
-
`;
|
|
249
|
-
this.removeButton.addEventListener('click', () => this.removePreview());
|
|
250
|
-
|
|
251
|
-
this.previewContainer.appendChild(this.videoPreview);
|
|
252
|
-
this.previewContainer.appendChild(this.removeButton);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Check if URL is a valid video URL
|
|
257
|
-
*/
|
|
258
|
-
isValidVideoUrl(url) {
|
|
259
|
-
try {
|
|
260
|
-
const urlObj = new URL(url);
|
|
261
|
-
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'];
|
|
262
|
-
const videoHosts = ['youtube.com', 'youtu.be', 'vimeo.com', 'dailymotion.com'];
|
|
263
|
-
|
|
264
|
-
const pathname = urlObj.pathname.toLowerCase();
|
|
265
|
-
const hasVideoExtension = videoExtensions.some(ext => pathname.endsWith(ext));
|
|
266
|
-
const isFromVideoHost = videoHosts.some(host => urlObj.hostname.includes(host));
|
|
267
|
-
|
|
268
|
-
return hasVideoExtension || isFromVideoHost;
|
|
269
|
-
} catch {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async insertVideo() {
|
|
275
|
-
let src = this.selectedVideoSrc || this.urlInput.value.trim();
|
|
276
|
-
|
|
277
|
-
if (!src) return;
|
|
278
|
-
|
|
279
|
-
// Always validate URL (both file upload and URL input)
|
|
280
|
-
try {
|
|
281
|
-
const { default: Video } = await import('../formats/video.js');
|
|
282
|
-
const isValid = await Video.validateVideoUrl(src);
|
|
283
|
-
if (!isValid) {
|
|
284
|
-
alert('Invalid video URL. Please check the URL and try again.');
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
alert('Error validating video URL.');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Restore editor selection before inserting
|
|
293
|
-
this.restoreSelection();
|
|
294
|
-
|
|
295
|
-
if (this.options.onVideoInsert) {
|
|
296
|
-
this.options.onVideoInsert(src);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
this.hide();
|
|
300
|
-
this.reset();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
reset() {
|
|
304
|
-
this.fileInput.value = '';
|
|
305
|
-
this.urlInput.value = '';
|
|
306
|
-
this.selectedVideoSrc = null;
|
|
307
|
-
|
|
308
|
-
// Hide preview and show input
|
|
309
|
-
this.previewContainer.style.display = 'none';
|
|
310
|
-
this.videoPreview.src = '';
|
|
311
|
-
this.toggleInputGroup(true);
|
|
312
|
-
|
|
313
|
-
this.updateInsertButton();
|
|
314
|
-
this.customButton.style.display = 'block';
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Save current editor selection
|
|
319
|
-
*/
|
|
320
|
-
saveSelection() {
|
|
321
|
-
const selection = window.getSelection();
|
|
322
|
-
if (selection && selection.rangeCount > 0) {
|
|
323
|
-
this.savedSelection = selection.getRangeAt(0).cloneRange();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Restore editor selection
|
|
329
|
-
*/
|
|
330
|
-
restoreSelection() {
|
|
331
|
-
if (this.savedSelection) {
|
|
332
|
-
const selection = window.getSelection();
|
|
333
|
-
selection.removeAllRanges();
|
|
334
|
-
selection.addRange(this.savedSelection);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
setupClickOutside() {
|
|
339
|
-
if (this.clickOutsideHandler) {
|
|
340
|
-
document.removeEventListener('click', this.clickOutsideHandler);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
this.clickOutsideHandler = (e) => {
|
|
344
|
-
if (!this.popup.contains(e.target)) {
|
|
345
|
-
this.hide();
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
setTimeout(() => {
|
|
350
|
-
document.addEventListener('click', this.clickOutsideHandler);
|
|
351
|
-
}, 100);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
removeClickOutside() {
|
|
355
|
-
if (this.clickOutsideHandler) {
|
|
356
|
-
document.removeEventListener('click', this.clickOutsideHandler);
|
|
357
|
-
this.clickOutsideHandler = null;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
show(anchor) {
|
|
362
|
-
if (!anchor) return;
|
|
363
|
-
|
|
364
|
-
// Save current editor selection before showing popup
|
|
365
|
-
this.saveSelection();
|
|
366
|
-
|
|
367
|
-
// Reset state when showing popup
|
|
368
|
-
this.reset();
|
|
369
|
-
|
|
370
|
-
// Store anchor for recalculation
|
|
371
|
-
this.currentAnchor = anchor;
|
|
372
|
-
|
|
373
|
-
// Calculate and set popup position
|
|
374
|
-
const position = calculatePopupPosition(anchor, this.popup, {
|
|
375
|
-
offsetY: 5,
|
|
376
|
-
offsetX: 0
|
|
377
|
-
});
|
|
378
|
-
setPopupPosition(this.popup, position);
|
|
379
|
-
|
|
380
|
-
this.popup.classList.add('visible');
|
|
381
|
-
this.isVisible = true;
|
|
382
|
-
|
|
383
|
-
this.setupClickOutside();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Recalculate popup position to ensure it stays within viewport
|
|
388
|
-
*/
|
|
389
|
-
recalculatePosition() {
|
|
390
|
-
if (!this.currentAnchor || !this.isVisible) return;
|
|
391
|
-
|
|
392
|
-
// Small delay to ensure DOM updates are complete
|
|
393
|
-
setTimeout(() => {
|
|
394
|
-
const position = calculatePopupPosition(this.currentAnchor, this.popup, {
|
|
395
|
-
offsetY: 5,
|
|
396
|
-
offsetX: 0
|
|
397
|
-
});
|
|
398
|
-
setPopupPosition(this.popup, position);
|
|
399
|
-
}, 10);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
hide() {
|
|
403
|
-
this.popup.classList.remove('visible');
|
|
404
|
-
this.isVisible = false;
|
|
405
|
-
this.removeClickOutside();
|
|
406
|
-
// Clear saved selection to avoid memory leaks
|
|
407
|
-
this.savedSelection = null;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
destroy() {
|
|
411
|
-
this.removeClickOutside();
|
|
412
|
-
|
|
413
|
-
if (this.popup && this.popup.parentNode) {
|
|
414
|
-
this.popup.parentNode.removeChild(this.popup);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
this.popup = null;
|
|
418
|
-
this.isVisible = false;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export default VideoPopup;
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Popup Helper Utility
|
|
3
|
-
* Helps popups append to the yjd-rich-editor instead of document.body
|
|
4
|
-
* Now supports multiple editor instances with separate popup containers
|
|
5
|
-
*/
|
|
6
|
-
import Editor from '../core/editor.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Get the appropriate container for popups
|
|
10
|
-
* @param {string} editorId - Optional editor instance ID
|
|
11
|
-
* @returns {HTMLElement} Container element for popups
|
|
12
|
-
*/
|
|
13
|
-
export function getPopupContainer(editorId = null) {
|
|
14
|
-
let editor;
|
|
15
|
-
|
|
16
|
-
if (editorId) {
|
|
17
|
-
// Get specific editor instance
|
|
18
|
-
editor = Editor.getInstanceById(editorId);
|
|
19
|
-
} else {
|
|
20
|
-
// Try to get current editor instance
|
|
21
|
-
editor = Editor.getCurrentInstance();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (editor) {
|
|
25
|
-
return editor.getPopupContainer();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Fallback to document.body if no editor instance
|
|
29
|
-
return document.body;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Append popup to the appropriate container
|
|
34
|
-
* @param {HTMLElement} popup - Popup element to append
|
|
35
|
-
* @param {string} editorId - Optional editor instance ID
|
|
36
|
-
*/
|
|
37
|
-
export function appendPopup(popup, editorId = null) {
|
|
38
|
-
const container = getPopupContainer(editorId);
|
|
39
|
-
|
|
40
|
-
// Remove from current parent if exists
|
|
41
|
-
if (popup.parentNode) {
|
|
42
|
-
popup.parentNode.removeChild(popup);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
container.appendChild(popup);
|
|
46
|
-
|
|
47
|
-
// Note: pointer-events are now controlled by CSS rules
|
|
48
|
-
// Popup containers have pointer-events: none by default
|
|
49
|
-
// Interactive elements inside popups have pointer-events: auto
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get popup dimensions by temporarily showing it if needed
|
|
54
|
-
* @param {HTMLElement} popup - Popup element
|
|
55
|
-
* @returns {Object} Object with width and height
|
|
56
|
-
*/
|
|
57
|
-
function getPopupDimensions(popup) {
|
|
58
|
-
if (!popup) return { width: 300, height: 200 };
|
|
59
|
-
|
|
60
|
-
// Try getBoundingClientRect first
|
|
61
|
-
const rect = popup.getBoundingClientRect();
|
|
62
|
-
if (rect.width > 0 && rect.height > 0) {
|
|
63
|
-
return { width: rect.width, height: rect.height };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Try offsetWidth/offsetHeight
|
|
67
|
-
if (popup.offsetWidth > 0 && popup.offsetHeight > 0) {
|
|
68
|
-
return { width: popup.offsetWidth, height: popup.offsetHeight };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Check if popup is hidden
|
|
72
|
-
const computedStyle = window.getComputedStyle(popup);
|
|
73
|
-
const isHidden = computedStyle.display === 'none' || computedStyle.visibility === 'hidden';
|
|
74
|
-
|
|
75
|
-
if (isHidden) {
|
|
76
|
-
// Temporarily show popup to get dimensions
|
|
77
|
-
const originalDisplay = popup.style.display;
|
|
78
|
-
const originalVisibility = popup.style.visibility;
|
|
79
|
-
const originalPosition = popup.style.position;
|
|
80
|
-
const originalTop = popup.style.top;
|
|
81
|
-
const originalLeft = popup.style.left;
|
|
82
|
-
const originalZIndex = popup.style.zIndex;
|
|
83
|
-
|
|
84
|
-
// Make popup visible but off-screen
|
|
85
|
-
popup.style.display = 'block';
|
|
86
|
-
popup.style.visibility = 'visible';
|
|
87
|
-
popup.style.position = 'absolute';
|
|
88
|
-
popup.style.top = '-9999px';
|
|
89
|
-
popup.style.left = '-9999px';
|
|
90
|
-
popup.style.zIndex = '-1';
|
|
91
|
-
|
|
92
|
-
// Force reflow
|
|
93
|
-
popup.offsetHeight;
|
|
94
|
-
|
|
95
|
-
// Get dimensions
|
|
96
|
-
const tempRect = popup.getBoundingClientRect();
|
|
97
|
-
const width = tempRect.width > 0 ? tempRect.width : 300;
|
|
98
|
-
const height = tempRect.height > 0 ? tempRect.height : 200;
|
|
99
|
-
|
|
100
|
-
// Restore original styles
|
|
101
|
-
popup.style.display = originalDisplay;
|
|
102
|
-
popup.style.visibility = originalVisibility;
|
|
103
|
-
popup.style.position = originalPosition;
|
|
104
|
-
popup.style.top = originalTop;
|
|
105
|
-
popup.style.left = originalLeft;
|
|
106
|
-
popup.style.zIndex = originalZIndex;
|
|
107
|
-
|
|
108
|
-
return { width, height };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Last resort: try computed styles
|
|
112
|
-
const computedWidth = parseInt(computedStyle.width);
|
|
113
|
-
const computedHeight = parseInt(computedStyle.height);
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
width: computedWidth > 0 ? computedWidth : 300,
|
|
117
|
-
height: computedHeight > 0 ? computedHeight : 200
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Calculate position for popup relative to anchor element
|
|
123
|
-
* @param {HTMLElement} anchor - Anchor element
|
|
124
|
-
* @param {HTMLElement} popup - Popup element
|
|
125
|
-
* @param {Object} options - Positioning options
|
|
126
|
-
* @returns {Object} Position object with top and left values
|
|
127
|
-
*/
|
|
128
|
-
export function calculatePopupPosition(anchor, popup, options = {}) {
|
|
129
|
-
const {
|
|
130
|
-
offsetX = 0,
|
|
131
|
-
offsetY = 5,
|
|
132
|
-
preferTop = false,
|
|
133
|
-
preferLeft = false
|
|
134
|
-
} = options;
|
|
135
|
-
|
|
136
|
-
const anchorRect = anchor.getBoundingClientRect();
|
|
137
|
-
const container = getPopupContainer();
|
|
138
|
-
const isInWrapper = container.classList.contains('rich-editor-popup-container');
|
|
139
|
-
|
|
140
|
-
let top, left;
|
|
141
|
-
|
|
142
|
-
if (isInWrapper) {
|
|
143
|
-
// Position relative to wrapper
|
|
144
|
-
const wrapperRect = container.getBoundingClientRect();
|
|
145
|
-
|
|
146
|
-
// Calculate position relative to wrapper
|
|
147
|
-
top = anchorRect.top - wrapperRect.top + anchorRect.height + offsetY;
|
|
148
|
-
left = anchorRect.left - wrapperRect.left + offsetX;
|
|
149
|
-
|
|
150
|
-
// Get popup dimensions using the helper function
|
|
151
|
-
const { width: popupWidth, height: popupHeight } = getPopupDimensions(popup);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// Check if popup would overflow bottom of wrapper
|
|
155
|
-
if (top + popupHeight > wrapperRect.height && !preferTop) {
|
|
156
|
-
// Try to position above the anchor
|
|
157
|
-
const topPosition = anchorRect.top - wrapperRect.top - popupHeight - offsetY;
|
|
158
|
-
if (topPosition >= 0) {
|
|
159
|
-
top = topPosition;
|
|
160
|
-
} else {
|
|
161
|
-
// If still doesn't fit, try to center it vertically within the wrapper
|
|
162
|
-
top = Math.max(offsetY, (wrapperRect.height - popupHeight) / 2);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Check if popup would overflow right of wrapper
|
|
167
|
-
if (left + popupWidth + 5 > wrapperRect.width && !preferLeft) {
|
|
168
|
-
left = wrapperRect.width - popupWidth - offsetX -15;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Ensure popup doesn't go off-screen
|
|
172
|
-
if (left < 0) left = offsetX;
|
|
173
|
-
if (top < 0) top = offsetY;
|
|
174
|
-
|
|
175
|
-
} else {
|
|
176
|
-
// Fallback to document.body positioning
|
|
177
|
-
top = anchorRect.bottom + window.scrollY + offsetY;
|
|
178
|
-
left = anchorRect.left + window.scrollX + offsetX;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Get popup dimensions using the helper function
|
|
182
|
-
const { width: popupWidth, height: popupHeight } = getPopupDimensions(popup);
|
|
183
|
-
|
|
184
|
-
// Check if popup would overflow right edge
|
|
185
|
-
if (left + popupWidth > window.innerWidth && !preferLeft) {
|
|
186
|
-
left = window.innerWidth - popupWidth - offsetX;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Check if popup would overflow bottom edge
|
|
190
|
-
if (top + popupHeight > window.innerHeight + window.scrollY && !preferTop) {
|
|
191
|
-
// Try to position above the anchor
|
|
192
|
-
const topPosition = anchorRect.top + window.scrollY - popupHeight - offsetY;
|
|
193
|
-
if (topPosition >= window.scrollY) {
|
|
194
|
-
top = topPosition;
|
|
195
|
-
} else {
|
|
196
|
-
// If still doesn't fit, try to center it vertically within the viewport
|
|
197
|
-
top = Math.max(window.scrollY + offsetY, window.scrollY + (window.innerHeight - popupHeight) / 2);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Ensure popup doesn't go off-screen
|
|
202
|
-
if (left < 0) left = offsetX;
|
|
203
|
-
if (top < 0) top = offsetY;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return { top, left };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Set popup position
|
|
211
|
-
* @param {HTMLElement} popup - Popup element
|
|
212
|
-
* @param {Object} position - Position object with top and left values
|
|
213
|
-
*/
|
|
214
|
-
export function setPopupPosition(popup, position) {
|
|
215
|
-
popup.style.position = 'absolute';
|
|
216
|
-
popup.style.top = `${position.top}px`;
|
|
217
|
-
popup.style.left = `${position.left}px`;
|
|
218
|
-
popup.style.zIndex = '1000';
|
|
219
|
-
}
|