@sex-editor/emoji 0.0.1 → 0.0.3-dev.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 sexandviolence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @sex-editor/emoji
2
+
3
+ Hello World usage:
4
+
5
+ ```ts
6
+ import { createSexEditor } from "@sex-editor/core";
7
+ import { registerEmojiPlugin } from "@sex-editor/emoji";
8
+
9
+ const editor = createSexEditor("editor", {
10
+ language: "en",
11
+ });
12
+
13
+ registerEmojiPlugin(editor);
14
+ ```
15
+
16
+ Install:
17
+
18
+ ```bash
19
+ npm i @sex-editor/emoji
20
+ ```
21
+
22
+ ```bash
23
+ pnpm add @sex-editor/emoji
24
+ ```
25
+
26
+ License: MIT
package/dist/index.d.ts CHANGED
@@ -1,6 +1,56 @@
1
1
  import { LexicalEditor } from 'lexical';
2
2
 
3
- declare function registerEmojiPlugin(editor: LexicalEditor): () => void;
3
+ type EmojiCommandRange = {
4
+ startKey: string;
5
+ startOffset: number;
6
+ endKey: string;
7
+ endOffset: number;
8
+ };
9
+ type EmojiTriggerMatch = {
10
+ query: string;
11
+ range: EmojiCommandRange;
12
+ };
13
+ type EmojiMenuItem = {
14
+ unifiedID: string;
15
+ shortcode: string;
16
+ label: string;
17
+ keywords: string[];
18
+ };
19
+ type CustomEmojiRuleMap = Record<string, string>;
20
+ type CustomEmojiItem = Readonly<{
21
+ shortcode: string;
22
+ unifiedID?: string;
23
+ emoji?: string;
24
+ keywords?: string[];
25
+ label?: string;
26
+ }>;
27
+ type EmojiConfig = Readonly<{
28
+ customEmojis?: CustomEmojiItem[];
29
+ customRules?: CustomEmojiRuleMap;
30
+ }>;
31
+
32
+ declare function registerEmojiPlugin(editor: LexicalEditor, config?: EmojiConfig): () => void;
33
+
34
+ declare class EmojiMenuPlugin {
35
+ private editor;
36
+ private config;
37
+ private menuElement;
38
+ private selectedIndex;
39
+ private queryString;
40
+ private filteredItems;
41
+ private active;
42
+ private commandRange;
43
+ private disposers;
44
+ constructor(editor: LexicalEditor, config?: EmojiConfig);
45
+ register(): () => void;
46
+ private destroy;
47
+ private filterItems;
48
+ private showMenu;
49
+ private hideMenu;
50
+ private moveSelection;
51
+ private updateMenuUI;
52
+ private executeSelection;
53
+ }
4
54
 
5
55
  type EmojiMatch = Readonly<{
6
56
  position: number;
@@ -9,4 +59,4 @@ type EmojiMatch = Readonly<{
9
59
  }>;
10
60
  declare function findEmoji(text: string): EmojiMatch | null;
11
61
 
12
- export { type EmojiMatch, findEmoji, registerEmojiPlugin };
62
+ export { type CustomEmojiItem, type CustomEmojiRuleMap, type EmojiCommandRange, type EmojiConfig, type EmojiMatch, type EmojiMenuItem, EmojiMenuPlugin, type EmojiTriggerMatch, findEmoji, registerEmojiPlugin };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/plugin/registerEmojiPlugin.ts
2
- import { TextNode } from "lexical";
3
- import { $createEmojiNode } from "@sex-editor/core";
2
+ import { TextNode as TextNode2 } from "lexical";
3
+ import { $createEmojiNode as $createEmojiNode2 } from "@sex-editor/core";
4
4
 
5
5
  // src/utils/findEmoji.ts
6
6
  import emojis from "emoji-datasource-facebook/emoji.json";
@@ -18,50 +18,503 @@ var emojiReplacementMap = emojis.reduce((acc, row) => {
18
18
  return acc;
19
19
  }, /* @__PURE__ */ new Map());
20
20
  function findEmoji(text) {
21
- const skippedText = [];
22
- for (const word of text.split(" ")) {
23
- if (!emojiReplacementMap.has(word)) {
24
- skippedText.push(word);
21
+ return findEmojiWithRules(text);
22
+ }
23
+ function findEmojiWithRules(text, customRules) {
24
+ const customMap = customRules && typeof customRules === "object" ? new Map(Object.entries(customRules).filter(([k, v]) => typeof k === "string" && typeof v === "string")) : null;
25
+ const isUnifiedID2 = (value) => /^[0-9a-fA-F]+(?:-[0-9a-fA-F]+)*$/.test(value);
26
+ const emojiToUnifiedID2 = (emoji) => {
27
+ const cps = [];
28
+ for (const ch of Array.from(emoji)) cps.push(ch.codePointAt(0).toString(16));
29
+ return cps.join("-");
30
+ };
31
+ let index = 0;
32
+ const words = text.split(" ");
33
+ for (let i = 0; i < words.length; i++) {
34
+ const word = words[i];
35
+ const position = index;
36
+ const rawCustom = customMap?.get(word);
37
+ const resolvedCustom = rawCustom ? isUnifiedID2(rawCustom) ? rawCustom : emojiToUnifiedID2(rawCustom) : null;
38
+ const unifiedID = resolvedCustom ?? emojiReplacementMap.get(word);
39
+ if (!unifiedID) {
40
+ index = position + word.length + 1;
25
41
  continue;
26
42
  }
27
- if (skippedText.length > 0) {
28
- skippedText.push("");
43
+ const isShortcodeLike = word.length >= 3 && word.startsWith(":") && word.endsWith(":");
44
+ if (!isShortcodeLike) {
45
+ const end = position + word.length;
46
+ const hasTrailingWhitespace = end < text.length && /\s/.test(text[end]);
47
+ if (!hasTrailingWhitespace) {
48
+ index = position + word.length + 1;
49
+ continue;
50
+ }
29
51
  }
30
52
  return {
31
- position: skippedText.join(" ").length,
53
+ position,
32
54
  shortcode: word,
33
- unifiedID: emojiReplacementMap.get(word)
55
+ unifiedID
34
56
  };
35
57
  }
36
58
  return null;
37
59
  }
38
60
 
39
- // src/plugin/registerEmojiPlugin.ts
40
- function $textNodeTransform(node) {
41
- if (!node.isSimpleText() || node.hasFormat("code")) {
42
- return;
61
+ // src/plugin/EmojiMenuPlugin.ts
62
+ import {
63
+ $createRangeSelection,
64
+ $getNodeByKey,
65
+ $getSelection,
66
+ $insertNodes,
67
+ $isRangeSelection,
68
+ $isTextNode as $isTextNode2,
69
+ $setSelection,
70
+ COMMAND_PRIORITY_LOW,
71
+ KEY_ARROW_DOWN_COMMAND,
72
+ KEY_ARROW_UP_COMMAND,
73
+ KEY_ENTER_COMMAND,
74
+ KEY_ESCAPE_COMMAND
75
+ } from "lexical";
76
+ import { $createEmojiNode } from "@sex-editor/core";
77
+
78
+ // src/constants/emojiIndex.ts
79
+ import emojis2 from "emoji-datasource-facebook/emoji.json";
80
+ var unifiedToChar = (unifiedID) => {
81
+ const safe = typeof unifiedID === "string" && unifiedID.length ? unifiedID : "1f600";
82
+ return String.fromCodePoint(...safe.split("-").map((v) => parseInt(v, 16)));
83
+ };
84
+ var isUnifiedID = (value) => /^[0-9a-fA-F]+(?:-[0-9a-fA-F]+)*$/.test(value);
85
+ var emojiToUnifiedID = (emoji) => {
86
+ const cps = [];
87
+ for (const ch of Array.from(emoji)) {
88
+ cps.push(ch.codePointAt(0).toString(16));
89
+ }
90
+ return cps.join("-");
91
+ };
92
+ var normalize = (s) => s.toLowerCase();
93
+ var normalizeCustomEmojis = (custom) => {
94
+ if (!custom?.length) return [];
95
+ const out = [];
96
+ const seen = /* @__PURE__ */ new Set();
97
+ for (const item of custom) {
98
+ const rawUnified = typeof item?.unifiedID === "string" ? item.unifiedID : "";
99
+ const rawEmoji = typeof item?.emoji === "string" ? item.emoji : "";
100
+ const unifiedID = (rawUnified && isUnifiedID(rawUnified) ? rawUnified : rawEmoji ? emojiToUnifiedID(rawEmoji) : "").toLowerCase();
101
+ const shortcode = String(item?.shortcode ?? "");
102
+ if (!unifiedID || !shortcode) continue;
103
+ const label = typeof item.label === "string" && item.label.length ? item.label : rawEmoji ? rawEmoji : unifiedToChar(unifiedID);
104
+ const keywords = Array.isArray(item.keywords) ? item.keywords.filter((k) => typeof k === "string") : [];
105
+ const key = `${unifiedID}__${shortcode}`;
106
+ if (seen.has(key)) continue;
107
+ seen.add(key);
108
+ out.push({
109
+ unifiedID,
110
+ shortcode,
111
+ label,
112
+ keywords: [shortcode, ...keywords]
113
+ });
114
+ }
115
+ return out;
116
+ };
117
+ var buildEmojiIndex = () => {
118
+ const items = [];
119
+ for (const row of emojis2) {
120
+ if (!row?.has_img_facebook) continue;
121
+ const unifiedID = String(row.unified ?? "").toLowerCase();
122
+ const shortName = String(row.short_name ?? "");
123
+ if (!unifiedID || !shortName) continue;
124
+ const label = unifiedToChar(unifiedID);
125
+ const keywordsSet = /* @__PURE__ */ new Set();
126
+ keywordsSet.add(shortName);
127
+ if (Array.isArray(row.short_names)) row.short_names.forEach((s) => typeof s === "string" && keywordsSet.add(s));
128
+ if (typeof row.name === "string") keywordsSet.add(row.name);
129
+ if (typeof row.category === "string") keywordsSet.add(row.category);
130
+ items.push({
131
+ unifiedID,
132
+ shortcode: shortName,
133
+ label,
134
+ keywords: Array.from(keywordsSet)
135
+ });
136
+ }
137
+ return items;
138
+ };
139
+ var EMOJI_ITEMS = buildEmojiIndex();
140
+ var searchEmojis = (query, limit, customEmojis) => {
141
+ const q = normalize(query.trim());
142
+ const customItems = normalizeCustomEmojis(customEmojis);
143
+ if (!q) {
144
+ const head = customItems.slice(0, limit);
145
+ if (head.length >= limit) return head;
146
+ return head.concat(EMOJI_ITEMS.slice(0, limit - head.length));
147
+ }
148
+ const results = [];
149
+ for (const item of customItems) {
150
+ if (normalize(item.shortcode).includes(q) || item.keywords.some((k) => normalize(k).includes(q))) {
151
+ results.push(item);
152
+ if (results.length >= limit) return results;
153
+ }
43
154
  }
44
- const text = node.getTextContent();
45
- const emojiMatch = findEmoji(text);
46
- if (emojiMatch === null) {
155
+ for (const item of EMOJI_ITEMS) {
156
+ if (normalize(item.shortcode).includes(q)) {
157
+ results.push(item);
158
+ if (results.length >= limit) break;
159
+ continue;
160
+ }
161
+ const hit = item.keywords.some((k) => normalize(k).includes(q));
162
+ if (hit) {
163
+ results.push(item);
164
+ if (results.length >= limit) break;
165
+ }
166
+ }
167
+ return results;
168
+ };
169
+
170
+ // src/utils/getEmojiMatch.ts
171
+ import { $isElementNode, $isTextNode } from "lexical";
172
+ var collectTextNodes = (node, out) => {
173
+ if ($isTextNode(node)) {
174
+ out.push(node);
47
175
  return;
48
176
  }
49
- let targetNode;
50
- if (emojiMatch.position === 0) {
51
- [targetNode] = node.splitText(emojiMatch.position + emojiMatch.shortcode.length);
52
- } else {
53
- [, targetNode] = node.splitText(
54
- emojiMatch.position,
55
- emojiMatch.position + emojiMatch.shortcode.length
177
+ if ($isElementNode(node)) {
178
+ const children = node.getChildren();
179
+ for (const child of children) collectTextNodes(child, out);
180
+ }
181
+ };
182
+ var isValidQuery = (query) => {
183
+ if (query.length === 0) return true;
184
+ return /^[a-zA-Z0-9_+\-]*$/.test(query);
185
+ };
186
+ var getEmojiMatch = (selection) => {
187
+ const anchorNode = selection.anchor.getNode();
188
+ if (!$isTextNode(anchorNode)) return null;
189
+ if (!anchorNode.isSimpleText() || anchorNode.hasFormat("code")) return null;
190
+ const block = anchorNode.getTopLevelElement();
191
+ if (!block) return null;
192
+ const textNodes = [];
193
+ collectTextNodes(block, textNodes);
194
+ if (textNodes.length === 0) return null;
195
+ let cursorIndex = null;
196
+ let running = 0;
197
+ for (const n of textNodes) {
198
+ if (n.getKey() === anchorNode.getKey()) {
199
+ cursorIndex = running + selection.anchor.offset;
200
+ break;
201
+ }
202
+ running += n.getTextContentSize();
203
+ }
204
+ if (cursorIndex === null) return null;
205
+ const blockText = block.getTextContent();
206
+ const beforeCursor = blockText.slice(0, cursorIndex);
207
+ const colonIndex = beforeCursor.lastIndexOf(":");
208
+ if (colonIndex === -1) return null;
209
+ const prevChar = colonIndex > 0 ? beforeCursor[colonIndex - 1] : "";
210
+ if (prevChar && /[a-zA-Z0-9]/.test(prevChar)) return null;
211
+ const query = beforeCursor.slice(colonIndex + 1);
212
+ if (/\s/.test(query)) return null;
213
+ if (!isValidQuery(query)) return null;
214
+ const indexToPoint = (index) => {
215
+ let remaining = index;
216
+ for (const n of textNodes) {
217
+ const size = n.getTextContentSize();
218
+ if (remaining <= size) return { key: n.getKey(), offset: remaining };
219
+ remaining -= size;
220
+ }
221
+ const last = textNodes[textNodes.length - 1];
222
+ return { key: last.getKey(), offset: last.getTextContentSize() };
223
+ };
224
+ const start = indexToPoint(colonIndex);
225
+ const end = indexToPoint(cursorIndex);
226
+ if (!start || !end) return null;
227
+ return {
228
+ query,
229
+ range: {
230
+ startKey: start.key,
231
+ startOffset: start.offset,
232
+ endKey: end.key,
233
+ endOffset: end.offset
234
+ }
235
+ };
236
+ };
237
+
238
+ // src/plugin/EmojiMenuPlugin.ts
239
+ var EmojiMenuPlugin = class {
240
+ constructor(editor, config) {
241
+ this.menuElement = null;
242
+ this.selectedIndex = 0;
243
+ this.queryString = null;
244
+ this.filteredItems = [];
245
+ this.active = false;
246
+ this.commandRange = null;
247
+ this.disposers = [];
248
+ this.editor = editor;
249
+ this.config = config;
250
+ }
251
+ register() {
252
+ const unregUpdate = this.editor.registerUpdateListener(({ editorState }) => {
253
+ editorState.read(() => {
254
+ const selection = $getSelection();
255
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
256
+ this.hideMenu();
257
+ return;
258
+ }
259
+ const match = getEmojiMatch(selection);
260
+ if (!match) {
261
+ this.hideMenu();
262
+ return;
263
+ }
264
+ this.queryString = match.query;
265
+ this.commandRange = match.range;
266
+ this.showMenu(selection);
267
+ });
268
+ });
269
+ this.disposers.push(unregUpdate);
270
+ this.disposers.push(
271
+ this.editor.registerCommand(
272
+ KEY_ARROW_UP_COMMAND,
273
+ (event) => {
274
+ if (!this.active) return false;
275
+ event.preventDefault();
276
+ this.moveSelection(-1);
277
+ return true;
278
+ },
279
+ COMMAND_PRIORITY_LOW
280
+ )
56
281
  );
282
+ this.disposers.push(
283
+ this.editor.registerCommand(
284
+ KEY_ARROW_DOWN_COMMAND,
285
+ (event) => {
286
+ if (!this.active) return false;
287
+ event.preventDefault();
288
+ this.moveSelection(1);
289
+ return true;
290
+ },
291
+ COMMAND_PRIORITY_LOW
292
+ )
293
+ );
294
+ this.disposers.push(
295
+ this.editor.registerCommand(
296
+ KEY_ENTER_COMMAND,
297
+ (event) => {
298
+ if (!this.active) return false;
299
+ event?.preventDefault();
300
+ this.executeSelection();
301
+ return true;
302
+ },
303
+ COMMAND_PRIORITY_LOW
304
+ )
305
+ );
306
+ this.disposers.push(
307
+ this.editor.registerCommand(
308
+ KEY_ESCAPE_COMMAND,
309
+ (event) => {
310
+ if (!this.active) return false;
311
+ event.preventDefault();
312
+ this.hideMenu();
313
+ return true;
314
+ },
315
+ COMMAND_PRIORITY_LOW
316
+ )
317
+ );
318
+ return () => this.destroy();
319
+ }
320
+ destroy() {
321
+ this.hideMenu(true);
322
+ for (const dispose of this.disposers) dispose();
323
+ this.disposers = [];
324
+ }
325
+ filterItems() {
326
+ const q = this.queryString ?? "";
327
+ this.filteredItems = searchEmojis(q, 60, this.config?.customEmojis);
328
+ }
329
+ showMenu(selection) {
330
+ if (typeof document === "undefined") return;
331
+ this.active = true;
332
+ this.filterItems();
333
+ if (this.filteredItems.length === 0) {
334
+ this.hideMenu();
335
+ return;
336
+ }
337
+ if (this.selectedIndex >= this.filteredItems.length) this.selectedIndex = 0;
338
+ if (!this.menuElement) {
339
+ this.menuElement = document.createElement("div");
340
+ this.menuElement.className = "sex-emoji-menu";
341
+ document.body.appendChild(this.menuElement);
342
+ }
343
+ this.menuElement.innerHTML = "";
344
+ this.menuElement.style.display = "block";
345
+ this.filteredItems.forEach((item, index) => {
346
+ const el = document.createElement("div");
347
+ el.className = `sex-emoji-menu-item ${index === this.selectedIndex ? "selected" : ""}`;
348
+ el.innerHTML = `
349
+ <span class="sex-emoji-menu-icon">${item.label}</span>
350
+ <span class="sex-emoji-menu-label">:${item.shortcode}:</span>
351
+ `;
352
+ el.onclick = () => {
353
+ this.selectedIndex = index;
354
+ this.executeSelection();
355
+ };
356
+ this.menuElement?.appendChild(el);
357
+ });
358
+ const domSelection = window.getSelection();
359
+ if (domSelection && domSelection.rangeCount > 0) {
360
+ const range = domSelection.getRangeAt(0);
361
+ const rect = range.getBoundingClientRect();
362
+ if (this.menuElement) {
363
+ this.menuElement.style.top = `${rect.bottom + window.scrollY + 6}px`;
364
+ this.menuElement.style.left = `${rect.left + window.scrollX}px`;
365
+ }
366
+ }
367
+ }
368
+ hideMenu(remove = false) {
369
+ this.active = false;
370
+ this.queryString = null;
371
+ this.selectedIndex = 0;
372
+ this.commandRange = null;
373
+ if (!this.menuElement) return;
374
+ if (remove) {
375
+ this.menuElement.remove();
376
+ this.menuElement = null;
377
+ return;
378
+ }
379
+ this.menuElement.style.display = "none";
380
+ }
381
+ moveSelection(delta) {
382
+ this.selectedIndex += delta;
383
+ if (this.selectedIndex < 0) this.selectedIndex = this.filteredItems.length - 1;
384
+ else if (this.selectedIndex >= this.filteredItems.length) this.selectedIndex = 0;
385
+ this.updateMenuUI();
57
386
  }
58
- const emojiNode = $createEmojiNode(emojiMatch.unifiedID);
59
- targetNode.replace(emojiNode);
387
+ updateMenuUI() {
388
+ if (!this.menuElement) return;
389
+ const items = this.menuElement.querySelectorAll(".sex-emoji-menu-item");
390
+ items.forEach((item, index) => {
391
+ if (index === this.selectedIndex) {
392
+ item.classList.add("selected");
393
+ item.scrollIntoView({ block: "nearest" });
394
+ } else {
395
+ item.classList.remove("selected");
396
+ }
397
+ });
398
+ }
399
+ executeSelection() {
400
+ const item = this.filteredItems[this.selectedIndex];
401
+ if (!item) return;
402
+ const commandRange = this.commandRange;
403
+ this.hideMenu();
404
+ this.editor.update(
405
+ () => {
406
+ if (!commandRange) return;
407
+ const startNode = $getNodeByKey(commandRange.startKey);
408
+ const endNode = $getNodeByKey(commandRange.endKey);
409
+ if (!startNode || !endNode) return;
410
+ if (!$isTextNode2(startNode) || !$isTextNode2(endNode)) return;
411
+ const rangeSelection = $createRangeSelection();
412
+ rangeSelection.setTextNodeRange(
413
+ startNode,
414
+ commandRange.startOffset,
415
+ endNode,
416
+ commandRange.endOffset
417
+ );
418
+ $setSelection(rangeSelection);
419
+ rangeSelection.removeText();
420
+ const emojiNode = $createEmojiNode(item.unifiedID);
421
+ $insertNodes([emojiNode]);
422
+ },
423
+ { discrete: true }
424
+ );
425
+ }
426
+ };
427
+
428
+ // src/plugin/registerEmojiPlugin.ts
429
+ function registerEmojiPlugin(editor, config = {}) {
430
+ const menuPlugin = new EmojiMenuPlugin(editor, config);
431
+ const disposeMenu = menuPlugin.register();
432
+ const disposeTransform = editor.registerNodeTransform(TextNode2, (node) => {
433
+ if (!node.isSimpleText() || node.hasFormat("code")) return;
434
+ const text = node.getTextContent();
435
+ const emojiMatch = findEmojiWithRules(text, config.customRules);
436
+ if (emojiMatch === null) return;
437
+ let targetNode;
438
+ if (emojiMatch.position === 0) {
439
+ [targetNode] = node.splitText(emojiMatch.position + emojiMatch.shortcode.length);
440
+ } else {
441
+ [, targetNode] = node.splitText(emojiMatch.position, emojiMatch.position + emojiMatch.shortcode.length);
442
+ }
443
+ const emojiNode = $createEmojiNode2(emojiMatch.unifiedID);
444
+ targetNode.replace(emojiNode);
445
+ });
446
+ return () => {
447
+ disposeMenu();
448
+ disposeTransform();
449
+ };
450
+ }
451
+
452
+ // src/index.ts
453
+ var ensureStyle = (id, cssText) => {
454
+ if (typeof document === "undefined") return;
455
+ if (document.getElementById(id)) return;
456
+ const style = document.createElement("style");
457
+ style.id = id;
458
+ style.textContent = cssText;
459
+ (document.head ?? document.documentElement).appendChild(style);
460
+ };
461
+ ensureStyle(
462
+ "sex-emoji-styles",
463
+ `.sex-emoji-menu {
464
+ position: absolute;
465
+ z-index: 100;
466
+ background: white;
467
+ border: 1px solid #e5e7eb;
468
+ border-radius: 8px;
469
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
470
+ width: 320px;
471
+ max-height: 320px;
472
+ overflow-y: auto;
473
+ padding: 4px;
474
+ }
475
+
476
+ .sex-emoji-menu-item {
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 10px;
480
+ padding: 8px 12px;
481
+ cursor: pointer;
482
+ border-radius: 4px;
483
+ font-size: 14px;
484
+ color: #374151;
485
+ transition: background-color 0.2s;
60
486
  }
61
- function registerEmojiPlugin(editor) {
62
- return editor.registerNodeTransform(TextNode, $textNodeTransform);
487
+
488
+ .sex-emoji-menu-item:hover,
489
+ .sex-emoji-menu-item.selected {
490
+ background-color: #f3f4f6;
491
+ }
492
+
493
+ .sex-emoji-menu-icon {
494
+ width: 22px;
495
+ height: 22px;
496
+ display: flex;
497
+ align-items: center;
498
+ justify-content: center;
499
+ font-size: 18px;
500
+ line-height: 1;
501
+ user-select: none;
502
+ }
503
+
504
+ .sex-emoji-menu-label {
505
+ flex: 1;
506
+ white-space: nowrap;
507
+ overflow: hidden;
508
+ text-overflow: ellipsis;
509
+ }
510
+
511
+ .sex-emoji-menu-hidden {
512
+ display: none;
63
513
  }
514
+ `
515
+ );
64
516
  export {
517
+ EmojiMenuPlugin,
65
518
  findEmoji,
66
519
  registerEmojiPlugin
67
520
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sex-editor/emoji",
3
- "version": "0.0.1",
3
+ "version": "0.0.3-dev.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "devDependencies": {
16
16
  "tsup": "8.3.5",
17
- "@sex-editor/core": "0.0.2"
17
+ "@sex-editor/core": "0.0.3-dev.0"
18
18
  },
19
19
  "dependencies": {
20
20
  "emoji-datasource-facebook": "^16.0.0"
@@ -29,7 +29,7 @@
29
29
  "sideEffects": false,
30
30
  "keywords": [],
31
31
  "author": "",
32
- "license": "ISC",
32
+ "license": "MIT",
33
33
  "scripts": {
34
34
  "dev": "tsup --watch",
35
35
  "build": "tsup"