@oix1987/yjd 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +146 -142
- package/core.js +77 -0
- package/dist/core.esm.js +2 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +134 -103
- package/index.js +227 -0
- package/lib/core/editor.js +1806 -0
- package/lib/core/format.js +540 -0
- package/lib/core/module.js +81 -0
- package/lib/core/registry.js +158 -0
- package/lib/formats/background.js +213 -0
- package/lib/formats/bold.js +49 -0
- package/lib/formats/capitalization.js +579 -0
- package/lib/formats/color.js +183 -0
- package/lib/formats/emoji.js +282 -0
- package/lib/formats/font-family.js +548 -0
- package/lib/formats/heading.js +502 -0
- package/lib/formats/image.js +347 -0
- package/lib/formats/import.js +385 -0
- package/lib/formats/indent.js +297 -0
- package/lib/formats/italic.js +27 -0
- package/lib/formats/line-height.js +562 -0
- package/lib/formats/link.js +251 -0
- package/lib/formats/list.js +635 -0
- package/lib/formats/strike.js +31 -0
- package/lib/formats/subscript.js +40 -0
- package/lib/formats/superscript.js +39 -0
- package/lib/formats/table.js +293 -0
- package/lib/formats/tag.js +304 -0
- package/lib/formats/text-align.js +422 -0
- package/lib/formats/text-size.js +498 -0
- package/lib/formats/underline.js +30 -0
- package/lib/formats/video.js +381 -0
- package/lib/modules/block-toolbar.js +639 -0
- package/lib/modules/code-view.js +447 -0
- package/lib/modules/find-replace.js +273 -0
- package/lib/modules/history.js +425 -0
- package/lib/modules/resize-handles.js +701 -0
- package/lib/modules/slash-menu.js +183 -0
- package/lib/modules/table-toolbar.js +635 -0
- package/lib/modules/toolbar.js +607 -0
- package/lib/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1285 -35
- package/lib/styles.css.js +2 -0
- package/lib/styles.min.css +1 -0
- package/lib/ui/color-picker.js +296 -0
- package/lib/ui/customselect.js +351 -0
- package/lib/ui/emoji-picker.js +196 -0
- package/lib/ui/icons.js +145 -0
- package/lib/ui/image-popup.js +435 -0
- package/lib/ui/import-popup.js +288 -0
- package/lib/ui/link-popup.js +139 -0
- package/lib/ui/list-picker.js +307 -0
- package/lib/ui/select-button.js +68 -0
- package/lib/ui/table-popup.js +171 -0
- package/lib/ui/tag-popup.js +249 -0
- package/lib/ui/text-align-picker.js +278 -0
- package/lib/ui/video-popup.js +413 -0
- package/lib/utils/exec-command.js +72 -0
- package/lib/utils/history-helper.js +50 -0
- package/lib/utils/popup-helper.js +219 -0
- package/lib/utils/popup-positioning.js +234 -0
- package/lib/utils/sanitize.js +164 -0
- package/package.json +51 -32
- package/umd-entry.js +18 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import IconUtils from './icons.js';
|
|
2
|
+
import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
|
|
3
|
+
import Editor from '../core/editor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom Select Component - Reusable dropdown/popup select component
|
|
7
|
+
*/
|
|
8
|
+
class CustomSelect {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = {
|
|
11
|
+
items: [], // Array of items to display
|
|
12
|
+
onItemSelect: null, // Callback when item is selected
|
|
13
|
+
displayProperty: 'label', // Property to display as text
|
|
14
|
+
valueProperty: 'value', // Property to use as value
|
|
15
|
+
className: 'custom-select', // CSS class for the popup
|
|
16
|
+
title: '', // Optional header label shown at the top of the popup
|
|
17
|
+
width: 200, // Popup width
|
|
18
|
+
height: 280, // Popup height
|
|
19
|
+
...options
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
this.popup = null;
|
|
23
|
+
this.isVisible = false;
|
|
24
|
+
this.currentValue = null;
|
|
25
|
+
this.clickOutsideHandler = null;
|
|
26
|
+
this.initialized = false;
|
|
27
|
+
|
|
28
|
+
this.createSelect();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create select popup
|
|
33
|
+
*/
|
|
34
|
+
createSelect() {
|
|
35
|
+
// Create popup
|
|
36
|
+
this.popup = document.createElement('div');
|
|
37
|
+
this.popup.className = `${this.options.className}-popup`;
|
|
38
|
+
|
|
39
|
+
// Optional header so it's clear what the dropdown controls.
|
|
40
|
+
if (this.options.title) {
|
|
41
|
+
const header = document.createElement('div');
|
|
42
|
+
header.className = 'custom-select-header';
|
|
43
|
+
header.textContent = this.options.title;
|
|
44
|
+
this.popup.appendChild(header);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add popup to container
|
|
48
|
+
appendPopup(this.popup);
|
|
49
|
+
|
|
50
|
+
// Initialize async
|
|
51
|
+
this.init();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize component with async operations
|
|
56
|
+
*/
|
|
57
|
+
async init() {
|
|
58
|
+
// Create item list
|
|
59
|
+
await this.createItemList();
|
|
60
|
+
this.initialized = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create item list
|
|
65
|
+
*/
|
|
66
|
+
async createItemList() {
|
|
67
|
+
const list = document.createElement('div');
|
|
68
|
+
list.className = 'item-list';
|
|
69
|
+
|
|
70
|
+
// Get check icon
|
|
71
|
+
const checkIconSvg = IconUtils.getIcon('check');
|
|
72
|
+
|
|
73
|
+
this.options.items.forEach(item => {
|
|
74
|
+
const itemButton = document.createElement('button');
|
|
75
|
+
itemButton.type = 'button';
|
|
76
|
+
itemButton.className = 'custom-select-item-button';
|
|
77
|
+
itemButton.dataset.value = this.getItemValue(item);
|
|
78
|
+
|
|
79
|
+
// Create item content with text and checkmark
|
|
80
|
+
const itemText = document.createElement('div');
|
|
81
|
+
itemText.className = 'item-text';
|
|
82
|
+
itemText.innerHTML = this.getItemDisplay(item);
|
|
83
|
+
|
|
84
|
+
const checkmark = document.createElement('span');
|
|
85
|
+
checkmark.className = 'item-checkmark';
|
|
86
|
+
checkmark.innerHTML = checkIconSvg || '';
|
|
87
|
+
|
|
88
|
+
itemButton.appendChild(itemText);
|
|
89
|
+
itemButton.appendChild(checkmark);
|
|
90
|
+
|
|
91
|
+
itemButton.addEventListener('click', (e) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
this.selectItem(item);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
list.appendChild(itemButton);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.popup.appendChild(list);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get display text for item
|
|
105
|
+
*/
|
|
106
|
+
getItemDisplay(item) {
|
|
107
|
+
return item[this.options.displayProperty] || item.toString();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get value for item
|
|
112
|
+
*/
|
|
113
|
+
getItemValue(item) {
|
|
114
|
+
return item[this.options.valueProperty] || item[this.options.displayProperty] || item;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update items in the select
|
|
119
|
+
*/
|
|
120
|
+
async updateItems(items) {
|
|
121
|
+
this.options.items = items;
|
|
122
|
+
|
|
123
|
+
// Remove existing list
|
|
124
|
+
const existingList = this.popup.querySelector('.item-list');
|
|
125
|
+
if (existingList) {
|
|
126
|
+
existingList.remove();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create new list
|
|
130
|
+
await this.createItemList();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Setup click outside handler
|
|
135
|
+
*/
|
|
136
|
+
setupClickOutside() {
|
|
137
|
+
if (this.clickOutsideHandler) {
|
|
138
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.clickOutsideHandler = (e) => {
|
|
142
|
+
// Don't hide if clicking on block toolbar or its buttons
|
|
143
|
+
if (e.target.closest('.block-toolbar')) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!this.popup.contains(e.target)) {
|
|
148
|
+
this.hide();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Add slight delay to avoid immediate close
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
155
|
+
}, 100);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove click outside handler
|
|
160
|
+
*/
|
|
161
|
+
removeClickOutside() {
|
|
162
|
+
if (this.clickOutsideHandler) {
|
|
163
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
164
|
+
this.clickOutsideHandler = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Setup scroll handler to update popup position
|
|
170
|
+
*/
|
|
171
|
+
setupScrollHandler() {
|
|
172
|
+
if (this.scrollHandler) {
|
|
173
|
+
window.removeEventListener('scroll', this.scrollHandler);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.scrollHandler = () => {
|
|
177
|
+
if (this.isVisible) {
|
|
178
|
+
this.updatePosition();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
window.addEventListener('scroll', this.scrollHandler);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove scroll handler
|
|
187
|
+
*/
|
|
188
|
+
removeScrollHandler() {
|
|
189
|
+
if (this.scrollHandler) {
|
|
190
|
+
window.removeEventListener('scroll', this.scrollHandler);
|
|
191
|
+
this.scrollHandler = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Show select popup
|
|
197
|
+
*/
|
|
198
|
+
async show(anchor) {
|
|
199
|
+
if (!anchor) return;
|
|
200
|
+
|
|
201
|
+
// Capture the editor selection NOW so the format applies to the right place
|
|
202
|
+
// even if a tap on the popup clears the live selection (mobile/touch).
|
|
203
|
+
// Prefer the LIVE selection when it's inside the editor — including a
|
|
204
|
+
// collapsed caret (needed for "apply then keep typing"). Only fall back to
|
|
205
|
+
// the last real range when the selection is genuinely gone/outside.
|
|
206
|
+
const sel = window.getSelection();
|
|
207
|
+
const ed = Editor.getCurrentInstance && Editor.getCurrentInstance();
|
|
208
|
+
const editorEl = ed && ed.editor;
|
|
209
|
+
if (sel && sel.rangeCount && editorEl && editorEl.contains(sel.anchorNode)) {
|
|
210
|
+
this._savedRange = sel.getRangeAt(0).cloneRange();
|
|
211
|
+
} else {
|
|
212
|
+
this._savedRange = ed && ed._lastRange ? ed._lastRange.cloneRange() : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Wait for initialization if not ready
|
|
216
|
+
if (!this.initialized) {
|
|
217
|
+
await new Promise(resolve => {
|
|
218
|
+
const checkInit = () => {
|
|
219
|
+
if (this.initialized) {
|
|
220
|
+
resolve();
|
|
221
|
+
} else {
|
|
222
|
+
setTimeout(checkInit, 10);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
checkInit();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Ensure popup is in DOM
|
|
230
|
+
if (!document.body.contains(this.popup)) {
|
|
231
|
+
appendPopup(this.popup);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Update current selection highlight
|
|
235
|
+
this.highlightCurrentItem(this.currentValue);
|
|
236
|
+
|
|
237
|
+
// Calculate and set popup position
|
|
238
|
+
const position = calculatePopupPosition(anchor, this.popup, {
|
|
239
|
+
offsetY: 5,
|
|
240
|
+
offsetX: 0
|
|
241
|
+
});
|
|
242
|
+
setPopupPosition(this.popup, position);
|
|
243
|
+
|
|
244
|
+
// Show popup by adding visible class
|
|
245
|
+
this.popup.classList.add('visible');
|
|
246
|
+
this.isVisible = true;
|
|
247
|
+
|
|
248
|
+
// Setup click outside handler
|
|
249
|
+
this.setupClickOutside();
|
|
250
|
+
|
|
251
|
+
// Setup scroll handler to update position
|
|
252
|
+
this.setupScrollHandler();
|
|
253
|
+
|
|
254
|
+
// Store reference to anchor for potential repositioning
|
|
255
|
+
this.currentAnchor = anchor;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Hide select popup
|
|
260
|
+
*/
|
|
261
|
+
hide() {
|
|
262
|
+
this.popup.classList.remove('visible');
|
|
263
|
+
this.isVisible = false;
|
|
264
|
+
this.removeClickOutside();
|
|
265
|
+
this.currentAnchor = null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Update popup position based on current anchor
|
|
270
|
+
*/
|
|
271
|
+
updatePosition() {
|
|
272
|
+
if (this.isVisible && this.currentAnchor) {
|
|
273
|
+
// Calculate and set popup position
|
|
274
|
+
const position = calculatePopupPosition(this.currentAnchor, this.popup, {
|
|
275
|
+
offsetY: 5,
|
|
276
|
+
offsetX: 0
|
|
277
|
+
});
|
|
278
|
+
setPopupPosition(this.popup, position);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Set current value
|
|
284
|
+
*/
|
|
285
|
+
setCurrentValue(value) {
|
|
286
|
+
this.currentValue = value;
|
|
287
|
+
this.highlightCurrentItem(value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Highlight current item in the list
|
|
292
|
+
*/
|
|
293
|
+
highlightCurrentItem(value) {
|
|
294
|
+
// Remove previous highlights
|
|
295
|
+
this.popup.querySelectorAll('.custom-select-item-button.current').forEach(btn => {
|
|
296
|
+
btn.classList.remove('current');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Highlight current item - find by comparing dataset.value directly
|
|
300
|
+
if (value != null) {
|
|
301
|
+
const buttons = this.popup.querySelectorAll('.custom-select-item-button');
|
|
302
|
+
for (const button of buttons) {
|
|
303
|
+
if (button.dataset.value === value.toString()) {
|
|
304
|
+
button.classList.add('current');
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Select item and trigger callback
|
|
313
|
+
*/
|
|
314
|
+
selectItem(item) {
|
|
315
|
+
const value = this.getItemValue(item);
|
|
316
|
+
this.currentValue = value;
|
|
317
|
+
|
|
318
|
+
// Restore the selection captured when the popup opened, so the format
|
|
319
|
+
// applies even if the tap cleared the live selection (mobile/touch).
|
|
320
|
+
if (this._savedRange) {
|
|
321
|
+
const s = window.getSelection();
|
|
322
|
+
s.removeAllRanges();
|
|
323
|
+
s.addRange(this._savedRange);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (this.options.onItemSelect) {
|
|
327
|
+
this.options.onItemSelect(value, item);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.hide();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get current selected value
|
|
335
|
+
*/
|
|
336
|
+
getCurrentValue() {
|
|
337
|
+
return this.currentValue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Destroy select component
|
|
342
|
+
*/
|
|
343
|
+
destroy() {
|
|
344
|
+
this.removeClickOutside();
|
|
345
|
+
if (this.popup && this.popup.parentNode) {
|
|
346
|
+
this.popup.parentNode.removeChild(this.popup);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default CustomSelect;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emoji Picker Component - Popup for selecting emojis
|
|
3
|
+
*/
|
|
4
|
+
import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
|
|
5
|
+
|
|
6
|
+
class EmojiPicker {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = {
|
|
9
|
+
emojis: [
|
|
10
|
+
// Smileys & People
|
|
11
|
+
'😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',
|
|
12
|
+
'😋', '😎', '😍', '🥰', '😘', '😗', '😙', '😚', '🙂', '🤗',
|
|
13
|
+
'😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱',
|
|
14
|
+
'🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻',
|
|
15
|
+
],
|
|
16
|
+
onEmojiSelect: null,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
this.popup = null;
|
|
21
|
+
this.isVisible = false;
|
|
22
|
+
this.clickOutsideHandler = null;
|
|
23
|
+
|
|
24
|
+
this.createEmojiPicker();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect operating system
|
|
29
|
+
* @returns {string} 'mac' or 'windows'
|
|
30
|
+
*/
|
|
31
|
+
detectOS() {
|
|
32
|
+
const platform = navigator.platform.toLowerCase();
|
|
33
|
+
if (platform.includes('mac')) {
|
|
34
|
+
return 'mac';
|
|
35
|
+
} else if (platform.includes('win')) {
|
|
36
|
+
return 'windows';
|
|
37
|
+
}
|
|
38
|
+
// Default to windows for other platforms
|
|
39
|
+
return 'windows';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get emoji shortcut message based on OS
|
|
44
|
+
* @returns {string} HTML string for the shortcut message
|
|
45
|
+
*/
|
|
46
|
+
getEmojiShortcutMessage() {
|
|
47
|
+
const os = this.detectOS();
|
|
48
|
+
|
|
49
|
+
if (os === 'mac') {
|
|
50
|
+
return `<div style="color: rgb(113, 120, 124); font-style: normal; font-weight: 400; line-height: normal; text-align: center;">Get more emojis with <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">⌘</span> <span style="color: #000;">+</span> <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">CTRL</span> <span style="color: #000;">+</span> <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">SPACE</span></div>`;
|
|
51
|
+
} else {
|
|
52
|
+
return `<div style="color: rgb(113, 120, 124); font-style: normal; font-weight: 400; line-height: normal; text-align: center;">Get more emojis with <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">WIN</span> <span style="color: #000;">+</span> <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">.</span></div>`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create emoji picker popup
|
|
58
|
+
*/
|
|
59
|
+
createEmojiPicker() {
|
|
60
|
+
// Create popup
|
|
61
|
+
this.popup = document.createElement('div');
|
|
62
|
+
this.popup.className = 'emoji-picker-popup';
|
|
63
|
+
|
|
64
|
+
// Create emoji grid
|
|
65
|
+
this.createEmojiGrid();
|
|
66
|
+
const emojiTextMessage = document.createElement('div');
|
|
67
|
+
emojiTextMessage.className = 'emoji-text-message';
|
|
68
|
+
|
|
69
|
+
emojiTextMessage.innerHTML = this.getEmojiShortcutMessage();
|
|
70
|
+
this.popup.appendChild(emojiTextMessage);
|
|
71
|
+
|
|
72
|
+
// Add popup to container
|
|
73
|
+
appendPopup(this.popup);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create emoji grid
|
|
78
|
+
*/
|
|
79
|
+
createEmojiGrid() {
|
|
80
|
+
const emojiGrid = document.createElement('div');
|
|
81
|
+
emojiGrid.className = 'emoji-grid';
|
|
82
|
+
|
|
83
|
+
// Create emoji buttons
|
|
84
|
+
this.options.emojis.forEach(emoji => {
|
|
85
|
+
const emojiButton = document.createElement('button');
|
|
86
|
+
emojiButton.type = 'button';
|
|
87
|
+
emojiButton.className = 'emoji-button';
|
|
88
|
+
emojiButton.textContent = emoji;
|
|
89
|
+
emojiButton.title = emoji;
|
|
90
|
+
|
|
91
|
+
emojiButton.addEventListener('click', (e) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
this.selectEmoji(emoji);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
emojiGrid.appendChild(emojiButton);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.popup.appendChild(emojiGrid);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Setup click outside handler
|
|
105
|
+
*/
|
|
106
|
+
setupClickOutside() {
|
|
107
|
+
if (this.clickOutsideHandler) {
|
|
108
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.clickOutsideHandler = (e) => {
|
|
112
|
+
if (!this.popup.contains(e.target)) {
|
|
113
|
+
this.hide();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Add slight delay to avoid immediate close
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
120
|
+
}, 100);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove click outside handler
|
|
125
|
+
*/
|
|
126
|
+
removeClickOutside() {
|
|
127
|
+
if (this.clickOutsideHandler) {
|
|
128
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
129
|
+
this.clickOutsideHandler = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Show emoji picker popup
|
|
135
|
+
* @param {HTMLElement} anchor - Element to position popup relative to
|
|
136
|
+
*/
|
|
137
|
+
show(anchor) {
|
|
138
|
+
if (!anchor) return;
|
|
139
|
+
|
|
140
|
+
// Ensure popup is in DOM
|
|
141
|
+
if (!document.body.contains(this.popup)) {
|
|
142
|
+
appendPopup(this.popup);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Calculate and set popup position
|
|
146
|
+
const position = calculatePopupPosition(anchor, this.popup, {
|
|
147
|
+
offsetY: 5,
|
|
148
|
+
offsetX: 0
|
|
149
|
+
});
|
|
150
|
+
setPopupPosition(this.popup, position);
|
|
151
|
+
|
|
152
|
+
// Show popup by adding visible class
|
|
153
|
+
this.popup.classList.add('visible');
|
|
154
|
+
this.isVisible = true;
|
|
155
|
+
|
|
156
|
+
// Setup click outside handler
|
|
157
|
+
this.setupClickOutside();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Hide emoji picker popup
|
|
162
|
+
*/
|
|
163
|
+
hide() {
|
|
164
|
+
this.popup.classList.remove('visible');
|
|
165
|
+
this.isVisible = false;
|
|
166
|
+
this.removeClickOutside();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Select emoji and trigger callback
|
|
171
|
+
* @param {string} emoji - Selected emoji
|
|
172
|
+
*/
|
|
173
|
+
selectEmoji(emoji) {
|
|
174
|
+
if (this.options.onEmojiSelect) {
|
|
175
|
+
this.options.onEmojiSelect(emoji);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.hide();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Destroy the emoji picker
|
|
183
|
+
*/
|
|
184
|
+
destroy() {
|
|
185
|
+
this.removeClickOutside();
|
|
186
|
+
|
|
187
|
+
if (this.popup && this.popup.parentNode) {
|
|
188
|
+
this.popup.parentNode.removeChild(this.popup);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.popup = null;
|
|
192
|
+
this.isVisible = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default EmojiPicker;
|