151 lines
4.9 KiB
TypeScript
151 lines
4.9 KiB
TypeScript
import { db } from '$lib/db/schema';
|
||
import { upsertContext } from '$lib/db/repositories';
|
||
import type { AgendaContext } from '@ka-note/shared';
|
||
|
||
export interface MentionItem {
|
||
context: AgendaContext;
|
||
mentionName: string;
|
||
icon: string;
|
||
insertText: string;
|
||
}
|
||
|
||
export interface CreateOption {
|
||
type: 'person' | 'project' | 'company';
|
||
label: string;
|
||
query: string;
|
||
}
|
||
|
||
export function extractMentionName(ctx: AgendaContext): string {
|
||
return ctx.name.replace(/^(Person|Project|Firma)\s+/, '');
|
||
}
|
||
|
||
export function extractAbbreviation(ctx: AgendaContext): string | undefined {
|
||
if (ctx.type !== 'person') return undefined;
|
||
return (ctx.meta as { abbreviation?: string } | null)?.abbreviation || undefined;
|
||
}
|
||
|
||
export function quoteMention(prefix: string, name: string): string {
|
||
return name.includes(' ') ? `${prefix}"${name}"` : `${prefix}${name}`;
|
||
}
|
||
|
||
function buildMentionItem(ctx: AgendaContext): MentionItem {
|
||
const mentionName = extractMentionName(ctx);
|
||
const icon = ctx.type === 'company' ? '\u{1F3E2}' : ctx.type === 'person' ? '\u{1F464}' : '\u{1F4C1}';
|
||
const insertText = ctx.type === 'company'
|
||
? quoteMention('@F:', mentionName)
|
||
: ctx.type === 'person'
|
||
? quoteMention('@', mentionName)
|
||
: quoteMention('@P:', mentionName);
|
||
return { context: ctx, mentionName, icon, insertText };
|
||
}
|
||
|
||
export async function fetchMentionItems(query: string, mode: '@' | '->' = '@'): Promise<{ items: MentionItem[]; createOptions: CreateOption[] }> {
|
||
const all = await db.contexts
|
||
.filter(c => !c.deletedAt && !c.archivedAt && (c.type === 'person' || c.type === 'project' || c.type === 'company'))
|
||
.toArray();
|
||
|
||
const q = query.replace(/_/g, ' ').toLowerCase();
|
||
|
||
if (mode === '->') {
|
||
// Arrow mode: only persons with an abbreviation, match against abbreviation OR full name
|
||
const items = all
|
||
.filter(c => c.type === 'person')
|
||
.flatMap(c => {
|
||
const abbr = extractAbbreviation(c);
|
||
if (!abbr) return [];
|
||
const fullName = extractMentionName(c).toLowerCase();
|
||
if (!abbr.toLowerCase().includes(q) && !fullName.includes(q)) return [];
|
||
return [{
|
||
context: c,
|
||
mentionName: `${abbr} – ${extractMentionName(c)}`,
|
||
icon: '\u{1F464}',
|
||
insertText: `-> ${abbr}`,
|
||
} as MentionItem];
|
||
})
|
||
.slice(0, 8);
|
||
return { items, createOptions: [] };
|
||
}
|
||
|
||
const items = all
|
||
.map(buildMentionItem)
|
||
.filter(m => m.mentionName.toLowerCase().includes(q))
|
||
.slice(0, 8);
|
||
|
||
const exactMatch = items.some(m => m.mentionName.toLowerCase() === q);
|
||
const createOptions: CreateOption[] = [];
|
||
if (query.length > 0 && !exactMatch) {
|
||
const displayName = query.replace(/_/g, ' ');
|
||
createOptions.push(
|
||
{ type: 'person', label: `+ Person "${displayName}" anlegen`, query },
|
||
{ type: 'project', label: `+ Projekt "${displayName}" anlegen`, query },
|
||
{ type: 'company', label: `+ Firma "${displayName}" anlegen`, query }
|
||
);
|
||
}
|
||
|
||
return { items, createOptions };
|
||
}
|
||
|
||
export async function createMentionContext(opt: CreateOption): Promise<string> {
|
||
const name = opt.query.replace(/_/g, ' ');
|
||
const slug = name.toLowerCase().replace(/\s+/g, '-');
|
||
const id = opt.type === 'company' ? `f-${slug}` : opt.type === 'person' ? `u-${slug}` : `p-${slug}`;
|
||
const contextName = opt.type === 'company' ? `Firma ${name}` : opt.type === 'person' ? `Person ${name}` : `Project ${name}`;
|
||
|
||
const meta = opt.type === 'company'
|
||
? { website: '', address: '' }
|
||
: opt.type === 'person'
|
||
? { fullName: '', email: '', phone: '', duSince: '' }
|
||
: { status: '', owner: '', links: '' };
|
||
|
||
await upsertContext({
|
||
id,
|
||
name: contextName,
|
||
type: opt.type,
|
||
sortOrder: 99,
|
||
meta
|
||
});
|
||
|
||
return id;
|
||
}
|
||
|
||
export function renderDropdown(
|
||
container: HTMLDivElement,
|
||
items: MentionItem[],
|
||
createOptions: CreateOption[],
|
||
selectedIndex: number,
|
||
onSelect: (index: number) => void,
|
||
onHover: (index: number) => void
|
||
): void {
|
||
container.innerHTML = '';
|
||
|
||
items.forEach((item, i) => {
|
||
const row = document.createElement('div');
|
||
row.className = 'mention-item' + (i === selectedIndex ? ' mention-item-active' : '');
|
||
row.textContent = `${item.icon} ${item.mentionName}`;
|
||
row.classList.add(item.context.type === 'company' ? 'mention-company' : item.context.type === 'person' ? 'mention-person' : 'mention-project');
|
||
row.addEventListener('pointerdown', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('[mention] pointerdown item', i, item.mentionName);
|
||
onSelect(i);
|
||
});
|
||
row.addEventListener('mouseenter', () => onHover(i));
|
||
container.appendChild(row);
|
||
});
|
||
|
||
createOptions.forEach((opt, i) => {
|
||
const idx = items.length + i;
|
||
const row = document.createElement('div');
|
||
row.className = 'mention-item mention-create' + (idx === selectedIndex ? ' mention-item-active' : '');
|
||
row.textContent = opt.label;
|
||
row.addEventListener('pointerdown', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('[mention] pointerdown create', idx, opt.label);
|
||
onSelect(idx);
|
||
});
|
||
row.addEventListener('mouseenter', () => onHover(idx));
|
||
container.appendChild(row);
|
||
});
|
||
}
|