@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.
Files changed (70) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +146 -142
  3. package/core.js +77 -0
  4. package/dist/core.esm.js +2 -0
  5. package/dist/core.esm.js.map +1 -0
  6. package/dist/rich-editor.esm.js +1 -1
  7. package/dist/rich-editor.esm.js.map +1 -1
  8. package/dist/rich-editor.min.js +1 -1
  9. package/dist/rich-editor.min.js.map +1 -1
  10. package/index.d.ts +134 -103
  11. package/index.js +227 -0
  12. package/lib/core/editor.js +1806 -0
  13. package/lib/core/format.js +540 -0
  14. package/lib/core/module.js +81 -0
  15. package/lib/core/registry.js +158 -0
  16. package/lib/formats/background.js +213 -0
  17. package/lib/formats/bold.js +49 -0
  18. package/lib/formats/capitalization.js +579 -0
  19. package/lib/formats/color.js +183 -0
  20. package/lib/formats/emoji.js +282 -0
  21. package/lib/formats/font-family.js +548 -0
  22. package/lib/formats/heading.js +502 -0
  23. package/lib/formats/image.js +347 -0
  24. package/lib/formats/import.js +385 -0
  25. package/lib/formats/indent.js +297 -0
  26. package/lib/formats/italic.js +27 -0
  27. package/lib/formats/line-height.js +562 -0
  28. package/lib/formats/link.js +251 -0
  29. package/lib/formats/list.js +635 -0
  30. package/lib/formats/strike.js +31 -0
  31. package/lib/formats/subscript.js +40 -0
  32. package/lib/formats/superscript.js +39 -0
  33. package/lib/formats/table.js +293 -0
  34. package/lib/formats/tag.js +304 -0
  35. package/lib/formats/text-align.js +422 -0
  36. package/lib/formats/text-size.js +498 -0
  37. package/lib/formats/underline.js +30 -0
  38. package/lib/formats/video.js +381 -0
  39. package/lib/modules/block-toolbar.js +639 -0
  40. package/lib/modules/code-view.js +447 -0
  41. package/lib/modules/find-replace.js +273 -0
  42. package/lib/modules/history.js +425 -0
  43. package/lib/modules/resize-handles.js +701 -0
  44. package/lib/modules/slash-menu.js +183 -0
  45. package/lib/modules/table-toolbar.js +635 -0
  46. package/lib/modules/toolbar.js +607 -0
  47. package/lib/styles-loader.js +142 -0
  48. package/{dist → lib}/styles.css +1285 -35
  49. package/lib/styles.css.js +2 -0
  50. package/lib/styles.min.css +1 -0
  51. package/lib/ui/color-picker.js +296 -0
  52. package/lib/ui/customselect.js +351 -0
  53. package/lib/ui/emoji-picker.js +196 -0
  54. package/lib/ui/icons.js +145 -0
  55. package/lib/ui/image-popup.js +435 -0
  56. package/lib/ui/import-popup.js +288 -0
  57. package/lib/ui/link-popup.js +139 -0
  58. package/lib/ui/list-picker.js +307 -0
  59. package/lib/ui/select-button.js +68 -0
  60. package/lib/ui/table-popup.js +171 -0
  61. package/lib/ui/tag-popup.js +249 -0
  62. package/lib/ui/text-align-picker.js +278 -0
  63. package/lib/ui/video-popup.js +413 -0
  64. package/lib/utils/exec-command.js +72 -0
  65. package/lib/utils/history-helper.js +50 -0
  66. package/lib/utils/popup-helper.js +219 -0
  67. package/lib/utils/popup-positioning.js +234 -0
  68. package/lib/utils/sanitize.js +164 -0
  69. package/package.json +51 -32
  70. package/umd-entry.js +18 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Inline Icons — a single, cohesive outline icon set (Lucide-style).
3
+ * Every icon is a 24×24, stroke-based glyph using `currentColor`, so they all
4
+ * share one visual weight and follow the button's text/accent colour.
5
+ */
6
+ const S = (body) =>
7
+ `<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">${body}</svg>`;
8
+
9
+ export const Icons = {
10
+ // --- Text formatting ---
11
+ bold: S('<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>'),
12
+ italic: S('<line x1="19" x2="10" y1="4" y2="4"/><line x1="14" x2="5" y1="20" y2="20"/><line x1="15" x2="9" y1="4" y2="20"/>'),
13
+ underline: S('<path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" x2="20" y1="20" y2="20"/>'),
14
+ strike: S('<path d="M16 4H9a3 3 0 0 0-2.83 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" x2="20" y1="12" y2="12"/>'),
15
+ subscript: S('<path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/>'),
16
+ superscript: S('<path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.44-2 1.5-2.5S20 8.33 20 7c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/>'),
17
+
18
+ // --- Alignment ---
19
+ 'align-left': S('<line x1="21" x2="3" y1="6" y2="6"/><line x1="15" x2="3" y1="12" y2="12"/><line x1="17" x2="3" y1="18" y2="18"/>'),
20
+ 'align-center': S('<line x1="21" x2="3" y1="6" y2="6"/><line x1="17" x2="7" y1="12" y2="12"/><line x1="19" x2="5" y1="18" y2="18"/>'),
21
+ 'align-right': S('<line x1="21" x2="3" y1="6" y2="6"/><line x1="21" x2="9" y1="12" y2="12"/><line x1="21" x2="7" y1="18" y2="18"/>'),
22
+ 'align-justify': S('<line x1="3" x2="21" y1="6" y2="6"/><line x1="3" x2="21" y1="12" y2="12"/><line x1="3" x2="21" y1="18" y2="18"/>'),
23
+
24
+ // --- Lists ---
25
+ 'list-bullet': S('<line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><circle cx="3.5" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="3.5" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="3.5" cy="18" r="1" fill="currentColor" stroke="none"/>'),
26
+ 'list-ordered': S('<line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'),
27
+ 'list-alpha': S('<line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 10V8a1 1 0 0 1 2 0v2"/><path d="M4 9h2"/><path d="M4 14h1.5a1 1 0 0 1 0 2H4l2-2"/>'),
28
+ 'list-roman': S('<line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M5 7v3"/><path d="M4 14h2l-1 4"/>'),
29
+ list: S('<line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><circle cx="3.5" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="3.5" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="3.5" cy="18" r="1" fill="currentColor" stroke="none"/>'),
30
+
31
+ // --- Indentation ---
32
+ 'indent-increase': S('<polyline points="3 8 7 12 3 16"/><line x1="21" x2="11" y1="6" y2="6"/><line x1="21" x2="11" y1="12" y2="12"/><line x1="21" x2="11" y1="18" y2="18"/>'),
33
+ 'indent-decrease': S('<polyline points="7 8 3 12 7 16"/><line x1="21" x2="11" y1="6" y2="6"/><line x1="21" x2="11" y1="12" y2="12"/><line x1="21" x2="11" y1="18" y2="18"/>'),
34
+
35
+ // --- Media ---
36
+ image: S('<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>'),
37
+ video: S('<path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/>'),
38
+
39
+ // --- Table ---
40
+ table: S('<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>'),
41
+ 'table-profile': S('<path d="M15 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3"/><path d="M21 15H3"/>'),
42
+ 'add-row-above': S('<rect x="3" y="13" width="18" height="8" rx="2"/><line x1="12" x2="12" y1="3" y2="9"/><line x1="9" x2="15" y1="6" y2="6"/>'),
43
+ 'add-row-below': S('<rect x="3" y="3" width="18" height="8" rx="2"/><line x1="12" x2="12" y1="15" y2="21"/><line x1="9" x2="15" y1="18" y2="18"/>'),
44
+ 'add-col-left': S('<rect x="13" y="3" width="8" height="18" rx="2"/><line x1="3" x2="9" y1="12" y2="12"/><line x1="6" x2="6" y1="9" y2="15"/>'),
45
+ 'add-col-right': S('<rect x="3" y="3" width="8" height="18" rx="2"/><line x1="15" x2="21" y1="12" y2="12"/><line x1="18" x2="18" y1="9" y2="15"/>'),
46
+ 'delete-row': S('<rect x="3" y="9" width="18" height="6" rx="2"/><line x1="9" x2="15" y1="12" y2="12"/>'),
47
+ 'delete-col': S('<rect x="9" y="3" width="6" height="18" rx="2"/><line x1="12" x2="12" y1="9" y2="15"/>'),
48
+ 'delete-table': S('<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>'),
49
+
50
+ // --- Colour ---
51
+ color: S('<path d="M5.5 19 12 5l6.5 14"/><path d="M8 14h8"/>'),
52
+ background: S('<path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"/>'),
53
+ 'no-color': S('<circle cx="12" cy="12" r="9"/><line x1="5.6" x2="18.4" y1="5.6" y2="18.4"/>'),
54
+ 'custom-color': S('<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.93 0 1.65-.75 1.65-1.69 0-.43-.18-.83-.44-1.12-.29-.29-.44-.65-.44-1.13a1.64 1.64 0 0 1 1.67-1.67h2c3.05 0 5.55-2.5 5.55-5.55C22 6 17.5 2 12 2z"/><circle cx="8.5" cy="7.5" r="1" fill="currentColor" stroke="none"/><circle cx="6.5" cy="12.5" r="1" fill="currentColor" stroke="none"/><circle cx="13.5" cy="6.5" r="1" fill="currentColor" stroke="none"/><circle cx="17.5" cy="10.5" r="1" fill="currentColor" stroke="none"/>'),
55
+
56
+ // --- History ---
57
+ undo: S('<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>'),
58
+ redo: S('<path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/>'),
59
+
60
+ // --- Insert ---
61
+ link: S('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'),
62
+ emoji: S('<circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/>'),
63
+ tag: S('<path d="M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4z"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor" stroke="none"/>'),
64
+ import: S('<path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><path d="M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4"/>'),
65
+ code: S('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
66
+ 'code-view': S('<path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/>'),
67
+ 'clear-format': S('<path d="M4 7V4h16v3"/><path d="M5 20h6"/><path d="M13 4 8 20"/><path d="m15 15 5 5"/><path d="m20 15-5 5"/>'),
68
+ 'horizontal-rule': S('<path d="M5 12h14"/>'),
69
+ find: S('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>'),
70
+ 'chevron-up': S('<path d="m18 15-6-6-6 6"/>'),
71
+ 'chevron-down': S('<path d="m6 9 6 6 6-6"/>'),
72
+ close: S('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
73
+ 'text-direction': S('<path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>'),
74
+
75
+ // --- UI / utility ---
76
+ check: S('<polyline points="20 6 9 17 4 12"/>'),
77
+ dropdown: S('<path d="m6 9 6 6 6-6"/>'),
78
+ more: S('<circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none"/>'),
79
+ theme: S('<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.9 4.9 1.4 1.4"/><path d="m17.7 17.7 1.4 1.4"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.3 17.7-1.4 1.4"/><path d="m19.1 4.9-1.4 1.4"/>'),
80
+
81
+ // --- Typography (dropdown triggers; mostly shown as text) ---
82
+ heading: S('<path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="m17 12 3-2v8"/>'),
83
+ 'font-family': S('<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/>'),
84
+ 'line-height': S('<path d="M3 5h12"/><path d="M3 12h12"/><path d="M3 19h12"/><path d="M19 5v14"/><path d="m16.5 7.5 2.5-2.5 2.5 2.5"/><path d="m16.5 16.5 2.5 2.5 2.5-2.5"/>'),
85
+ capitalization: S('<path d="M4 18 8 8l4 10"/><path d="M5.5 14h5"/><path d="M16 18a3 3 0 1 0 0-6 3 3 0 0 0-3 3v3"/><path d="M19 12v6"/>'),
86
+ 'text-size': S('<path d="M21 14h-5"/><path d="M16 16v-3.5a2.5 2.5 0 0 1 5 0V16"/><path d="M4.5 13h6"/><path d="m3 16 4.5-9 4.5 9"/>')
87
+ };
88
+
89
+ /**
90
+ * Icon utility functions
91
+ */
92
+ export class IconUtils {
93
+ /**
94
+ * Get icon SVG content by name
95
+ * @param {string} iconName - Name of the icon
96
+ * @returns {string} SVG content or empty string if not found
97
+ */
98
+ static getIcon(iconName) {
99
+ return Icons[iconName] || '';
100
+ }
101
+
102
+ /**
103
+ * Create icon element with proper styling
104
+ * @param {string} iconName - Name of the icon
105
+ * @param {Object} options - Options for icon styling
106
+ * @returns {HTMLElement} Icon element
107
+ */
108
+ static createIconElement(iconName, options = {}) {
109
+ const iconElement = document.createElement('span');
110
+ iconElement.className = `icon icon-${iconName}`;
111
+
112
+ // Apply default styles
113
+ iconElement.style.display = 'inline-flex';
114
+ iconElement.style.alignItems = 'center';
115
+ iconElement.style.justifyContent = 'center';
116
+ iconElement.style.width = options.width || '16px';
117
+ iconElement.style.height = options.height || '16px';
118
+ iconElement.style.verticalAlign = 'middle';
119
+
120
+ // Set SVG content
121
+ iconElement.innerHTML = this.getIcon(iconName);
122
+
123
+ return iconElement;
124
+ }
125
+
126
+ /**
127
+ * Check if icon exists
128
+ * @param {string} iconName - Name of the icon
129
+ * @returns {boolean} True if icon exists
130
+ */
131
+ static hasIcon(iconName) {
132
+ return iconName in Icons;
133
+ }
134
+
135
+ /**
136
+ * Get all available icon names
137
+ * @returns {string[]} Array of icon names
138
+ */
139
+ static getIconNames() {
140
+ return Object.keys(Icons);
141
+ }
142
+ }
143
+
144
+ // Export default for backward compatibility
145
+ export default IconUtils;
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Image Popup Component - Popup for inserting images
3
+ */
4
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
5
+
6
+ class ImagePopup {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ onImageInsert: null,
10
+ editor: null,
11
+ ...options
12
+ };
13
+
14
+ this.popup = null;
15
+ this.isVisible = false;
16
+ this.clickOutsideHandler = null;
17
+ this.selectedImageSrc = null;
18
+ this.savedSelection = null; // Save editor selection
19
+ this.resizeHandler = null;
20
+
21
+ this.createImagePopup();
22
+ }
23
+
24
+ /**
25
+ * Create image popup
26
+ */
27
+ createImagePopup() {
28
+ this.popup = document.createElement('div');
29
+ this.popup.className = 'image-popup';
30
+
31
+ const content = document.createElement('div');
32
+ content.className = 'image-popup-content';
33
+
34
+ // Title
35
+ const title = document.createElement('h3');
36
+ title.textContent = 'Insert image';
37
+ title.className = 'yjd-input-title';
38
+ content.appendChild(title);
39
+
40
+ // Container
41
+ const uploadContainer = document.createElement('div');
42
+ uploadContainer.className = 'image-input-container';
43
+
44
+ const textLabel = document.createElement('p');
45
+ textLabel.textContent = 'Your image url';
46
+ textLabel.className = 'yjd-input-label';
47
+
48
+ const inputgroup1 = document.createElement('div');
49
+ inputgroup1.className = 'yjd-input-upload-group';
50
+ this.inputGroup = inputgroup1; // Store reference
51
+
52
+
53
+ // input url
54
+ this.urlInput = document.createElement('input');
55
+ this.urlInput.type = 'url';
56
+ this.urlInput.className = 'yjd-input';
57
+ this.urlInput.placeholder = 'Please enter your image URL';
58
+
59
+
60
+ // Hidden file input
61
+ this.fileInput = document.createElement('input');
62
+ this.fileInput.type = 'file';
63
+ this.fileInput.accept = 'image/*';
64
+ this.fileInput.className = 'image-input-hidden'; // ẩn bằng CSS
65
+ this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
66
+
67
+ // Custom button
68
+ const customButton = document.createElement('button');
69
+ 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>`;
70
+ customButton.className = 'yjd-custom-upload-button';
71
+ this.customButton = customButton;
72
+ customButton.addEventListener('click', () => this.fileInput.click());
73
+
74
+ // Create preview container
75
+ this.createPreviewContainer();
76
+
77
+ // Append elements
78
+ inputgroup1.appendChild(this.urlInput);
79
+ inputgroup1.appendChild(this.fileInput);
80
+ inputgroup1.appendChild(customButton);
81
+ uploadContainer.appendChild(textLabel);
82
+ uploadContainer.appendChild(inputgroup1);
83
+ uploadContainer.appendChild(this.previewContainer);
84
+ content.appendChild(uploadContainer);
85
+ this.urlInput.addEventListener('input', () => {
86
+ this.updateInsertButton();
87
+ // Show preview if URL is valid
88
+ const url = this.urlInput.value.trim();
89
+ if (url && this.isValidImageUrl(url)) {
90
+ this.showPreview(url);
91
+ } else {
92
+ this.removePreview();
93
+ }
94
+ if(this.urlInput.value.trim()){
95
+ this.customButton.style.display = 'none';
96
+ }else{
97
+ this.customButton.style.display = 'flex';
98
+ }
99
+ });
100
+ // Buttons
101
+ const buttonContainer = document.createElement('div');
102
+ buttonContainer.className = 'yjd-button-container';
103
+
104
+ const cancelButton = document.createElement('button');
105
+ cancelButton.type = 'button';
106
+ cancelButton.className = 'image-button yjd-button-cancel';
107
+ cancelButton.textContent = 'Cancel';
108
+ cancelButton.addEventListener('click', () => {
109
+ this.hide();
110
+ // Maintain editor focus after popup close
111
+ if (this.options.editor) {
112
+ setTimeout(() => this.options.editor.focus(), 0);
113
+ }
114
+ });
115
+
116
+ this.insertButton = document.createElement('button');
117
+ this.insertButton.type = 'button';
118
+ this.insertButton.className = 'image-button yjd-button-confirm button-disable';
119
+ this.insertButton.textContent = 'Add image';
120
+ this.insertButton.disabled = true;
121
+ this.insertButton.addEventListener('click', () => {
122
+ this.insertImage();
123
+ // Maintain editor focus after insert
124
+ if (this.options.editor) {
125
+ setTimeout(() => this.options.editor.focus(), 0);
126
+ }
127
+ });
128
+
129
+ buttonContainer.appendChild(cancelButton);
130
+ buttonContainer.appendChild(this.insertButton);
131
+ content.appendChild(buttonContainer);
132
+
133
+ this.popup.appendChild(content);
134
+ appendPopup(this.popup);
135
+
136
+ // Prevent focus loss when clicking on popup
137
+ if (this.options.editor && typeof this.options.editor.preventFocusLoss === 'function') {
138
+ this.options.editor.preventFocusLoss(this.popup);
139
+ }
140
+ }
141
+
142
+ async handleFileSelect(e) {
143
+ const file = e.target.files[0];
144
+ if (!file) return;
145
+
146
+ try {
147
+ const { default: Image } = await import('../formats/image.js');
148
+ this.selectedImageSrc = await Image.handleFileUpload(file);
149
+ this.urlInput.value = '';
150
+ this.showPreview(this.selectedImageSrc);
151
+ this.updateInsertButton();
152
+ } catch (error) {
153
+ alert(error.message);
154
+ }
155
+ }
156
+
157
+ updateInsertButton() {
158
+ const hasImage = this.selectedImageSrc || this.urlInput.value.trim();
159
+ this.insertButton.disabled = !hasImage;
160
+ this.insertButton.classList.toggle('button-disable', !hasImage);
161
+ }
162
+
163
+ /**
164
+ * Show image preview
165
+ */
166
+ showPreview(imageSrc) {
167
+ if (!imageSrc) return;
168
+
169
+ this.imagePreview.src = imageSrc;
170
+ this.previewContainer.style.display = 'block';
171
+ this.selectedImageSrc = imageSrc;
172
+
173
+ // Hide input group
174
+ this.toggleInputGroup(false);
175
+
176
+ // Recalculate position after preview is shown to ensure buttons remain visible
177
+ this.recalculatePosition();
178
+ }
179
+
180
+ /**
181
+ * Remove image preview and show input again
182
+ */
183
+ removePreview() {
184
+ this.selectedImageSrc = null;
185
+ this.previewContainer.style.display = 'none';
186
+ this.imagePreview.src = '';
187
+
188
+ // Show input group and reset file input
189
+ this.toggleInputGroup(true);
190
+ if (this.fileInput) {
191
+ this.fileInput.value = '';
192
+ }
193
+
194
+ this.updateInsertButton();
195
+
196
+ // Recalculate position after preview is removed
197
+ this.recalculatePosition();
198
+ }
199
+
200
+ /**
201
+ * Toggle input group visibility
202
+ */
203
+ toggleInputGroup(show) {
204
+ if (!this.inputGroup) return;
205
+
206
+ if (show) {
207
+ this.inputGroup.style.display = 'flex';
208
+ this.inputGroup.style.visibility = 'visible';
209
+ if (this.customButton) {
210
+ this.customButton.style.pointerEvents = 'auto';
211
+ }
212
+ } else {
213
+ this.inputGroup.style.display = 'none';
214
+ this.inputGroup.style.visibility = 'hidden';
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Create preview container with image and remove button
220
+ */
221
+ createPreviewContainer() {
222
+ this.previewContainer = document.createElement('div');
223
+ this.previewContainer.className = 'image-preview-container';
224
+ this.previewContainer.style.cssText = 'display: none; position: relative;';
225
+
226
+ // Image preview
227
+ this.imagePreview = document.createElement('img');
228
+ this.imagePreview.className = 'image-preview';
229
+ this.imagePreview.style.cssText = 'max-width: 100%; max-height: 200px; border-radius: 8px; object-fit: contain;';
230
+
231
+ // Remove button
232
+ this.removeButton = document.createElement('button');
233
+ this.removeButton.className = 'image-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.imagePreview);
243
+ this.previewContainer.appendChild(this.removeButton);
244
+ }
245
+
246
+ /**
247
+ * Check if URL is a valid image URL
248
+ */
249
+ isValidImageUrl(url) {
250
+ try {
251
+ const urlObj = new URL(url);
252
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'];
253
+ const imageHosts = ['imgur.com', 'images.unsplash.com', 'picsum.photos', 'via.placeholder.com'];
254
+
255
+ const pathname = urlObj.pathname.toLowerCase();
256
+ const hasImageExtension = imageExtensions.some(ext => pathname.endsWith(ext));
257
+ const isFromImageHost = imageHosts.some(host => urlObj.hostname.includes(host));
258
+
259
+ return hasImageExtension || isFromImageHost;
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+
265
+ async insertImage() {
266
+ let src = this.selectedImageSrc || this.urlInput.value.trim();
267
+ const alt = '';
268
+
269
+ if (!src) return;
270
+
271
+ // Always validate URL (both file upload and URL input)
272
+ try {
273
+ const { default: Image } = await import('../formats/image.js');
274
+ const isValid = await Image.validateImageUrl(src);
275
+ if (!isValid) {
276
+ alert('Invalid image URL. Please check the URL and try again.');
277
+ return;
278
+ }
279
+ } catch (error) {
280
+ alert('Error validating image URL.');
281
+ return;
282
+ }
283
+
284
+ // Restore editor selection before inserting
285
+ this.restoreSelection();
286
+
287
+ if (this.options.onImageInsert) {
288
+ this.options.onImageInsert(src, alt);
289
+ }
290
+
291
+ this.hide();
292
+ this.reset();
293
+ }
294
+
295
+ reset() {
296
+ this.fileInput.value = '';
297
+ this.urlInput.value = '';
298
+ this.selectedImageSrc = null;
299
+
300
+ // Hide preview and show input
301
+ this.previewContainer.style.display = 'none';
302
+ this.imagePreview.src = '';
303
+ this.toggleInputGroup(true);
304
+
305
+ this.updateInsertButton();
306
+ this.customButton.style.display = 'block';
307
+ }
308
+
309
+ /**
310
+ * Save current editor selection
311
+ */
312
+ saveSelection() {
313
+ const selection = window.getSelection();
314
+ if (selection && selection.rangeCount > 0) {
315
+ this.savedSelection = selection.getRangeAt(0).cloneRange();
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Restore editor selection
321
+ */
322
+ restoreSelection() {
323
+ if (this.savedSelection) {
324
+ const selection = window.getSelection();
325
+ selection.removeAllRanges();
326
+ selection.addRange(this.savedSelection);
327
+ }
328
+ }
329
+
330
+ setupClickOutside() {
331
+ if (this.clickOutsideHandler) {
332
+ document.removeEventListener('click', this.clickOutsideHandler);
333
+ }
334
+
335
+ this.clickOutsideHandler = (e) => {
336
+ if (!this.popup.contains(e.target)) {
337
+ this.hide();
338
+ }
339
+ };
340
+
341
+ setTimeout(() => {
342
+ document.addEventListener('click', this.clickOutsideHandler);
343
+ }, 100);
344
+ }
345
+
346
+ setupResizeHandler() {
347
+ if (this.resizeHandler) {
348
+ window.removeEventListener('resize', this.resizeHandler);
349
+ }
350
+
351
+ this.resizeHandler = () => {
352
+ if (this.isVisible) {
353
+ this.recalculatePosition();
354
+ }
355
+ };
356
+
357
+ window.addEventListener('resize', this.resizeHandler);
358
+ }
359
+
360
+ removeResizeHandler() {
361
+ if (this.resizeHandler) {
362
+ window.removeEventListener('resize', this.resizeHandler);
363
+ this.resizeHandler = null;
364
+ }
365
+ }
366
+
367
+ removeClickOutside() {
368
+ if (this.clickOutsideHandler) {
369
+ document.removeEventListener('click', this.clickOutsideHandler);
370
+ this.clickOutsideHandler = null;
371
+ }
372
+ }
373
+
374
+ show(anchor) {
375
+ if (!anchor) return;
376
+
377
+ // Save current editor selection before showing popup
378
+ this.saveSelection();
379
+
380
+ // Reset state when showing popup
381
+ this.reset();
382
+
383
+ // Store anchor for recalculation
384
+ this.currentAnchor = anchor;
385
+
386
+ // Calculate and set popup position
387
+ const position = calculatePopupPosition(anchor, this.popup, {
388
+ offsetY: 5,
389
+ offsetX: 0
390
+ });
391
+ setPopupPosition(this.popup, position);
392
+
393
+ this.popup.classList.add('visible');
394
+ this.isVisible = true;
395
+
396
+ this.setupClickOutside();
397
+ }
398
+
399
+ /**
400
+ * Recalculate popup position to ensure it stays within viewport
401
+ */
402
+ recalculatePosition() {
403
+ if (!this.currentAnchor || !this.isVisible) return;
404
+
405
+ // Small delay to ensure DOM updates are complete
406
+ setTimeout(() => {
407
+ const position = calculatePopupPosition(this.currentAnchor, this.popup, {
408
+ offsetY: 5,
409
+ offsetX: 0
410
+ });
411
+ setPopupPosition(this.popup, position);
412
+ }, 10);
413
+ }
414
+
415
+ hide() {
416
+ this.popup.classList.remove('visible');
417
+ this.isVisible = false;
418
+ this.removeClickOutside();
419
+ // Clear saved selection to avoid memory leaks
420
+ this.savedSelection = null;
421
+ }
422
+
423
+ destroy() {
424
+ this.removeClickOutside();
425
+
426
+ if (this.popup && this.popup.parentNode) {
427
+ this.popup.parentNode.removeChild(this.popup);
428
+ }
429
+
430
+ this.popup = null;
431
+ this.isVisible = false;
432
+ }
433
+ }
434
+
435
+ export default ImagePopup;