@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
package/lib/ui/icons.js
ADDED
|
@@ -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;
|