fix mention problem
This commit is contained in:
parent
5a4b191536
commit
fa4d3046d0
|
|
@ -71,30 +71,36 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertText(text: string) {
|
function insertTextAt(from: number, to: number, text: string) {
|
||||||
const before = node.value.slice(0, mentionStart);
|
const before = node.value.slice(0, from);
|
||||||
const after = node.value.slice(node.selectionStart ?? 0);
|
const after = node.value.slice(to);
|
||||||
node.value = before + text + ' ' + after;
|
node.value = before + text + ' ' + after;
|
||||||
const newPos = mentionStart + text.length + 1;
|
const newPos = from + text.length + 1;
|
||||||
node.selectionStart = newPos;
|
node.selectionStart = newPos;
|
||||||
node.selectionEnd = newPos;
|
node.selectionEnd = newPos;
|
||||||
node.dispatchEvent(new Event('input', { bubbles: true }));
|
node.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectItem(index: number) {
|
async function selectItem(index: number) {
|
||||||
console.log('[mention-action] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'mentionStart:', mentionStart, 'value:', JSON.stringify(node.value));
|
// Save positions before any async work (hide/blur could reset mentionStart)
|
||||||
|
const savedFrom = mentionStart;
|
||||||
|
const savedTo = node.selectionStart ?? node.value.length;
|
||||||
|
console.log('[mention-action] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'from:', savedFrom, 'to:', savedTo);
|
||||||
suppressInput = true;
|
suppressInput = true;
|
||||||
try {
|
try {
|
||||||
if (index < items.length) {
|
if (index < items.length) {
|
||||||
console.log('[mention-action] inserting:', items[index].insertText);
|
const text = items[index].insertText;
|
||||||
insertText(items[index].insertText);
|
console.log('[mention-action] inserting:', text);
|
||||||
|
insertTextAt(savedFrom, savedTo, text);
|
||||||
console.log('[mention-action] after insert, value:', JSON.stringify(node.value));
|
console.log('[mention-action] after insert, value:', JSON.stringify(node.value));
|
||||||
} else {
|
} else {
|
||||||
const opt = createOptions[index - items.length];
|
const opt = createOptions[index - items.length];
|
||||||
if (!opt) return;
|
if (!opt) return;
|
||||||
await createMentionContext(opt);
|
await createMentionContext(opt);
|
||||||
const tag = opt.type === 'company' ? quoteMention('@F:', opt.query) : opt.type === 'person' ? quoteMention('@', opt.query) : quoteMention('@P:', opt.query);
|
const tag = opt.type === 'company' ? quoteMention('@F:', opt.query) : opt.type === 'person' ? quoteMention('@', opt.query) : quoteMention('@P:', opt.query);
|
||||||
insertText(tag);
|
console.log('[mention-action] inserting after create:', tag);
|
||||||
|
insertTextAt(savedFrom, savedTo, tag);
|
||||||
|
console.log('[mention-action] after create insert, value:', JSON.stringify(node.value));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('mention selectItem error:', err);
|
console.error('mention selectItem error:', err);
|
||||||
|
|
@ -112,6 +118,15 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await fetchMentionItems(query);
|
const result = await fetchMentionItems(query);
|
||||||
|
|
||||||
|
// Re-check after async: state may have changed (e.g. selectItem ran)
|
||||||
|
if (suppressInput) return;
|
||||||
|
const queryNow = getQuery();
|
||||||
|
if (queryNow === null) {
|
||||||
|
if (active) hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
items = result.items;
|
items = result.items;
|
||||||
createOptions = result.createOptions;
|
createOptions = result.createOptions;
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
|
|
@ -144,6 +159,7 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
|
if (suppressInput) return;
|
||||||
setTimeout(hide, 150);
|
setTimeout(hide, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,13 +76,16 @@ export const TiptapMention = Extension.create({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectItem(index: number) {
|
async function selectItem(index: number) {
|
||||||
console.log('[tiptap-mention] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'mentionFrom:', mentionFrom);
|
// Save positions before any async work (hide could reset mentionFrom)
|
||||||
|
const savedFrom = mentionFrom;
|
||||||
|
const savedTo = editor.state.selection.from;
|
||||||
|
console.log('[tiptap-mention] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'from:', savedFrom, 'to:', savedTo);
|
||||||
try {
|
try {
|
||||||
if (index < items.length) {
|
if (index < items.length) {
|
||||||
const text = items[index].insertText;
|
const text = items[index].insertText;
|
||||||
console.log('[tiptap-mention] inserting:', text, 'deleteRange:', mentionFrom, '->', editor.state.selection.from);
|
console.log('[tiptap-mention] inserting:', text);
|
||||||
editor.chain().focus()
|
editor.chain().focus()
|
||||||
.deleteRange({ from: mentionFrom, to: editor.state.selection.from })
|
.deleteRange({ from: savedFrom, to: savedTo })
|
||||||
.insertContent(text + ' ')
|
.insertContent(text + ' ')
|
||||||
.run();
|
.run();
|
||||||
console.log('[tiptap-mention] after insert, content:', editor.storage.markdown.getMarkdown().slice(0, 100));
|
console.log('[tiptap-mention] after insert, content:', editor.storage.markdown.getMarkdown().slice(0, 100));
|
||||||
|
|
@ -91,8 +94,9 @@ export const TiptapMention = Extension.create({
|
||||||
if (!opt) return;
|
if (!opt) return;
|
||||||
await createMentionContext(opt);
|
await createMentionContext(opt);
|
||||||
const tag = opt.type === 'company' ? quoteMention('@F:', opt.query) : opt.type === 'person' ? quoteMention('@', opt.query) : quoteMention('@P:', opt.query);
|
const tag = opt.type === 'company' ? quoteMention('@F:', opt.query) : opt.type === 'person' ? quoteMention('@', opt.query) : quoteMention('@P:', opt.query);
|
||||||
|
console.log('[tiptap-mention] inserting after create:', tag);
|
||||||
editor.chain().focus()
|
editor.chain().focus()
|
||||||
.deleteRange({ from: mentionFrom, to: editor.state.selection.from })
|
.deleteRange({ from: savedFrom, to: savedTo })
|
||||||
.insertContent(tag + ' ')
|
.insertContent(tag + ' ')
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +123,7 @@ export const TiptapMention = Extension.create({
|
||||||
if (i === 0 || /\s/.test(textBefore[i - 1])) {
|
if (i === 0 || /\s/.test(textBefore[i - 1])) {
|
||||||
const query = textBefore.slice(i + 1);
|
const query = textBefore.slice(i + 1);
|
||||||
// Skip @P: and @F: prefixes (handled differently)
|
// Skip @P: and @F: prefixes (handled differently)
|
||||||
if (query.startsWith('P:') || query.startsWith('F:')) return null;
|
if (/^[Pp]:|^[Ff]:/.test(query)) return null;
|
||||||
const docFrom = from - (textBefore.length - i);
|
const docFrom = from - (textBefore.length - i);
|
||||||
return { active: true, query, from: docFrom };
|
return { active: true, query, from: docFrom };
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +143,13 @@ export const TiptapMention = Extension.create({
|
||||||
|
|
||||||
mentionFrom = mentionState.from;
|
mentionFrom = mentionState.from;
|
||||||
const result = await fetchMentionItems(mentionState.query);
|
const result = await fetchMentionItems(mentionState.query);
|
||||||
|
|
||||||
|
// Re-check after async: state may have changed (e.g. selectItem ran)
|
||||||
|
if (!getQueryFromState(view)) {
|
||||||
|
if (active) hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
items = result.items;
|
items = result.items;
|
||||||
createOptions = result.createOptions;
|
createOptions = result.createOptions;
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function extractAssignments(text: string): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractProjects(text: string): string[] {
|
export function extractProjects(text: string): string[] {
|
||||||
const regex = /@P:(?:"([^"]+)"|([\w-]+))/g;
|
const regex = /@[Pp]:(?:"([^"]+)"|([\w-]+))/g;
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
|
@ -19,7 +19,7 @@ export function extractProjects(text: string): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractMentions(text: string): string[] {
|
export function extractMentions(text: string): string[] {
|
||||||
const regex = /@(?!P:|F:)(?:"([^"]+)"|([\w-]+))/g;
|
const regex = /@(?![Pp]:|[Ff]:)(?:"([^"]+)"|([\w-]+))/g;
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
|
@ -29,7 +29,7 @@ export function extractMentions(text: string): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractCompanies(text: string): string[] {
|
export function extractCompanies(text: string): string[] {
|
||||||
const regex = /@F:(?:"([^"]+)"|([\w-]+))/g;
|
const regex = /@[Ff]:(?:"([^"]+)"|([\w-]+))/g;
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ const assignmentExt: TokenizerExtension & RendererExtension = {
|
||||||
const projectRefExt: TokenizerExtension & RendererExtension = {
|
const projectRefExt: TokenizerExtension & RendererExtension = {
|
||||||
name: 'projectRef',
|
name: 'projectRef',
|
||||||
level: 'inline',
|
level: 'inline',
|
||||||
start(src: string) { return src.indexOf('@P:'); },
|
start(src: string) { return src.search(/@[Pp]:/); },
|
||||||
tokenizer(src: string) {
|
tokenizer(src: string) {
|
||||||
const match = /^@P:"(.+?)"/.exec(src) ?? /^@P:([\w-]+)/.exec(src);
|
const match = /^@[Pp]:"(.+?)"/.exec(src) ?? /^@[Pp]:([\w-]+)/.exec(src);
|
||||||
if (match) {
|
if (match) {
|
||||||
return { type: 'projectRef', raw: match[0], project: match[1] };
|
return { type: 'projectRef', raw: match[0], project: match[1] };
|
||||||
}
|
}
|
||||||
|
|
@ -37,9 +37,9 @@ const projectRefExt: TokenizerExtension & RendererExtension = {
|
||||||
const companyRefExt: TokenizerExtension & RendererExtension = {
|
const companyRefExt: TokenizerExtension & RendererExtension = {
|
||||||
name: 'companyRef',
|
name: 'companyRef',
|
||||||
level: 'inline',
|
level: 'inline',
|
||||||
start(src: string) { return src.indexOf('@F:'); },
|
start(src: string) { return src.search(/@[Ff]:/); },
|
||||||
tokenizer(src: string) {
|
tokenizer(src: string) {
|
||||||
const match = /^@F:"(.+?)"/.exec(src) ?? /^@F:([\w-]+)/.exec(src);
|
const match = /^@[Ff]:"(.+?)"/.exec(src) ?? /^@[Ff]:([\w-]+)/.exec(src);
|
||||||
if (match) {
|
if (match) {
|
||||||
return { type: 'companyRef', raw: match[0], company: match[1] };
|
return { type: 'companyRef', raw: match[0], company: match[1] };
|
||||||
}
|
}
|
||||||
|
|
@ -55,13 +55,13 @@ const personMentionExt: TokenizerExtension & RendererExtension = {
|
||||||
start(src: string) {
|
start(src: string) {
|
||||||
const idx = src.indexOf('@');
|
const idx = src.indexOf('@');
|
||||||
if (idx === -1) return -1;
|
if (idx === -1) return -1;
|
||||||
// Skip if followed by P: or F: (handled by other extensions)
|
// Skip if followed by P:/p: or F:/f: (handled by other extensions)
|
||||||
const after = src.slice(idx + 1);
|
const after = src.slice(idx + 1);
|
||||||
if (after.startsWith('P:') || after.startsWith('F:')) return src.indexOf('@', idx + 1);
|
if (/^[Pp]:|^[Ff]:/.test(after)) return src.indexOf('@', idx + 1);
|
||||||
return idx;
|
return idx;
|
||||||
},
|
},
|
||||||
tokenizer(src: string) {
|
tokenizer(src: string) {
|
||||||
const match = /^@(?!P:|F:)"(.+?)"/.exec(src) ?? /^@(?!P:|F:)([\w-]+)/.exec(src);
|
const match = /^@(?![Pp]:|[Ff]:)"(.+?)"/.exec(src) ?? /^@(?![Pp]:|[Ff]:)([\w-]+)/.exec(src);
|
||||||
if (match) {
|
if (match) {
|
||||||
return { type: 'personMention', raw: match[0], person: match[1] };
|
return { type: 'personMention', raw: match[0], person: match[1] };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue