@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.
Files changed (58) hide show
  1. package/README.md +73 -22
  2. package/dist/rich-editor.esm.js +2 -0
  3. package/dist/rich-editor.esm.js.map +1 -0
  4. package/dist/rich-editor.min.js +2 -0
  5. package/dist/rich-editor.min.js.map +1 -0
  6. package/package.json +12 -7
  7. package/index.js +0 -221
  8. package/lib/core/editor.js +0 -1175
  9. package/lib/core/format.js +0 -542
  10. package/lib/core/module.js +0 -81
  11. package/lib/core/registry.js +0 -152
  12. package/lib/formats/background.js +0 -212
  13. package/lib/formats/bold.js +0 -67
  14. package/lib/formats/capitalization.js +0 -563
  15. package/lib/formats/color.js +0 -165
  16. package/lib/formats/emoji.js +0 -282
  17. package/lib/formats/font-family.js +0 -547
  18. package/lib/formats/heading.js +0 -502
  19. package/lib/formats/image.js +0 -344
  20. package/lib/formats/import.js +0 -385
  21. package/lib/formats/indent.js +0 -297
  22. package/lib/formats/italic.js +0 -27
  23. package/lib/formats/line-height.js +0 -558
  24. package/lib/formats/link.js +0 -251
  25. package/lib/formats/list.js +0 -635
  26. package/lib/formats/strike.js +0 -31
  27. package/lib/formats/subscript.js +0 -36
  28. package/lib/formats/superscript.js +0 -35
  29. package/lib/formats/table.js +0 -288
  30. package/lib/formats/tag.js +0 -304
  31. package/lib/formats/text-align.js +0 -421
  32. package/lib/formats/text-size.js +0 -497
  33. package/lib/formats/underline.js +0 -30
  34. package/lib/formats/video.js +0 -372
  35. package/lib/modules/block-toolbar.js +0 -628
  36. package/lib/modules/code-view.js +0 -434
  37. package/lib/modules/history.js +0 -410
  38. package/lib/modules/resize-handles.js +0 -677
  39. package/lib/modules/table-toolbar.js +0 -618
  40. package/lib/modules/toolbar.js +0 -424
  41. package/lib/styles-loader.js +0 -144
  42. package/lib/styles.css +0 -2123
  43. package/lib/ui/color-picker.js +0 -296
  44. package/lib/ui/customselect.js +0 -319
  45. package/lib/ui/emoji-picker.js +0 -196
  46. package/lib/ui/icons.js +0 -413
  47. package/lib/ui/image-popup.js +0 -444
  48. package/lib/ui/import-popup.js +0 -288
  49. package/lib/ui/link-popup.js +0 -191
  50. package/lib/ui/list-picker.js +0 -307
  51. package/lib/ui/select-button.js +0 -61
  52. package/lib/ui/table-popup.js +0 -171
  53. package/lib/ui/tag-popup.js +0 -249
  54. package/lib/ui/text-align-picker.js +0 -281
  55. package/lib/ui/video-popup.js +0 -422
  56. package/lib/utils/history-helper.js +0 -50
  57. package/lib/utils/popup-helper.js +0 -219
  58. package/lib/utils/popup-positioning.js +0 -231
@@ -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
- }