"packages/frontend/src/ui/universal.widgets.vue" did not exist on "1ade89be3581c5b206b152a655426181376355e4"
Newer
Older
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { nextTick, Ref, ref, defineAsyncComponent } from 'vue';
import getCaretCoordinates from 'textarea-caret';
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
export class Autocomplete {
private suggestion: {
x: Ref<number>;
y: Ref<number>;
private textarea: HTMLInputElement | HTMLTextAreaElement;
private textRef: Ref<string>;
private onlyType: SuggestionType[];
Kagami Sascha Rosylight
committed
// Use raw .value to get the latest value
// (Because v-model does not update while composition)
return this.textarea.value;
Kagami Sascha Rosylight
committed
// Use ref value to notify other watchers
// (Because .value setter never fires input/change events)
this.textRef.value = text;
constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) {
this.complete = this.complete.bind(this);
this.textRef = textRef;
this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
this.textarea.addEventListener('input', this.onInput);
}
/**
* このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
*/
this.textarea.removeEventListener('input', this.onInput);
this.close();
}
/**
const text = this.text.substring(0, caretPos).split('\n').pop()!;
const isMention = mentionIndex !== -1;
const isHashtag = hashtagIndex !== -1;
const isMfmTag = mfmTagIndex !== -1;
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
if (isMention && this.onlyType.includes('user')) {
const username = text.substring(mentionIndex + 1);
if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) {
} else if (username === '') {
this.open('user', null);
opened = true;
if (isHashtag && !opened && this.onlyType.includes('hashtag')) {
const hashtag = text.substring(hashtagIndex + 1);
if (isEmoji && !opened && this.onlyType.includes('emoji')) {
const emoji = text.substring(emojiIndex + 1);
if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
const mfmTag = text.substring(mfmTagIndex + 1);
this.open('mfmTag', mfmTag.replace('[', ''));
if (type !== this.currentType) {
if (this.opening) return;
this.opening = true;
//#region サジェストを表示すべき位置を計算
const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
const rect = this.textarea.getBoundingClientRect();
const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
const y = rect.top + caretPosition.top - this.textarea.scrollTop;
//#endregion
this.suggestion.x.value = x;
this.suggestion.y.value = y;
this.suggestion.q.value = q;
const _x = ref(x);
const _y = ref(y);
const _q = ref(q);
const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
textarea: this.textarea,
close: this.close,
type: type,
q: _q,
x: _x,
y: _y,
}, {
done: (res) => {
this.complete(res);
this.suggestion = {
q: _q,
x: _x,
y: _y,
close: () => dispose(),
};
const before = source.substring(0, caret);
const after = source.substring(caret);
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
this.text = `${trimmedBefore}@${acct} ${after}`;
} else if (type === 'hashtag') {
const before = source.substring(0, caret);
const after = source.substring(caret);
this.text = `${trimmedBefore}#${value} ${after}`;
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 2);
this.textarea.setSelectionRange(pos, pos);
});
} else if (type === 'emoji') {
const before = source.substring(0, caret);
const after = source.substring(caret);
} else if (type === 'mfmTag') {
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`;
// キャレットを戻す
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});