fauna-api 0.0.0-stage → 1.0.676
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/brands.d.ts +2 -0
- package/dist/brands.js +2 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +121 -0
- package/dist/src/ant/chat.d.ts +130 -0
- package/dist/src/ant/chat.js +467 -0
- package/dist/src/ant/format.d.ts +40 -0
- package/dist/src/ant/format.js +143 -0
- package/dist/src/ant/graph.d.ts +130 -0
- package/dist/src/ant/graph.js +449 -0
- package/dist/src/ant/index.d.ts +16 -0
- package/dist/src/ant/index.js +61 -0
- package/dist/src/ant/persona.d.ts +151 -0
- package/dist/src/ant/persona.js +136 -0
- package/dist/src/ant/timeline.d.ts +283 -0
- package/dist/src/ant/timeline.js +1025 -0
- package/dist/src/authentification.d.ts +45 -0
- package/dist/src/authentification.js +194 -0
- package/dist/src/billing.d.ts +135 -0
- package/dist/src/billing.js +128 -0
- package/dist/src/calendar.d.ts +83 -0
- package/dist/src/calendar.js +133 -0
- package/dist/src/chats.d.ts +113 -0
- package/dist/src/chats.js +100 -0
- package/dist/src/cicada/contacts.d.ts +31 -0
- package/dist/src/cicada/contacts.js +28 -0
- package/dist/src/cicada/conversations.d.ts +73 -0
- package/dist/src/cicada/conversations.js +236 -0
- package/dist/src/cicada/inboxes.d.ts +48 -0
- package/dist/src/cicada/inboxes.js +82 -0
- package/dist/src/cicada/index.d.ts +16 -0
- package/dist/src/cicada/index.js +61 -0
- package/dist/src/cicada/phone.d.ts +48 -0
- package/dist/src/cicada/phone.js +306 -0
- package/dist/src/drive.d.ts +80 -0
- package/dist/src/drive.js +472 -0
- package/dist/src/extensions/index.d.ts +4 -0
- package/dist/src/extensions/index.js +5 -0
- package/dist/src/extensions/web/index.d.ts +6 -0
- package/dist/src/extensions/web/index.js +7 -0
- package/dist/src/extensions/web/session.d.ts +106 -0
- package/dist/src/extensions/web/session.js +469 -0
- package/dist/src/extensions/web/tools.d.ts +13 -0
- package/dist/src/extensions/web/tools.js +72 -0
- package/dist/src/friends.d.ts +39 -0
- package/dist/src/friends.js +119 -0
- package/dist/src/groups.d.ts +82 -0
- package/dist/src/groups.js +269 -0
- package/dist/src/inbox.d.ts +11 -0
- package/dist/src/inbox.js +35 -0
- package/dist/src/integrations.d.ts +52 -0
- package/dist/src/integrations.js +205 -0
- package/dist/src/magpie/explorer.d.ts +14 -0
- package/dist/src/magpie/explorer.js +36 -0
- package/dist/src/magpie/index.d.ts +16 -0
- package/dist/src/magpie/index.js +61 -0
- package/dist/src/magpie/portfolio.d.ts +52 -0
- package/dist/src/magpie/portfolio.js +179 -0
- package/dist/src/magpie/quotes.d.ts +35 -0
- package/dist/src/magpie/quotes.js +47 -0
- package/dist/src/magpie/room.d.ts +61 -0
- package/dist/src/magpie/room.js +238 -0
- package/dist/src/maps.d.ts +159 -0
- package/dist/src/maps.js +171 -0
- package/dist/src/marmot/browser.d.ts +139 -0
- package/dist/src/marmot/browser.js +389 -0
- package/dist/src/marmot/index.d.ts +20 -0
- package/dist/src/marmot/index.js +65 -0
- package/dist/src/marmot/media.d.ts +56 -0
- package/dist/src/marmot/media.js +265 -0
- package/dist/src/marmot/research.d.ts +337 -0
- package/dist/src/marmot/research.js +1138 -0
- package/dist/src/marmot/scraper.d.ts +59 -0
- package/dist/src/marmot/scraper.js +94 -0
- package/dist/src/marmot/web.d.ts +402 -0
- package/dist/src/marmot/web.js +2586 -0
- package/dist/src/marmot/workspace.d.ts +81 -0
- package/dist/src/marmot/workspace.js +263 -0
- package/dist/src/parrot/agent.d.ts +53 -0
- package/dist/src/parrot/agent.js +93 -0
- package/dist/src/parrot/chat.d.ts +84 -0
- package/dist/src/parrot/chat.js +237 -0
- package/dist/src/parrot/format.d.ts +128 -0
- package/dist/src/parrot/format.js +639 -0
- package/dist/src/parrot/generative.d.ts +261 -0
- package/dist/src/parrot/generative.js +185 -0
- package/dist/src/parrot/index.d.ts +22 -0
- package/dist/src/parrot/index.js +67 -0
- package/dist/src/parrot/room.d.ts +225 -0
- package/dist/src/parrot/room.js +466 -0
- package/dist/src/parrot/routing.d.ts +90 -0
- package/dist/src/parrot/routing.js +78 -0
- package/dist/src/parrot/share.d.ts +126 -0
- package/dist/src/parrot/share.js +261 -0
- package/dist/src/parrot/socket.d.ts +41 -0
- package/dist/src/parrot/socket.js +478 -0
- package/dist/src/payments.d.ts +73 -0
- package/dist/src/payments.js +107 -0
- package/dist/src/private/cards.css +75 -0
- package/dist/src/rooms.d.ts +76 -0
- package/dist/src/rooms.js +168 -0
- package/dist/src/shared/schema.d.ts +24 -0
- package/dist/src/shared/schema.js +1 -0
- package/dist/src/shared/server.d.ts +65 -0
- package/dist/src/shared/server.js +302 -0
- package/dist/src/termite/environment.d.ts +110 -0
- package/dist/src/termite/environment.js +263 -0
- package/dist/src/termite/index.d.ts +14 -0
- package/dist/src/termite/index.js +59 -0
- package/dist/src/termite/os.d.ts +326 -0
- package/dist/src/termite/os.js +572 -0
- package/dist/src/termite/versioning.d.ts +94 -0
- package/dist/src/termite/versioning.js +217 -0
- package/dist/src/tickets.d.ts +43 -0
- package/dist/src/tickets.js +138 -0
- package/dist/src/travel.d.ts +85 -0
- package/dist/src/travel.js +44 -0
- package/dist/src/ui/authentification.d.ts +24 -0
- package/dist/src/ui/authentification.js +177 -0
- package/dist/src/ui/brands.css +11 -0
- package/dist/src/ui/brands.d.ts +7 -0
- package/dist/src/ui/brands.js +14 -0
- package/dist/src/ui/browser.d.ts +74 -0
- package/dist/src/ui/browser.js +470 -0
- package/dist/src/ui/chat.d.ts +183 -0
- package/dist/src/ui/chat.js +2379 -0
- package/dist/src/ui/chatPanel.d.ts +109 -0
- package/dist/src/ui/chatPanel.js +768 -0
- package/dist/src/ui/chatSequence.d.ts +36 -0
- package/dist/src/ui/chatSequence.js +42 -0
- package/dist/src/ui/crown.d.ts +19 -0
- package/dist/src/ui/crown.js +199 -0
- package/dist/src/ui/cursor.d.ts +73 -0
- package/dist/src/ui/cursor.js +175 -0
- package/dist/src/ui/device.d.ts +33 -0
- package/dist/src/ui/device.js +49 -0
- package/dist/src/ui/drive.d.ts +69 -0
- package/dist/src/ui/drive.js +1243 -0
- package/dist/src/ui/generative.d.ts +39 -0
- package/dist/src/ui/generative.js +313 -0
- package/dist/src/ui/hover.d.ts +6 -0
- package/dist/src/ui/hover.js +210 -0
- package/dist/src/ui/index.d.ts +42 -0
- package/dist/src/ui/index.js +87 -0
- package/dist/src/ui/integrations.d.ts +13 -0
- package/dist/src/ui/integrations.js +175 -0
- package/dist/src/ui/maps.d.ts +87 -0
- package/dist/src/ui/maps.js +547 -0
- package/dist/src/ui/roster.d.ts +32 -0
- package/dist/src/ui/roster.js +163 -0
- package/dist/src/ui/talk.d.ts +12 -0
- package/dist/src/ui/talk.js +560 -0
- package/dist/src/ui/tickets.d.ts +13 -0
- package/dist/src/ui/tickets.js +966 -0
- package/dist/src/ui/toast.d.ts +7 -0
- package/dist/src/ui/toast.js +258 -0
- package/dist/src/ui/tokenText.d.ts +18 -0
- package/dist/src/ui/tokenText.js +96 -0
- package/dist/src/ui/tokenized.d.ts +42 -0
- package/dist/src/ui/tokenized.js +622 -0
- package/dist/src/ui/ui.css +4763 -0
- package/dist/src/ui/view.d.ts +23 -0
- package/dist/src/ui/view.js +584 -0
- package/dist/src/view.d.ts +113 -0
- package/dist/src/view.js +386 -0
- package/dist/src/webhooks.d.ts +66 -0
- package/dist/src/webhooks.js +78 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/package.json +89 -6
- package/README.md +0 -3
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import gsap from "gsap";
|
|
2
|
+
import { Fauna } from "fauna-api";
|
|
3
|
+
import { serializeParts } from "./tokenText.js";
|
|
4
|
+
export { encodeToken, serializeParts, parseRichText, hasTokens, toPlainText } from "./tokenText.js";
|
|
5
|
+
// ─── createTokenizedInput ────────────────────────────────────────────────────
|
|
6
|
+
export function createTokenizedInput(container, options = {}) {
|
|
7
|
+
const triggers = options.triggers ?? [];
|
|
8
|
+
const triggerChars = new Set(triggers.map(t => t.char));
|
|
9
|
+
// ── DOM ──
|
|
10
|
+
const root = document.createElement('div');
|
|
11
|
+
root.className = 'tokenized-root';
|
|
12
|
+
const editor = document.createElement('div');
|
|
13
|
+
editor.className = 'tokenized-editor';
|
|
14
|
+
editor.contentEditable = 'true';
|
|
15
|
+
editor.setAttribute('role', 'textbox');
|
|
16
|
+
editor.setAttribute('aria-multiline', options.multiline ? 'true' : 'false');
|
|
17
|
+
if (options.placeholder) {
|
|
18
|
+
editor.dataset.placeholder = options.placeholder;
|
|
19
|
+
}
|
|
20
|
+
root.appendChild(editor);
|
|
21
|
+
const suggestionsEl = document.createElement('div');
|
|
22
|
+
suggestionsEl.className = 'tokenized-suggestions';
|
|
23
|
+
suggestionsEl.style.display = 'none';
|
|
24
|
+
root.appendChild(suggestionsEl);
|
|
25
|
+
container.appendChild(root);
|
|
26
|
+
// ── Suggestion state ──
|
|
27
|
+
let activeTrigger = null;
|
|
28
|
+
let triggerAnchorNode = null;
|
|
29
|
+
let triggerAnchorOffset = 0;
|
|
30
|
+
let suggestionItems = [];
|
|
31
|
+
let activeIndex = 0;
|
|
32
|
+
let suggestGeneration = 0;
|
|
33
|
+
// ── Helpers ──
|
|
34
|
+
function parseParts() {
|
|
35
|
+
const parts = [];
|
|
36
|
+
function walk(node) {
|
|
37
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
38
|
+
const text = node.textContent ?? '';
|
|
39
|
+
if (text.length > 0) {
|
|
40
|
+
// Merge consecutive text parts
|
|
41
|
+
const last = parts[parts.length - 1];
|
|
42
|
+
if (last && last.kind === 'text') {
|
|
43
|
+
last.text += text;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
parts.push({ kind: 'text', text });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
51
|
+
const el = node;
|
|
52
|
+
if (el.classList.contains('tokenized-token')) {
|
|
53
|
+
parts.push({
|
|
54
|
+
kind: 'token',
|
|
55
|
+
token: {
|
|
56
|
+
type: el.dataset.type ?? '',
|
|
57
|
+
value: el.dataset.value ?? '',
|
|
58
|
+
label: el.dataset.label ?? '',
|
|
59
|
+
icon: el.dataset.icon || undefined
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (el.tagName === 'BR') {
|
|
64
|
+
const last = parts[parts.length - 1];
|
|
65
|
+
if (last && last.kind === 'text') {
|
|
66
|
+
last.text += '\n';
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
parts.push({ kind: 'text', text: '\n' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Recurse into non-token elements (e.g. divs from contenteditable line wrapping)
|
|
74
|
+
for (const child of node.childNodes) {
|
|
75
|
+
walk(child);
|
|
76
|
+
}
|
|
77
|
+
// Contenteditable wraps lines in divs — add newline after block elements
|
|
78
|
+
if (el.tagName === 'DIV' || el.tagName === 'P') {
|
|
79
|
+
const last = parts[parts.length - 1];
|
|
80
|
+
if (last && last.kind === 'text' && !last.text.endsWith('\n')) {
|
|
81
|
+
last.text += '\n';
|
|
82
|
+
}
|
|
83
|
+
else if (!last || last.kind !== 'text') {
|
|
84
|
+
parts.push({ kind: 'text', text: '\n' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const child of editor.childNodes) {
|
|
91
|
+
walk(child);
|
|
92
|
+
}
|
|
93
|
+
// Trim trailing newline
|
|
94
|
+
const last = parts[parts.length - 1];
|
|
95
|
+
if (last && last.kind === 'text') {
|
|
96
|
+
last.text = last.text.replace(/\n+$/, '');
|
|
97
|
+
if (last.text.length === 0)
|
|
98
|
+
parts.pop();
|
|
99
|
+
}
|
|
100
|
+
return parts;
|
|
101
|
+
}
|
|
102
|
+
function serializeText(parts) {
|
|
103
|
+
return serializeParts(parts);
|
|
104
|
+
}
|
|
105
|
+
function buildTokenSpan(token) {
|
|
106
|
+
const span = document.createElement('span');
|
|
107
|
+
span.className = 'tokenized-token';
|
|
108
|
+
span.contentEditable = 'false';
|
|
109
|
+
span.dataset.type = token.type;
|
|
110
|
+
span.dataset.value = token.value;
|
|
111
|
+
span.dataset.label = token.label;
|
|
112
|
+
if (token.icon)
|
|
113
|
+
span.dataset.icon = token.icon;
|
|
114
|
+
if (token.icon) {
|
|
115
|
+
const iconEl = document.createElement('span');
|
|
116
|
+
iconEl.className = 'material-symbols-outlined tokenized-token-icon';
|
|
117
|
+
iconEl.textContent = token.icon;
|
|
118
|
+
span.appendChild(iconEl);
|
|
119
|
+
}
|
|
120
|
+
const labelEl = document.createElement('span');
|
|
121
|
+
labelEl.className = 'tokenized-token-label';
|
|
122
|
+
labelEl.textContent = token.label;
|
|
123
|
+
span.appendChild(labelEl);
|
|
124
|
+
const removeEl = document.createElement('span');
|
|
125
|
+
removeEl.className = 'material-symbols-outlined tokenized-token-remove';
|
|
126
|
+
removeEl.textContent = 'close';
|
|
127
|
+
removeEl.onmousedown = (e) => {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
gsap.to(span, {
|
|
131
|
+
duration: 0.2, opacity: 0, scale: 0.8, ease: 'power2.in',
|
|
132
|
+
onComplete: () => {
|
|
133
|
+
// Remove the trailing zero-width space if present
|
|
134
|
+
const next = span.nextSibling;
|
|
135
|
+
if (next && next.nodeType === Node.TEXT_NODE && next.textContent === '\u200B') {
|
|
136
|
+
next.remove();
|
|
137
|
+
}
|
|
138
|
+
span.remove();
|
|
139
|
+
fireInput();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
span.appendChild(removeEl);
|
|
144
|
+
return span;
|
|
145
|
+
}
|
|
146
|
+
function insertTokenAtCaret(token) {
|
|
147
|
+
const span = buildTokenSpan(token);
|
|
148
|
+
const zwsp = document.createTextNode('\u200B');
|
|
149
|
+
const sel = window.getSelection();
|
|
150
|
+
const range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
151
|
+
// Only honor the caret when it actually lives inside this editor.
|
|
152
|
+
// Otherwise the token would be injected wherever the document selection
|
|
153
|
+
// happens to be (e.g. up in the page header) — so append into the editor
|
|
154
|
+
// instead and drop the caret after it.
|
|
155
|
+
if (!range || !editor.contains(range.commonAncestorContainer)) {
|
|
156
|
+
editor.appendChild(span);
|
|
157
|
+
editor.appendChild(zwsp);
|
|
158
|
+
if (sel) {
|
|
159
|
+
const end = document.createRange();
|
|
160
|
+
end.setStartAfter(zwsp);
|
|
161
|
+
end.collapse(true);
|
|
162
|
+
sel.removeAllRanges();
|
|
163
|
+
sel.addRange(end);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
range.deleteContents();
|
|
168
|
+
range.insertNode(zwsp);
|
|
169
|
+
range.insertNode(span);
|
|
170
|
+
// Move caret after zero-width space
|
|
171
|
+
range.setStartAfter(zwsp);
|
|
172
|
+
range.setEndAfter(zwsp);
|
|
173
|
+
sel.removeAllRanges();
|
|
174
|
+
sel.addRange(range);
|
|
175
|
+
}
|
|
176
|
+
gsap.from(span, { duration: 0.25, opacity: 0, scale: 0.85, ease: 'power2.out' });
|
|
177
|
+
fireInput();
|
|
178
|
+
}
|
|
179
|
+
function fireInput() {
|
|
180
|
+
options.onInput?.(parseParts());
|
|
181
|
+
}
|
|
182
|
+
// ── Suggestion popup ──
|
|
183
|
+
function showSuggestions(items) {
|
|
184
|
+
suggestionItems = items;
|
|
185
|
+
activeIndex = 0;
|
|
186
|
+
suggestionsEl.innerHTML = '';
|
|
187
|
+
if (items.length === 0) {
|
|
188
|
+
hideSuggestions();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (let i = 0; i < items.length; i++) {
|
|
192
|
+
const row = document.createElement('div');
|
|
193
|
+
row.className = 'tokenized-suggestion';
|
|
194
|
+
if (i === 0)
|
|
195
|
+
row.classList.add('active');
|
|
196
|
+
if (items[i].image) {
|
|
197
|
+
const img = document.createElement('img');
|
|
198
|
+
img.className = 'tokenized-suggestion-image';
|
|
199
|
+
img.src = items[i].image;
|
|
200
|
+
row.appendChild(img);
|
|
201
|
+
}
|
|
202
|
+
else if (items[i].icon) {
|
|
203
|
+
const icon = document.createElement('span');
|
|
204
|
+
icon.className = 'material-symbols-outlined tokenized-suggestion-icon';
|
|
205
|
+
icon.textContent = items[i].icon;
|
|
206
|
+
row.appendChild(icon);
|
|
207
|
+
}
|
|
208
|
+
const text = document.createElement('div');
|
|
209
|
+
text.className = 'tokenized-suggestion-text';
|
|
210
|
+
const label = document.createElement('div');
|
|
211
|
+
label.className = 'tokenized-suggestion-label';
|
|
212
|
+
label.textContent = items[i].label;
|
|
213
|
+
text.appendChild(label);
|
|
214
|
+
if (items[i].subtitle) {
|
|
215
|
+
const sub = document.createElement('div');
|
|
216
|
+
sub.className = 'tokenized-suggestion-subtitle';
|
|
217
|
+
sub.textContent = items[i].subtitle;
|
|
218
|
+
text.appendChild(sub);
|
|
219
|
+
}
|
|
220
|
+
row.appendChild(text);
|
|
221
|
+
row.onmousedown = (e) => {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
pickSuggestion(i);
|
|
224
|
+
};
|
|
225
|
+
suggestionsEl.appendChild(row);
|
|
226
|
+
}
|
|
227
|
+
// Position popup near caret
|
|
228
|
+
positionSuggestions();
|
|
229
|
+
suggestionsEl.style.display = 'flex';
|
|
230
|
+
gsap.fromTo(suggestionsEl, { opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.15, ease: 'power2.out' });
|
|
231
|
+
}
|
|
232
|
+
function positionSuggestions() {
|
|
233
|
+
const sel = window.getSelection();
|
|
234
|
+
if (!sel || sel.rangeCount === 0)
|
|
235
|
+
return;
|
|
236
|
+
const range = sel.getRangeAt(0).cloneRange();
|
|
237
|
+
range.collapse(true);
|
|
238
|
+
const rect = range.getBoundingClientRect();
|
|
239
|
+
const rootRect = root.getBoundingClientRect();
|
|
240
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
241
|
+
// Fallback — position at bottom-left of editor
|
|
242
|
+
suggestionsEl.style.left = '0px';
|
|
243
|
+
suggestionsEl.style.bottom = `${root.offsetHeight - editor.offsetTop}px`;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
suggestionsEl.style.left = `${rect.left - rootRect.left}px`;
|
|
247
|
+
suggestionsEl.style.bottom = `${rootRect.bottom - rect.top + 4}px`;
|
|
248
|
+
}
|
|
249
|
+
function hideSuggestions() {
|
|
250
|
+
activeTrigger = null;
|
|
251
|
+
triggerAnchorNode = null;
|
|
252
|
+
suggestionItems = [];
|
|
253
|
+
suggestGeneration++;
|
|
254
|
+
suggestionsEl.style.display = 'none';
|
|
255
|
+
suggestionsEl.innerHTML = '';
|
|
256
|
+
}
|
|
257
|
+
function updateActiveRow() {
|
|
258
|
+
const rows = suggestionsEl.querySelectorAll('.tokenized-suggestion');
|
|
259
|
+
rows.forEach((row, i) => {
|
|
260
|
+
row.classList.toggle('active', i === activeIndex);
|
|
261
|
+
});
|
|
262
|
+
// Scroll active into view
|
|
263
|
+
const activeRow = rows[activeIndex];
|
|
264
|
+
activeRow?.scrollIntoView({ block: 'nearest' });
|
|
265
|
+
}
|
|
266
|
+
function pickSuggestion(index) {
|
|
267
|
+
if (!activeTrigger || index < 0 || index >= suggestionItems.length) {
|
|
268
|
+
hideSuggestions();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const token = activeTrigger.onPick(suggestionItems[index]);
|
|
272
|
+
// Delete the trigger char + query text before inserting the token
|
|
273
|
+
const sel = window.getSelection();
|
|
274
|
+
if (sel && sel.rangeCount > 0 && triggerAnchorNode) {
|
|
275
|
+
const range = document.createRange();
|
|
276
|
+
range.setStart(triggerAnchorNode, triggerAnchorOffset);
|
|
277
|
+
range.setEnd(sel.focusNode, sel.focusOffset);
|
|
278
|
+
range.deleteContents();
|
|
279
|
+
sel.removeAllRanges();
|
|
280
|
+
sel.addRange(range);
|
|
281
|
+
}
|
|
282
|
+
hideSuggestions();
|
|
283
|
+
insertTokenAtCaret(token);
|
|
284
|
+
}
|
|
285
|
+
function getTriggerQuery() {
|
|
286
|
+
if (!activeTrigger || !triggerAnchorNode)
|
|
287
|
+
return null;
|
|
288
|
+
const sel = window.getSelection();
|
|
289
|
+
if (!sel || sel.rangeCount === 0)
|
|
290
|
+
return null;
|
|
291
|
+
// Build text from anchor to current caret
|
|
292
|
+
try {
|
|
293
|
+
const range = document.createRange();
|
|
294
|
+
range.setStart(triggerAnchorNode, triggerAnchorOffset);
|
|
295
|
+
range.setEnd(sel.focusNode, sel.focusOffset);
|
|
296
|
+
const text = range.toString();
|
|
297
|
+
// Should start with the trigger char
|
|
298
|
+
if (!text.startsWith(activeTrigger.char)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return text.slice(activeTrigger.char.length);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function runSuggest(query) {
|
|
308
|
+
if (!activeTrigger)
|
|
309
|
+
return;
|
|
310
|
+
suggestGeneration++;
|
|
311
|
+
const gen = suggestGeneration;
|
|
312
|
+
const trigger = activeTrigger;
|
|
313
|
+
try {
|
|
314
|
+
const results = await trigger.suggest(query);
|
|
315
|
+
if (gen !== suggestGeneration || activeTrigger !== trigger)
|
|
316
|
+
return;
|
|
317
|
+
showSuggestions(results);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
if (gen === suggestGeneration)
|
|
321
|
+
hideSuggestions();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Debounce for suggest calls
|
|
325
|
+
let suggestTimer = null;
|
|
326
|
+
function debounceSuggest(query) {
|
|
327
|
+
if (suggestTimer)
|
|
328
|
+
clearTimeout(suggestTimer);
|
|
329
|
+
suggestTimer = setTimeout(() => runSuggest(query), 120);
|
|
330
|
+
}
|
|
331
|
+
// ── Input events ──
|
|
332
|
+
editor.addEventListener('input', () => {
|
|
333
|
+
fireInput();
|
|
334
|
+
if (activeTrigger) {
|
|
335
|
+
const query = getTriggerQuery();
|
|
336
|
+
if (query === null) {
|
|
337
|
+
hideSuggestions();
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
debounceSuggest(query);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
editor.addEventListener('keydown', (e) => {
|
|
345
|
+
// Suggestion navigation
|
|
346
|
+
if (suggestionsEl.style.display !== 'none' && suggestionItems.length > 0) {
|
|
347
|
+
if (e.key === 'ArrowDown') {
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
activeIndex = (activeIndex + 1) % suggestionItems.length;
|
|
350
|
+
updateActiveRow();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (e.key === 'ArrowUp') {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
activeIndex = (activeIndex - 1 + suggestionItems.length) % suggestionItems.length;
|
|
356
|
+
updateActiveRow();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
pickSuggestion(activeIndex);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (e.key === 'Escape') {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
hideSuggestions();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Submit
|
|
371
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
372
|
+
if (options.multiline === false || !options.multiline) {
|
|
373
|
+
// Single-line or default: Enter submits
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
const parts = parseParts();
|
|
376
|
+
if (parts.length > 0) {
|
|
377
|
+
options.onSubmit?.(parts);
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (e.key === 'Enter' && !e.shiftKey && options.multiline) {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
const parts = parseParts();
|
|
385
|
+
if (parts.length > 0) {
|
|
386
|
+
options.onSubmit?.(parts);
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// Trigger detection — check the character being typed, not e.key
|
|
391
|
+
// (e.key can differ from the actual inserted char on some layouts)
|
|
392
|
+
if (triggerChars.has(e.key) && !activeTrigger) {
|
|
393
|
+
const trigger = triggers.find(t => t.char === e.key);
|
|
394
|
+
// Use setTimeout(0) instead of rAF — rAF can fire before the
|
|
395
|
+
// input event, causing the anchor to be captured too early
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
const sel = window.getSelection();
|
|
398
|
+
if (!sel || sel.rangeCount === 0)
|
|
399
|
+
return;
|
|
400
|
+
activeTrigger = trigger;
|
|
401
|
+
triggerAnchorNode = sel.focusNode;
|
|
402
|
+
triggerAnchorOffset = Math.max(0, sel.focusOffset - trigger.char.length);
|
|
403
|
+
}, 0);
|
|
404
|
+
}
|
|
405
|
+
// Backspace near token — handle deletion of whole token
|
|
406
|
+
if (e.key === 'Backspace') {
|
|
407
|
+
const sel = window.getSelection();
|
|
408
|
+
if (sel && sel.isCollapsed && sel.rangeCount > 0) {
|
|
409
|
+
const node = sel.focusNode;
|
|
410
|
+
const offset = sel.focusOffset;
|
|
411
|
+
// If caret is in a zero-width space right after a token
|
|
412
|
+
if (node && node.nodeType === Node.TEXT_NODE && offset === 0) {
|
|
413
|
+
const prev = node.previousSibling;
|
|
414
|
+
if (prev && prev instanceof HTMLElement && prev.classList.contains('tokenized-token')) {
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
if (node.textContent === '\u200B')
|
|
417
|
+
node.remove();
|
|
418
|
+
prev.remove();
|
|
419
|
+
fireInput();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// If caret is at offset 1 in a zwsp text node (common position)
|
|
424
|
+
if (node && node.nodeType === Node.TEXT_NODE && node.textContent === '\u200B' && offset <= 1) {
|
|
425
|
+
const prev = node.previousSibling;
|
|
426
|
+
if (prev && prev instanceof HTMLElement && prev.classList.contains('tokenized-token')) {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
node.remove();
|
|
429
|
+
prev.remove();
|
|
430
|
+
fireInput();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
editor.addEventListener('blur', () => {
|
|
438
|
+
// Delay to allow mousedown on suggestions
|
|
439
|
+
setTimeout(() => {
|
|
440
|
+
if (!suggestionsEl.matches(':hover')) {
|
|
441
|
+
hideSuggestions();
|
|
442
|
+
}
|
|
443
|
+
}, 150);
|
|
444
|
+
});
|
|
445
|
+
// Prevent dropping content into tokens
|
|
446
|
+
editor.addEventListener('drop', (e) => {
|
|
447
|
+
const target = e.target;
|
|
448
|
+
if (target.closest?.('.tokenized-token')) {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
// Clean pasted content — strip HTML, keep plain text
|
|
453
|
+
editor.addEventListener('paste', (e) => {
|
|
454
|
+
e.preventDefault();
|
|
455
|
+
const text = e.clipboardData?.getData('text/plain') ?? '';
|
|
456
|
+
document.execCommand('insertText', false, text);
|
|
457
|
+
});
|
|
458
|
+
// ── Public API ──
|
|
459
|
+
return {
|
|
460
|
+
getParts() {
|
|
461
|
+
return parseParts();
|
|
462
|
+
},
|
|
463
|
+
getText() {
|
|
464
|
+
return serializeText(parseParts());
|
|
465
|
+
},
|
|
466
|
+
setParts(parts) {
|
|
467
|
+
editor.innerHTML = '';
|
|
468
|
+
for (const part of parts) {
|
|
469
|
+
if (part.kind === 'text') {
|
|
470
|
+
const lines = part.text.split('\n');
|
|
471
|
+
for (let i = 0; i < lines.length; i++) {
|
|
472
|
+
if (lines[i].length > 0) {
|
|
473
|
+
editor.appendChild(document.createTextNode(lines[i]));
|
|
474
|
+
}
|
|
475
|
+
if (i < lines.length - 1) {
|
|
476
|
+
editor.appendChild(document.createElement('br'));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
editor.appendChild(buildTokenSpan(part.token));
|
|
482
|
+
editor.appendChild(document.createTextNode('\u200B'));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
insertToken(token) {
|
|
487
|
+
insertTokenAtCaret(token);
|
|
488
|
+
},
|
|
489
|
+
clear() {
|
|
490
|
+
editor.innerHTML = '';
|
|
491
|
+
hideSuggestions();
|
|
492
|
+
fireInput();
|
|
493
|
+
},
|
|
494
|
+
focus() {
|
|
495
|
+
editor.focus();
|
|
496
|
+
// Drop the caret at the very end so subsequent typing / inserts land
|
|
497
|
+
// inside the editor.
|
|
498
|
+
const sel = window.getSelection();
|
|
499
|
+
if (!sel)
|
|
500
|
+
return;
|
|
501
|
+
const range = document.createRange();
|
|
502
|
+
range.selectNodeContents(editor);
|
|
503
|
+
range.collapse(false);
|
|
504
|
+
sel.removeAllRanges();
|
|
505
|
+
sel.addRange(range);
|
|
506
|
+
},
|
|
507
|
+
getElement() {
|
|
508
|
+
return root;
|
|
509
|
+
},
|
|
510
|
+
setPlaceholder(text) {
|
|
511
|
+
editor.dataset.placeholder = text;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
// ─── Ready-made triggers ─────────────────────────────────────────────────────
|
|
516
|
+
/**
|
|
517
|
+
* `@` mentions. By default it searches every Fauna user; pass `people` (a
|
|
518
|
+
* group's members, say) to pick only from that list, shown in full as soon as
|
|
519
|
+
* `@` is typed.
|
|
520
|
+
*/
|
|
521
|
+
export function createUserTrigger(credentials, options = {}) {
|
|
522
|
+
const domain = credentials.email.includes('@')
|
|
523
|
+
? credentials.email.split('@')[1]
|
|
524
|
+
: '';
|
|
525
|
+
const suggestion = (p) => {
|
|
526
|
+
const name = p.displayName ?? p.email.split('@')[0];
|
|
527
|
+
const avatar = p.icon
|
|
528
|
+
? p.icon
|
|
529
|
+
: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=0B1020&color=F3F6FF&size=64`;
|
|
530
|
+
return {
|
|
531
|
+
label: name,
|
|
532
|
+
subtitle: p.email,
|
|
533
|
+
image: avatar,
|
|
534
|
+
data: { email: p.email, displayName: p.displayName, icon: p.icon }
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
return {
|
|
538
|
+
char: '@',
|
|
539
|
+
type: 'user',
|
|
540
|
+
async suggest(query) {
|
|
541
|
+
const people = options.people;
|
|
542
|
+
if (people) {
|
|
543
|
+
const needle = query.trim().toLowerCase();
|
|
544
|
+
return people
|
|
545
|
+
.filter((p) => p.email !== credentials.email)
|
|
546
|
+
.filter((p) => !needle
|
|
547
|
+
|| (p.displayName ?? '').toLowerCase().includes(needle)
|
|
548
|
+
|| p.email.toLowerCase().includes(needle))
|
|
549
|
+
.slice(0, 12)
|
|
550
|
+
.map(suggestion);
|
|
551
|
+
}
|
|
552
|
+
if (!query.trim())
|
|
553
|
+
return [];
|
|
554
|
+
try {
|
|
555
|
+
// If query has no @, append caller's domain to email search
|
|
556
|
+
// so same-domain users rank higher server-side
|
|
557
|
+
const emailQuery = query.includes('@') ? query : (query + '@' + domain);
|
|
558
|
+
const results = await Fauna.Authentification.query(credentials, {
|
|
559
|
+
displayName: query,
|
|
560
|
+
email: emailQuery
|
|
561
|
+
});
|
|
562
|
+
return results.slice(0, 12).map(suggestion);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
onPick(s) {
|
|
569
|
+
return {
|
|
570
|
+
type: 'user',
|
|
571
|
+
value: s.data?.email ?? s.label,
|
|
572
|
+
label: s.label,
|
|
573
|
+
icon: 'person'
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
export function createDriveTrigger(credentials) {
|
|
579
|
+
return {
|
|
580
|
+
char: '@',
|
|
581
|
+
type: 'drive',
|
|
582
|
+
async suggest(query) {
|
|
583
|
+
// Normalize query path
|
|
584
|
+
const path = query.replace(/\\/g, '/');
|
|
585
|
+
try {
|
|
586
|
+
// List the parent directory
|
|
587
|
+
const parentDir = path.includes('/')
|
|
588
|
+
? path.slice(0, path.lastIndexOf('/') + 1)
|
|
589
|
+
: '';
|
|
590
|
+
const prefix = path.slice(parentDir.length).toLowerCase();
|
|
591
|
+
const entries = await Fauna.Drive.list(credentials, parentDir);
|
|
592
|
+
return entries
|
|
593
|
+
.filter(entry => {
|
|
594
|
+
const name = entry.split('/').filter(Boolean).pop() ?? entry;
|
|
595
|
+
return name.toLowerCase().startsWith(prefix) || prefix === '';
|
|
596
|
+
})
|
|
597
|
+
.slice(0, 12)
|
|
598
|
+
.map(entry => {
|
|
599
|
+
const isDir = entry.endsWith('/');
|
|
600
|
+
const name = entry.split('/').filter(Boolean).pop() ?? entry;
|
|
601
|
+
return {
|
|
602
|
+
label: name,
|
|
603
|
+
subtitle: parentDir + entry,
|
|
604
|
+
icon: isDir ? 'folder' : 'description',
|
|
605
|
+
data: { path: parentDir + entry, isDir }
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
onPick(s) {
|
|
614
|
+
return {
|
|
615
|
+
type: 'drive',
|
|
616
|
+
value: s.data?.path ?? s.label,
|
|
617
|
+
label: s.label,
|
|
618
|
+
icon: s.icon ?? 'description'
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|