diff options
Diffstat (limited to 'packages/frontend/src/scripts/autocomplete.ts')
| -rw-r--r-- | packages/frontend/src/scripts/autocomplete.ts | 47 |
1 files changed, 39 insertions, 8 deletions
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index 2a9a42ace5..9fc8f7843e 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,18 +8,18 @@ import getCaretCoordinates from 'textarea-caret'; import { toASCII } from 'punycode/'; import { popup } from '@/os.js'; -export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag'; +export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam'; export class Autocomplete { private suggestion: { x: Ref<number>; y: Ref<number>; - q: Ref<string | null>; + q: Ref<any>; close: () => void; } | null; private textarea: HTMLInputElement | HTMLTextAreaElement; private currentType: string; - private textRef: Ref<string>; + private textRef: Ref<string | number | null>; private opening: boolean; private onlyType: SuggestionType[]; @@ -38,7 +38,7 @@ export class Autocomplete { /** * 対象のテキストエリアを与えてインスタンスを初期化します。 */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) { + constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string | number | null>, onlyType?: SuggestionType[]) { //#region BIND this.onInput = this.onInput.bind(this); this.complete = this.complete.bind(this); @@ -49,7 +49,7 @@ export class Autocomplete { this.textarea = textarea; this.textRef = textRef; this.opening = false; - this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag']; + this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam']; this.attach(); } @@ -80,6 +80,7 @@ export class Autocomplete { const hashtagIndex = text.lastIndexOf('#'); const emojiIndex = text.lastIndexOf(':'); const mfmTagIndex = text.lastIndexOf('$'); + const mfmParamIndex = text.lastIndexOf('.'); const max = Math.max( mentionIndex, @@ -92,9 +93,12 @@ export class Autocomplete { return; } + const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop(); + const isMention = mentionIndex !== -1; const isHashtag = hashtagIndex !== -1; - const isMfmTag = mfmTagIndex !== -1; + const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' '); + const isMfmTag = mfmTagIndex !== -1 && !isMfmParam; const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); let opened = false; @@ -134,6 +138,17 @@ export class Autocomplete { } } + if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) { + const mfmParam = text.substring(mfmParamIndex + 1); + if (!mfmParam.includes(' ')) { + this.open('mfmParam', { + tag: text.substring(mfmTagIndex + 2, mfmParamIndex), + params: mfmParam.split(','), + }); + opened = true; + } + } + if (!opened) { this.close(); } @@ -142,7 +157,7 @@ export class Autocomplete { /** * サジェストを提示します。 */ - private async open(type: string, q: string | null) { + private async open(type: string, q: any) { if (type !== this.currentType) { this.close(); } @@ -280,6 +295,22 @@ export class Autocomplete { const pos = trimmedBefore.length + (value.length + 3); this.textarea.setSelectionRange(pos, pos); }); + } else if (type === 'mfmParam') { + const source = this.text; + + const before = source.substring(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('.')); + const after = source.substring(caret); + + // 挿入 + this.text = `${trimmedBefore}.${value}${after}`; + + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (value.length + 1); + this.textarea.setSelectionRange(pos, pos); + }); } } } |