@editorjs-mentions/plugin 0.1.4

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/dist/index.mjs ADDED
@@ -0,0 +1,927 @@
1
+ // src/dropdown.ts
2
+ var MentionsDropdown = class {
3
+ constructor(options) {
4
+ this.items = [];
5
+ this.activeIndex = 0;
6
+ this.onSelect = options.onSelect;
7
+ this.renderItem = options.renderItem;
8
+ this.className = options.className;
9
+ this.root = document.createElement("div");
10
+ this.root.className = this.className || "editorjs-mentions-dropdown";
11
+ this.root.style.display = "none";
12
+ document.body.appendChild(this.root);
13
+ }
14
+ show(position, items) {
15
+ this.items = items;
16
+ this.activeIndex = 0;
17
+ this.root.innerHTML = "";
18
+ items.forEach((item, index) => {
19
+ const row = document.createElement("div");
20
+ row.className = "editorjs-mentions-item";
21
+ row.dataset.active = index === this.activeIndex ? "true" : "false";
22
+ row.addEventListener("mousedown", (event) => {
23
+ event.preventDefault();
24
+ this.onSelect(item);
25
+ });
26
+ if (this.renderItem) {
27
+ row.innerHTML = this.renderItem(item);
28
+ } else {
29
+ const avatar = document.createElement("img");
30
+ avatar.className = "editorjs-mentions-item-avatar";
31
+ avatar.alt = item.displayName;
32
+ avatar.src = item.image || `data:image/svg+xml,${encodeURIComponent(
33
+ `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><rect width="28" height="28" fill="#D5DEF0"/><text x="50%" y="55%" text-anchor="middle" fill="#334155" font-size="12" font-family="sans-serif">${item.displayName.slice(0, 1).toUpperCase()}</text></svg>`
34
+ )}`;
35
+ const main = document.createElement("div");
36
+ main.className = "editorjs-mentions-item-main";
37
+ const name = document.createElement("div");
38
+ name.className = "editorjs-mentions-item-name";
39
+ name.textContent = item.displayName;
40
+ main.appendChild(name);
41
+ if (item.description) {
42
+ const description = document.createElement("div");
43
+ description.className = "editorjs-mentions-item-description";
44
+ description.textContent = item.description;
45
+ main.appendChild(description);
46
+ }
47
+ row.appendChild(avatar);
48
+ row.appendChild(main);
49
+ }
50
+ this.root.appendChild(row);
51
+ });
52
+ this.root.style.left = `${Math.max(8, position.left)}px`;
53
+ this.root.style.top = `${Math.max(8, position.top)}px`;
54
+ this.root.style.display = items.length > 0 ? "block" : "none";
55
+ }
56
+ hide() {
57
+ this.root.style.display = "none";
58
+ this.items = [];
59
+ this.activeIndex = 0;
60
+ }
61
+ isVisible() {
62
+ return this.root.style.display !== "none";
63
+ }
64
+ hasItems() {
65
+ return this.items.length > 0;
66
+ }
67
+ moveUp() {
68
+ if (!this.items.length) {
69
+ return;
70
+ }
71
+ this.activeIndex = (this.activeIndex - 1 + this.items.length) % this.items.length;
72
+ this.syncActiveRow();
73
+ }
74
+ moveDown() {
75
+ if (!this.items.length) {
76
+ return;
77
+ }
78
+ this.activeIndex = (this.activeIndex + 1) % this.items.length;
79
+ this.syncActiveRow();
80
+ }
81
+ chooseActive() {
82
+ if (!this.items.length) {
83
+ return;
84
+ }
85
+ this.onSelect(this.items[this.activeIndex]);
86
+ }
87
+ destroy() {
88
+ this.root.remove();
89
+ }
90
+ syncActiveRow() {
91
+ Array.from(this.root.children).forEach((child, index) => {
92
+ const row = child;
93
+ row.dataset.active = index === this.activeIndex ? "true" : "false";
94
+ if (index === this.activeIndex) {
95
+ row.scrollIntoView({ block: "nearest" });
96
+ }
97
+ });
98
+ }
99
+ };
100
+
101
+ // src/providers.ts
102
+ function normalizeProvider(provider) {
103
+ if (typeof provider === "function") {
104
+ return provider;
105
+ }
106
+ return provider.search.bind(provider);
107
+ }
108
+ function createRestMentionProvider(options) {
109
+ const queryParam = options.queryParam ?? "query";
110
+ const triggerParam = options.triggerParam ?? "trigger";
111
+ const limitParam = options.limitParam ?? "limit";
112
+ return async (query) => {
113
+ const url = new URL(options.endpoint, window.location.origin);
114
+ url.searchParams.set(queryParam, query.query);
115
+ url.searchParams.set(triggerParam, query.trigger);
116
+ url.searchParams.set(limitParam, String(query.limit));
117
+ const response = await fetch(url.toString(), options.fetchInit);
118
+ if (!response.ok) {
119
+ throw new Error(`Mention provider request failed: ${response.status}`);
120
+ }
121
+ const json = await response.json();
122
+ if (options.mapResponse) {
123
+ return options.mapResponse(json);
124
+ }
125
+ if (typeof json !== "object" || json === null || !("items" in json)) {
126
+ return [];
127
+ }
128
+ const maybeItems = json.items;
129
+ return Array.isArray(maybeItems) ? maybeItems : [];
130
+ };
131
+ }
132
+
133
+ // src/styles.ts
134
+ var MENTIONS_STYLE_ID = "editorjs-mentions-style";
135
+ var CSS = `
136
+ a.editorjs-mention,
137
+ a.editorjs-mention:visited,
138
+ a.editorjs-mention:hover,
139
+ a.editorjs-mention:active,
140
+ .editorjs-mention {
141
+ background: #e9f2ff;
142
+ color: #0b4fb3;
143
+ border-radius: 4px;
144
+ padding: 0 4px;
145
+ white-space: nowrap;
146
+ text-decoration: none !important;
147
+ cursor: pointer;
148
+ }
149
+
150
+ .editorjs-mentions-dropdown {
151
+ position: fixed;
152
+ min-width: 260px;
153
+ max-width: 360px;
154
+ max-height: 280px;
155
+ overflow: auto;
156
+ z-index: 9999;
157
+ background: #fff;
158
+ border: 1px solid #d7dde5;
159
+ border-radius: 8px;
160
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
161
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
162
+ }
163
+
164
+ .editorjs-mentions-item {
165
+ display: grid;
166
+ grid-template-columns: 28px 1fr;
167
+ gap: 10px;
168
+ align-items: center;
169
+ padding: 8px 10px;
170
+ cursor: pointer;
171
+ }
172
+
173
+ .editorjs-mentions-item:hover,
174
+ .editorjs-mentions-item[data-active="true"] {
175
+ background: #f4f8ff;
176
+ }
177
+
178
+ .editorjs-mentions-item-avatar {
179
+ width: 28px;
180
+ height: 28px;
181
+ border-radius: 50%;
182
+ object-fit: cover;
183
+ background: #d5def0;
184
+ }
185
+
186
+ .editorjs-mentions-item-main {
187
+ min-width: 0;
188
+ }
189
+
190
+ .editorjs-mentions-item-name {
191
+ font-size: 14px;
192
+ color: #1f2937;
193
+ line-height: 1.2;
194
+ }
195
+
196
+ .editorjs-mentions-item-description {
197
+ font-size: 12px;
198
+ color: #6b7280;
199
+ line-height: 1.2;
200
+ white-space: nowrap;
201
+ overflow: hidden;
202
+ text-overflow: ellipsis;
203
+ }
204
+
205
+ .editorjs-mention-tooltip {
206
+ position: fixed;
207
+ z-index: 10000;
208
+ min-width: 220px;
209
+ max-width: 320px;
210
+ background: #fff;
211
+ border: 1px solid #d7dde5;
212
+ border-radius: 10px;
213
+ box-shadow: 0 14px 30px rgba(0, 0, 0, 0.16);
214
+ }
215
+
216
+ .editorjs-mention-tooltip-inner {
217
+ display: grid;
218
+ grid-template-columns: 40px 1fr;
219
+ gap: 10px;
220
+ padding: 10px;
221
+ align-items: start;
222
+ }
223
+
224
+ .editorjs-mention-tooltip-image,
225
+ .editorjs-mention-tooltip-placeholder {
226
+ width: 40px;
227
+ height: 40px;
228
+ border-radius: 50%;
229
+ }
230
+
231
+ .editorjs-mention-tooltip-placeholder {
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ background: #d5def0;
236
+ color: #334155;
237
+ font-weight: 600;
238
+ }
239
+
240
+ .editorjs-mention-tooltip-name {
241
+ font-size: 14px;
242
+ line-height: 1.2;
243
+ color: #1f2937;
244
+ }
245
+
246
+ .editorjs-mention-tooltip-description {
247
+ margin-top: 2px;
248
+ font-size: 12px;
249
+ color: #6b7280;
250
+ }
251
+
252
+ .editorjs-mention-tooltip-link {
253
+ margin-top: 7px;
254
+ display: inline-block;
255
+ font-size: 12px;
256
+ color: #0b4fb3;
257
+ }
258
+ `;
259
+ function ensureMentionsStyleInjected() {
260
+ if (typeof document === "undefined") {
261
+ return;
262
+ }
263
+ if (document.getElementById(MENTIONS_STYLE_ID)) {
264
+ return;
265
+ }
266
+ const style = document.createElement("style");
267
+ style.id = MENTIONS_STYLE_ID;
268
+ style.textContent = CSS;
269
+ document.head.appendChild(style);
270
+ }
271
+
272
+ // src/editorjs-mentions.ts
273
+ var EditorJSMentions = class {
274
+ constructor(config) {
275
+ this.requestSerial = 0;
276
+ this.activeContext = null;
277
+ this.destroyed = false;
278
+ this.onClick = (event) => {
279
+ const target = event.target instanceof HTMLElement ? event.target : null;
280
+ const mentionNode = target?.closest("a.editorjs-mention");
281
+ if (mentionNode) {
282
+ event.preventDefault();
283
+ const item = this.readMentionFromElement(mentionNode);
284
+ if (item) {
285
+ this.showTooltip(mentionNode, item);
286
+ }
287
+ return;
288
+ }
289
+ this.hideTooltip();
290
+ if (!this.dropdown.isVisible()) {
291
+ return;
292
+ }
293
+ this.evaluateAndFetch();
294
+ };
295
+ this.onDocumentMouseDown = (event) => {
296
+ if (!this.tooltipRoot || this.tooltipRoot.style.display === "none") {
297
+ return;
298
+ }
299
+ const target = event.target;
300
+ if (!target) {
301
+ return;
302
+ }
303
+ const clickedMention = target instanceof HTMLElement ? target.closest("a.editorjs-mention") : null;
304
+ if (clickedMention || this.tooltipRoot.contains(target)) {
305
+ return;
306
+ }
307
+ this.hideTooltip();
308
+ };
309
+ this.onInput = () => {
310
+ this.evaluateAndFetch();
311
+ };
312
+ this.onCopy = (event) => {
313
+ this.handleCopyOrCut(event, false);
314
+ };
315
+ this.onCut = (event) => {
316
+ this.handleCopyOrCut(event, true);
317
+ };
318
+ this.onPaste = (event) => {
319
+ const data = event.clipboardData;
320
+ if (!data) {
321
+ return;
322
+ }
323
+ const customHtml = data.getData("application/x-editorjs-mentions");
324
+ const plainHtml = data.getData("text/html");
325
+ const candidateHtml = customHtml || plainHtml;
326
+ if (!candidateHtml || !candidateHtml.includes("editorjs-mention")) {
327
+ return;
328
+ }
329
+ const normalized = normalizeMentionAnchorsHtml(candidateHtml);
330
+ if (!normalized) {
331
+ return;
332
+ }
333
+ const selection = window.getSelection();
334
+ if (!selection || !selection.rangeCount) {
335
+ return;
336
+ }
337
+ event.preventDefault();
338
+ event.stopPropagation();
339
+ event.stopImmediatePropagation();
340
+ const range = selection.getRangeAt(0);
341
+ range.deleteContents();
342
+ const fragment = range.createContextualFragment(normalized);
343
+ const pastedMentions = Array.from(fragment.querySelectorAll("a.editorjs-mention"));
344
+ const last = fragment.lastChild;
345
+ range.insertNode(fragment);
346
+ for (const mention of pastedMentions) {
347
+ const item = this.readMentionFromElement(mention);
348
+ if (!item) {
349
+ continue;
350
+ }
351
+ const trigger = mention.dataset.mentionTrigger || "@";
352
+ this.applyMentionRendering(mention, item, trigger, "paste");
353
+ }
354
+ if (last) {
355
+ const caret = document.createRange();
356
+ caret.setStartAfter(last);
357
+ caret.collapse(true);
358
+ selection.removeAllRanges();
359
+ selection.addRange(caret);
360
+ }
361
+ };
362
+ this.onKeyDown = (event) => {
363
+ if (!this.dropdown.isVisible()) {
364
+ return;
365
+ }
366
+ if (event.key === "ArrowDown") {
367
+ event.preventDefault();
368
+ event.stopPropagation();
369
+ event.stopImmediatePropagation();
370
+ this.dropdown.moveDown();
371
+ return;
372
+ }
373
+ if (event.key === "ArrowUp") {
374
+ event.preventDefault();
375
+ event.stopPropagation();
376
+ event.stopImmediatePropagation();
377
+ this.dropdown.moveUp();
378
+ return;
379
+ }
380
+ if (event.key === "Enter" || event.key === "Tab") {
381
+ event.preventDefault();
382
+ event.stopPropagation();
383
+ event.stopImmediatePropagation();
384
+ this.dropdown.chooseActive();
385
+ return;
386
+ }
387
+ if (event.key === "Escape") {
388
+ event.stopPropagation();
389
+ event.stopImmediatePropagation();
390
+ this.dropdown.hide();
391
+ this.activeContext = null;
392
+ }
393
+ };
394
+ this.holder = typeof config.holder === "string" ? document.getElementById(config.holder) ?? (() => {
395
+ throw new Error(`Cannot find holder element by id: ${config.holder}`);
396
+ })() : config.holder;
397
+ this.config = {
398
+ triggerSymbols: config.triggerSymbols ?? ["@"],
399
+ maxResults: config.maxResults ?? 8,
400
+ minChars: config.minChars ?? 0,
401
+ debounceMs: config.debounceMs ?? 160,
402
+ onSelect: config.onSelect,
403
+ renderItem: config.renderItem,
404
+ className: config.className,
405
+ renderMention: config.renderMention,
406
+ mentionRenderContext: config.mentionRenderContext
407
+ };
408
+ this.provider = normalizeProvider(config.provider);
409
+ ensureMentionsStyleInjected();
410
+ this.dropdown = new MentionsDropdown({
411
+ className: this.config.className,
412
+ renderItem: this.config.renderItem,
413
+ onSelect: (item) => this.selectMention(item)
414
+ });
415
+ this.tooltipRoot = document.createElement("div");
416
+ this.tooltipRoot.className = "editorjs-mention-tooltip";
417
+ this.tooltipRoot.style.display = "none";
418
+ document.body.appendChild(this.tooltipRoot);
419
+ this.bind();
420
+ this.refreshMentionRendering();
421
+ }
422
+ setMentionRenderContext(context) {
423
+ this.config.mentionRenderContext = context;
424
+ this.refreshMentionRendering();
425
+ }
426
+ refreshMentionRendering() {
427
+ const mentions = Array.from(this.holder.querySelectorAll("a.editorjs-mention"));
428
+ for (const mention of mentions) {
429
+ const anchor = mention;
430
+ const item = this.readMentionFromElement(anchor);
431
+ if (!item) {
432
+ continue;
433
+ }
434
+ const trigger = anchor.dataset.mentionTrigger || (anchor.textContent?.startsWith("@") ? "@" : "@");
435
+ this.applyMentionRendering(anchor, item, trigger, "refresh");
436
+ }
437
+ }
438
+ destroy() {
439
+ if (this.destroyed) {
440
+ return;
441
+ }
442
+ this.destroyed = true;
443
+ this.holder.removeEventListener("input", this.onInput, true);
444
+ this.holder.removeEventListener("keydown", this.onKeyDown, true);
445
+ this.holder.removeEventListener("click", this.onClick, true);
446
+ this.holder.removeEventListener("copy", this.onCopy, true);
447
+ this.holder.removeEventListener("cut", this.onCut, true);
448
+ this.holder.removeEventListener("paste", this.onPaste, true);
449
+ document.removeEventListener("mousedown", this.onDocumentMouseDown, true);
450
+ if (this.debounceTimer) {
451
+ clearTimeout(this.debounceTimer);
452
+ }
453
+ this.dropdown.destroy();
454
+ this.tooltipRoot.remove();
455
+ }
456
+ bind() {
457
+ this.holder.addEventListener("input", this.onInput, true);
458
+ this.holder.addEventListener("keydown", this.onKeyDown, true);
459
+ this.holder.addEventListener("click", this.onClick, true);
460
+ this.holder.addEventListener("copy", this.onCopy, true);
461
+ this.holder.addEventListener("cut", this.onCut, true);
462
+ this.holder.addEventListener("paste", this.onPaste, true);
463
+ document.addEventListener("mousedown", this.onDocumentMouseDown, true);
464
+ }
465
+ handleCopyOrCut(event, isCut) {
466
+ const selection = window.getSelection();
467
+ const data = event.clipboardData;
468
+ if (!selection || !selection.rangeCount || !data || selection.isCollapsed) {
469
+ return;
470
+ }
471
+ const range = selection.getRangeAt(0);
472
+ const root = range.commonAncestorContainer;
473
+ if (!this.holder.contains(root.nodeType === Node.ELEMENT_NODE ? root : root.parentNode)) {
474
+ return;
475
+ }
476
+ const fragment = range.cloneContents();
477
+ const wrapper = document.createElement("div");
478
+ wrapper.appendChild(fragment);
479
+ const html = wrapper.innerHTML;
480
+ if (!html.includes("editorjs-mention")) {
481
+ return;
482
+ }
483
+ const normalized = normalizeMentionAnchorsHtml(html);
484
+ const plain = selection.toString().replace(/\u00A0/g, " ");
485
+ data.setData("text/plain", plain);
486
+ data.setData("text/html", normalized);
487
+ data.setData("application/x-editorjs-mentions", normalized);
488
+ event.preventDefault();
489
+ event.stopPropagation();
490
+ event.stopImmediatePropagation();
491
+ if (isCut) {
492
+ range.deleteContents();
493
+ selection.removeAllRanges();
494
+ const caret = document.createRange();
495
+ caret.setStart(range.startContainer, range.startOffset);
496
+ caret.collapse(true);
497
+ selection.addRange(caret);
498
+ }
499
+ }
500
+ evaluateAndFetch() {
501
+ this.activeContext = this.readActiveContext();
502
+ if (!this.activeContext || this.activeContext.query.length < this.config.minChars) {
503
+ this.dropdown.hide();
504
+ return;
505
+ }
506
+ const requestId = ++this.requestSerial;
507
+ if (this.debounceTimer) {
508
+ clearTimeout(this.debounceTimer);
509
+ }
510
+ this.debounceTimer = window.setTimeout(async () => {
511
+ try {
512
+ const items = await this.provider({
513
+ trigger: this.activeContext.trigger,
514
+ query: this.activeContext.query,
515
+ limit: this.config.maxResults
516
+ });
517
+ if (requestId !== this.requestSerial || !this.activeContext) {
518
+ return;
519
+ }
520
+ const caretRect = this.getCaretRect();
521
+ if (!caretRect) {
522
+ this.dropdown.hide();
523
+ return;
524
+ }
525
+ this.dropdown.show(
526
+ {
527
+ left: caretRect.left,
528
+ top: caretRect.bottom + 6
529
+ },
530
+ items
531
+ );
532
+ } catch {
533
+ this.dropdown.hide();
534
+ }
535
+ }, this.config.debounceMs);
536
+ }
537
+ readActiveContext() {
538
+ const selection = window.getSelection();
539
+ if (!selection || !selection.rangeCount || !selection.isCollapsed) {
540
+ return null;
541
+ }
542
+ const range = selection.getRangeAt(0);
543
+ let node = range.startContainer;
544
+ let offset = range.startOffset;
545
+ if (node.nodeType !== Node.TEXT_NODE) {
546
+ if (!node.childNodes.length || offset === 0) {
547
+ return null;
548
+ }
549
+ const child = node.childNodes[Math.max(0, offset - 1)];
550
+ if (!child || child.nodeType !== Node.TEXT_NODE) {
551
+ return null;
552
+ }
553
+ node = child;
554
+ offset = child.data.length;
555
+ }
556
+ const textNode = node;
557
+ const textBefore = textNode.data.slice(0, offset);
558
+ let best = null;
559
+ for (const trigger of this.config.triggerSymbols) {
560
+ const index = textBefore.lastIndexOf(trigger);
561
+ if (index < 0) {
562
+ continue;
563
+ }
564
+ if (best === null || index > best.index) {
565
+ best = { trigger, index };
566
+ }
567
+ }
568
+ if (!best) {
569
+ return null;
570
+ }
571
+ const charBeforeTrigger = best.index === 0 ? " " : textBefore[best.index - 1];
572
+ if (!/\s/.test(charBeforeTrigger)) {
573
+ return null;
574
+ }
575
+ const query = textBefore.slice(best.index + best.trigger.length);
576
+ if (/\s/.test(query)) {
577
+ return null;
578
+ }
579
+ return {
580
+ trigger: best.trigger,
581
+ query,
582
+ textNode,
583
+ startOffset: best.index,
584
+ endOffset: offset
585
+ };
586
+ }
587
+ selectMention(item) {
588
+ const context = this.activeContext;
589
+ if (!context) {
590
+ return;
591
+ }
592
+ const range = document.createRange();
593
+ range.setStart(context.textNode, context.startOffset);
594
+ range.setEnd(context.textNode, context.endOffset);
595
+ range.deleteContents();
596
+ const anchor = document.createElement("a");
597
+ anchor.className = "editorjs-mention";
598
+ anchor.contentEditable = "false";
599
+ anchor.href = `mention://${encodeURIComponent(item.id)}`;
600
+ anchor.dataset.mentionPayload = encodeURIComponent(
601
+ JSON.stringify({
602
+ id: item.id,
603
+ displayName: item.displayName,
604
+ description: item.description,
605
+ image: item.image,
606
+ link: item.link
607
+ })
608
+ );
609
+ anchor.dataset.mentionLink = item.link || "";
610
+ anchor.dataset.mentionDescription = item.description || "";
611
+ anchor.dataset.mentionImage = item.image || "";
612
+ anchor.dataset.mentionId = item.id;
613
+ anchor.dataset.mentionDisplayName = item.displayName;
614
+ anchor.dataset.mentionTrigger = context.trigger;
615
+ anchor.textContent = `${context.trigger}${item.displayName}`;
616
+ const trailingSpace = document.createTextNode("\xA0");
617
+ range.insertNode(trailingSpace);
618
+ range.insertNode(anchor);
619
+ this.applyMentionRendering(anchor, item, context.trigger, "insert");
620
+ const selection = window.getSelection();
621
+ if (selection) {
622
+ selection.removeAllRanges();
623
+ const caretRange = document.createRange();
624
+ caretRange.setStartAfter(trailingSpace);
625
+ caretRange.collapse(true);
626
+ selection.addRange(caretRange);
627
+ }
628
+ this.dropdown.hide();
629
+ this.hideTooltip();
630
+ this.activeContext = null;
631
+ this.config.onSelect?.(item);
632
+ }
633
+ showTooltip(anchor, item) {
634
+ const rect = anchor.getBoundingClientRect();
635
+ const linkHtml = item.link ? `<a class="editorjs-mention-tooltip-link" href="${escapeHtml(item.link)}" target="_blank" rel="noopener noreferrer">Open details</a>` : "";
636
+ const imageHtml = item.image ? `<img class="editorjs-mention-tooltip-image" src="${escapeHtml(item.image)}" alt="${escapeHtml(item.displayName)}" />` : `<div class="editorjs-mention-tooltip-placeholder">${escapeHtml(item.displayName.slice(0, 1).toUpperCase())}</div>`;
637
+ this.tooltipRoot.innerHTML = `
638
+ <div class="editorjs-mention-tooltip-inner">
639
+ ${imageHtml}
640
+ <div class="editorjs-mention-tooltip-main">
641
+ <div class="editorjs-mention-tooltip-name">${escapeHtml(item.displayName)}</div>
642
+ ${item.description ? `<div class="editorjs-mention-tooltip-description">${escapeHtml(item.description)}</div>` : ""}
643
+ ${linkHtml}
644
+ </div>
645
+ </div>
646
+ `;
647
+ this.tooltipRoot.style.display = "block";
648
+ this.tooltipRoot.style.left = `${Math.max(8, rect.left)}px`;
649
+ this.tooltipRoot.style.top = `${Math.max(8, rect.bottom + 6)}px`;
650
+ }
651
+ hideTooltip() {
652
+ this.tooltipRoot.style.display = "none";
653
+ this.tooltipRoot.innerHTML = "";
654
+ }
655
+ readMentionFromElement(anchor) {
656
+ const payload = anchor.dataset.mentionPayload;
657
+ if (payload) {
658
+ try {
659
+ const json = JSON.parse(decodeURIComponent(payload));
660
+ if (json && typeof json.id === "string" && typeof json.displayName === "string") {
661
+ return json;
662
+ }
663
+ } catch {
664
+ }
665
+ }
666
+ const id = anchor.dataset.mentionId;
667
+ const displayName = anchor.dataset.mentionDisplayName || anchor.textContent?.replace(/^@/, "");
668
+ if (!id || !displayName) {
669
+ return null;
670
+ }
671
+ return {
672
+ id,
673
+ displayName,
674
+ description: anchor.dataset.mentionDescription || void 0,
675
+ image: anchor.dataset.mentionImage || void 0,
676
+ link: anchor.dataset.mentionLink || void 0
677
+ };
678
+ }
679
+ applyMentionRendering(anchor, item, trigger, source) {
680
+ const defaultText = `${trigger}${item.displayName}`;
681
+ if (!anchor.textContent || anchor.textContent.trim().length === 0) {
682
+ anchor.textContent = defaultText;
683
+ }
684
+ if (!anchor.classList.contains("editorjs-mention")) {
685
+ anchor.classList.add("editorjs-mention");
686
+ }
687
+ this.config.renderMention?.({
688
+ item,
689
+ trigger,
690
+ defaultText,
691
+ element: anchor,
692
+ source,
693
+ context: this.config.mentionRenderContext
694
+ });
695
+ }
696
+ getCaretRect() {
697
+ const selection = window.getSelection();
698
+ if (!selection || !selection.rangeCount) {
699
+ return null;
700
+ }
701
+ const range = selection.getRangeAt(0).cloneRange();
702
+ range.collapse(true);
703
+ const rect = range.getBoundingClientRect();
704
+ if (!rect || rect.x === 0 && rect.y === 0 && rect.width === 0 && rect.height === 0) {
705
+ const marker = document.createElement("span");
706
+ marker.textContent = "\u200B";
707
+ range.insertNode(marker);
708
+ const markerRect = marker.getBoundingClientRect();
709
+ marker.remove();
710
+ return markerRect;
711
+ }
712
+ return rect;
713
+ }
714
+ };
715
+ function escapeHtml(input) {
716
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
717
+ }
718
+ function normalizeMentionAnchorsHtml(html) {
719
+ const root = document.createElement("div");
720
+ root.innerHTML = html;
721
+ const mentions = Array.from(root.querySelectorAll("a.editorjs-mention, span.editorjs-mention"));
722
+ for (const mention of mentions) {
723
+ const el = mention;
724
+ const text = (el.textContent || "").replace(/\u00A0/g, " ");
725
+ const id = el.dataset.mentionId || mentionIdFromHref(el.getAttribute("href")) || text;
726
+ const displayName = el.dataset.mentionDisplayName || text.replace(/^@/, "");
727
+ const trigger = el.dataset.mentionTrigger || (text.startsWith("@") ? "@" : "@");
728
+ const payload = safeMentionPayload(el);
729
+ const anchor = document.createElement("a");
730
+ anchor.className = "editorjs-mention";
731
+ anchor.contentEditable = "false";
732
+ anchor.href = `mention://${encodeURIComponent(id)}`;
733
+ anchor.dataset.mentionId = id;
734
+ anchor.dataset.mentionDisplayName = displayName;
735
+ anchor.dataset.mentionTrigger = trigger;
736
+ anchor.dataset.mentionDescription = payload.description || "";
737
+ anchor.dataset.mentionImage = payload.image || "";
738
+ anchor.dataset.mentionLink = payload.link || "";
739
+ anchor.dataset.mentionPayload = encodeURIComponent(
740
+ JSON.stringify({
741
+ id,
742
+ displayName,
743
+ description: payload.description,
744
+ image: payload.image,
745
+ link: payload.link
746
+ })
747
+ );
748
+ anchor.textContent = text || `${trigger}${displayName}`;
749
+ el.replaceWith(anchor);
750
+ }
751
+ return root.innerHTML;
752
+ }
753
+ function safeMentionPayload(el) {
754
+ const encoded = el.dataset.mentionPayload;
755
+ if (encoded) {
756
+ try {
757
+ const parsed = JSON.parse(decodeURIComponent(encoded));
758
+ return parsed || {};
759
+ } catch {
760
+ }
761
+ }
762
+ return {
763
+ description: el.dataset.mentionDescription || void 0,
764
+ image: el.dataset.mentionImage || void 0,
765
+ link: el.dataset.mentionLink || void 0
766
+ };
767
+ }
768
+ function mentionIdFromHref(href) {
769
+ if (!href || !href.startsWith("mention://")) {
770
+ return void 0;
771
+ }
772
+ return decodeURIComponent(href.slice("mention://".length));
773
+ }
774
+
775
+ // src/serialization.ts
776
+ function encodeMentionsInOutput(output) {
777
+ const clone = cloneOutput(output);
778
+ for (const block of clone.blocks) {
779
+ const text = block.data?.text;
780
+ if (typeof text !== "string") {
781
+ continue;
782
+ }
783
+ const encoded = encodeMentionsFromHtml(text);
784
+ block.data.text = encoded.text;
785
+ if (encoded.entities.length > 0) {
786
+ block.data.entities = encoded.entities;
787
+ } else {
788
+ delete block.data.entities;
789
+ }
790
+ }
791
+ return clone;
792
+ }
793
+ function decodeMentionsInOutput(output) {
794
+ const clone = cloneOutput(output);
795
+ for (const block of clone.blocks) {
796
+ const text = block.data?.text;
797
+ const entities = block.data?.entities;
798
+ if (typeof text !== "string" || !Array.isArray(entities)) {
799
+ continue;
800
+ }
801
+ const mentionEntities = entities.filter(isMentionEntity).sort((a, b) => a.start - b.start);
802
+ block.data.text = decodeMentionsToHtml(text, mentionEntities);
803
+ }
804
+ return clone;
805
+ }
806
+ function encodeMentionsFromHtml(html) {
807
+ const root = document.createElement("div");
808
+ root.innerHTML = html;
809
+ const entities = [];
810
+ let text = "";
811
+ const walk = (node) => {
812
+ if (node.nodeType === Node.TEXT_NODE) {
813
+ text += (node.textContent || "").replace(/\u00A0/g, " ");
814
+ return;
815
+ }
816
+ if (node.nodeType !== Node.ELEMENT_NODE) {
817
+ return;
818
+ }
819
+ const el = node;
820
+ const isMention = el.tagName === "A" && (el.classList.contains("editorjs-mention") || mentionIdFromHref2(el.getAttribute("href"))) || !!el.dataset.mentionId;
821
+ if (isMention) {
822
+ const mentionText = (el.textContent || "").replace(/\u00A0/g, " ");
823
+ const payload = readMentionPayload(el);
824
+ const id = payload.id || el.dataset.mentionId || mentionIdFromHref2(el.getAttribute("href")) || mentionText;
825
+ const start = text.length;
826
+ text += mentionText;
827
+ const end = text.length;
828
+ entities.push({
829
+ type: "mention",
830
+ id,
831
+ displayName: payload.displayName || el.dataset.mentionDisplayName || mentionText.replace(/^@/, ""),
832
+ description: payload.description || void 0,
833
+ image: payload.image || void 0,
834
+ link: payload.link || void 0,
835
+ trigger: el.dataset.mentionTrigger || (mentionText.startsWith("@") ? "@" : void 0),
836
+ start,
837
+ end
838
+ });
839
+ return;
840
+ }
841
+ for (const child of Array.from(el.childNodes)) {
842
+ walk(child);
843
+ }
844
+ };
845
+ for (const node of Array.from(root.childNodes)) {
846
+ walk(node);
847
+ }
848
+ return { text, entities };
849
+ }
850
+ function decodeMentionsToHtml(text, entities) {
851
+ let cursor = 0;
852
+ let html = "";
853
+ for (const entity of entities) {
854
+ if (entity.start < cursor || entity.end < entity.start || entity.end > text.length) {
855
+ continue;
856
+ }
857
+ const rawBefore = text.slice(cursor, entity.start);
858
+ const rawMention = text.slice(entity.start, entity.end);
859
+ html += escapeHtml2(rawBefore);
860
+ html += renderMentionAnchor(rawMention, entity);
861
+ cursor = entity.end;
862
+ }
863
+ html += escapeHtml2(text.slice(cursor));
864
+ return html.replace(/ /g, " &nbsp;");
865
+ }
866
+ function renderMentionAnchor(displayText, entity) {
867
+ const payload = encodeURIComponent(
868
+ JSON.stringify({
869
+ id: entity.id,
870
+ displayName: entity.displayName,
871
+ description: entity.description,
872
+ image: entity.image,
873
+ link: entity.link
874
+ })
875
+ );
876
+ const href = `mention://${encodeURIComponent(entity.id)}`;
877
+ const visible = displayText || `${entity.trigger || "@"}${entity.displayName}`;
878
+ return `<a class="editorjs-mention" href="${escapeAttr(href)}" data-mention-id="${escapeAttr(
879
+ entity.id
880
+ )}" data-mention-display-name="${escapeAttr(entity.displayName)}" data-mention-trigger="${escapeAttr(
881
+ entity.trigger || "@"
882
+ )}" data-mention-payload="${escapeAttr(payload)}">${escapeHtml2(visible)}</a>`;
883
+ }
884
+ function readMentionPayload(el) {
885
+ const raw = el.dataset.mentionPayload;
886
+ if (!raw) {
887
+ return { id: "", displayName: "" };
888
+ }
889
+ try {
890
+ const decoded = JSON.parse(decodeURIComponent(raw));
891
+ return decoded || { id: "", displayName: "" };
892
+ } catch {
893
+ return { id: "", displayName: "" };
894
+ }
895
+ }
896
+ function mentionIdFromHref2(href) {
897
+ if (!href) {
898
+ return void 0;
899
+ }
900
+ if (!href.startsWith("mention://")) {
901
+ return void 0;
902
+ }
903
+ return decodeURIComponent(href.slice("mention://".length));
904
+ }
905
+ function isMentionEntity(value) {
906
+ if (!value || typeof value !== "object") {
907
+ return false;
908
+ }
909
+ const item = value;
910
+ return item.type === "mention" && typeof item.id === "string" && typeof item.displayName === "string" && typeof item.start === "number" && typeof item.end === "number";
911
+ }
912
+ function escapeHtml2(input) {
913
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
914
+ }
915
+ function escapeAttr(input) {
916
+ return escapeHtml2(input).replace(/`/g, "&#96;");
917
+ }
918
+ function cloneOutput(output) {
919
+ return JSON.parse(JSON.stringify(output));
920
+ }
921
+ export {
922
+ EditorJSMentions,
923
+ createRestMentionProvider,
924
+ decodeMentionsInOutput,
925
+ encodeMentionsFromHtml,
926
+ encodeMentionsInOutput
927
+ };