@editora/mentions
v1.0.1
Published
Mentions plugin for Editora Rich Text Editor
Downloads
19
Maintainers
Readme
@editora/mentions
[!IMPORTANT] Live Website: https://editora-ecosystem.netlify.app/
Storybook: https://editora-ecosystem-storybook.netlify.app/
@editora/mentions adds @mention autocomplete and atomic mention tokens to Editora.
Features
- Trigger mentions with
@while typing - Keyboard navigation (
ArrowUp/ArrowDown,Enter,Tab,Esc) - Multi-instance safe popup handling
- Atomic mention token deletion (Backspace/Delete)
- Dark/light theme styles
Install
npm install @editora/mentionsUsage
import { MentionPlugin } from '@editora/mentions';
const mentions = MentionPlugin({
triggerChars: ['@'],
minChars: 1,
maxSuggestions: 8,
items: [
{ id: 'john.doe', label: 'John Doe', meta: '[email protected]' },
{ id: 'sarah.lee', label: 'Sarah Lee', meta: '[email protected]' },
],
});Use it with @editora/react or web component setup by including it in your plugin list.
Web Component Configuration
mentions supports the same option shape in web components via pluginConfig.
<editora-editor id="wc-editor"></editora-editor>
<script>
const el = document.getElementById('wc-editor');
el.setConfig({
plugins: 'bold italic mentions history',
toolbar: { items: 'bold italic insertMention undo redo' },
pluginConfig: {
mentions: {
minChars: 2,
maxSuggestions: 10,
items: [
{ id: 'john.doe', label: 'John Doe', meta: '[email protected]' },
{ id: 'sarah.lee', label: 'Sarah Lee', meta: '[email protected]' },
],
},
},
});
</script>Notes:
- Use plugin name
mentions(aliasmentionalso works). - If
searchis provided, it overridesapiand staticitems.
Data Source Modes
Mentions can load suggestions from three sources:
items(static list, default)search(query, trigger)(custom logic, sync or async)api(declarative HTTP config)
Resolution precedence is:
search -> api -> items
If search is provided, it is used as the source of truth.
Search Callback (Optional)
Use search when you want full control in code (custom filtering, caching, auth, multi-source lookup).
import { MentionPlugin } from '@editora/mentions';
const mentions = MentionPlugin({
minChars: 1,
maxSuggestions: 8,
search: async (query, trigger) => {
if (trigger !== '@') return [];
if (!query.trim()) return [];
const res = await fetch(`/api/users/typeahead?q=${encodeURIComponent(query)}&limit=8`);
if (!res.ok) return [];
const json = await res.json();
return (json.items || []).map((u: any) => ({
id: String(u.id),
label: String(u.name),
meta: String(u.email || ''),
value: `@${u.username}`,
}));
},
});API Typeahead (Optional)
Static items remain the default. For database-backed typeahead, pass api:
import { MentionPlugin } from '@editora/mentions';
const mentions = MentionPlugin({
minChars: 2,
maxSuggestions: 10,
api: {
url: '/api/users/search',
method: 'GET',
headers: () => ({
Authorization: `Bearer ${localStorage.getItem('token') || ''}`,
}),
queryParam: 'q',
limitParam: 'limit',
staticParams: { status: 'active' },
debounceMs: 220,
timeoutMs: 8000,
responsePath: 'data.items',
mapItem: (raw) => ({
id: String((raw as any).id),
label: String((raw as any).name),
meta: String((raw as any).email || ''),
}),
onError: (error) => {
console.error('Mention API failed', error);
},
},
});For full control, use api.buildRequest(ctx) and/or api.transformResponse(response, ctx).
Usage Scenarios
Scenario 1: Static Team Directory
Best for small fixed lists (for example internal users loaded at boot).
MentionPlugin({
items: [
{ id: 'john.doe', label: 'John Doe', meta: 'Engineering' },
{ id: 'sarah.lee', label: 'Sarah Lee', meta: 'Product' },
],
});Scenario 2: Client-side Search Over Preloaded Data
Best when you preload a large list once, then filter in memory.
const users = await loadUsersOnce();
MentionPlugin({
search: (query) => {
const q = query.toLowerCase();
return users
.filter((u) => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q))
.slice(0, 10)
.map((u) => ({ id: u.id, label: u.name, meta: u.email }));
},
});Scenario 3: Server Typeahead via Search Callback
Best when you need custom auth, request cancellation strategy, or merged data from multiple APIs.
MentionPlugin({
search: async (query, trigger) => {
const [users, teams] = await Promise.all([
fetch(`/api/users?q=${encodeURIComponent(query)}&trigger=${trigger}`).then((r) => r.json()),
fetch(`/api/teams?q=${encodeURIComponent(query)}&trigger=${trigger}`).then((r) => r.json()),
]);
return [
...(users.items || []).map((u: any) => ({ id: `u:${u.id}`, label: u.name, meta: 'User' })),
...(teams.items || []).map((t: any) => ({ id: `t:${t.id}`, label: t.name, meta: 'Team' })),
].slice(0, 10);
},
});Scenario 4: Declarative API Configuration
Best when you want less custom code and standardized request behavior.
MentionPlugin({
api: {
url: '/api/users/search',
method: 'GET',
queryParam: 'q',
limitParam: 'limit',
responsePath: 'data.items',
mapItem: (raw) => ({
id: String((raw as any).id),
label: String((raw as any).name),
}),
},
});Scenario 5: Multiple Trigger Characters
Use different mention patterns in one editor (@ users, # tags, etc).
MentionPlugin({
triggerChars: ['@', '#'],
search: async (query, trigger) => {
if (trigger === '@') {
const r = await fetch(`/api/users?q=${encodeURIComponent(query)}`);
const j = await r.json();
return (j.items || []).map((u: any) => ({ id: u.id, label: u.name, meta: 'User' }));
}
const r = await fetch(`/api/tags?q=${encodeURIComponent(query)}`);
const j = await r.json();
return (j.items || []).map((t: any) => ({ id: t.id, label: t.name, meta: 'Tag' }));
},
});API Request Customization
MentionApiConfig supports full request control:
url: API endpoint.method: HTTP method (GET,POST,PUT,PATCH,HEAD, etc).headers: static headers object or(ctx) => headers.credentials,mode,cache: forwarded tofetch.queryParam,triggerParam,limitParam: query/body field names.staticParams: additional static parameters.body: static body or(ctx) => bodyfor non-GET/HEAD calls.buildRequest: full low-level override. If provided, it takes priority.responseType:json(default) ortext.responsePath: dotted path in response payload (for exampledata.items).mapItem: convert one raw API record into aMentionItem.transformResponse: convert whole payload intoMentionItem[].debounceMs: debounce query calls.timeoutMs: request timeout before abort.onError: centralized request error hook.
Example with full buildRequest control:
const mentions = MentionPlugin({
api: {
buildRequest: (ctx) => ({
url: "/api/v2/users/typeahead",
init: {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
search: ctx.query,
trigger: ctx.trigger,
pageSize: ctx.limit,
includeInactive: false,
}),
},
}),
transformResponse: (response) => {
const rows = (response as any)?.results ?? [];
return rows.map((row: any) => ({
id: String(row.userId),
label: String(row.displayName),
meta: String(row.email || ""),
value: `@${row.username}`,
}));
},
},
});Options
triggerChars?: string[]minChars?: numbermaxQueryLength?: numbermaxSuggestions?: numberitems?: MentionItem[]api?: MentionApiConfigsearch?: (query, trigger) => MentionItem[] | Promise<MentionItem[]>itemRenderer?: (item, query) => stringemptyStateText?: stringnoResultsText?: stringloadingText?: stringinsertSpaceAfterMention?: boolean
Security Note
If you provide itemRenderer, return trusted HTML only.
