summaryrefslogtreecommitdiff
path: root/packages/frontend/src/utility
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-04-24 14:23:45 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-04-24 14:23:45 -0400
commita4dd19fdd427a5adc8fa80871d1c742aa9708730 (patch)
tree3983a0e772922042043026b4af168e8dd3525fb2 /packages/frontend/src/utility
parentMerge branch 'develop' into merge/2025-03-24 (diff)
parentenhance(backend): DB note (userId) インデクス -> (userId, id) 複合イ... (diff)
downloadsharkey-a4dd19fdd427a5adc8fa80871d1c742aa9708730.tar.gz
sharkey-a4dd19fdd427a5adc8fa80871d1c742aa9708730.tar.bz2
sharkey-a4dd19fdd427a5adc8fa80871d1c742aa9708730.zip
merge upstream again
Diffstat (limited to 'packages/frontend/src/utility')
-rw-r--r--packages/frontend/src/utility/autocomplete.ts37
-rw-r--r--packages/frontend/src/utility/search-emoji.ts30
-rw-r--r--packages/frontend/src/utility/touch.ts2
3 files changed, 63 insertions, 6 deletions
diff --git a/packages/frontend/src/utility/autocomplete.ts b/packages/frontend/src/utility/autocomplete.ts
index 0276cc3c20..885c57caa9 100644
--- a/packages/frontend/src/utility/autocomplete.ts
+++ b/packages/frontend/src/utility/autocomplete.ts
@@ -7,6 +7,7 @@ import { nextTick, ref, defineAsyncComponent } from 'vue';
import getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode.js';
import type { Ref } from 'vue';
+import type { CompleteInfo } from '@/components/MkAutocomplete.vue';
import { popup } from '@/os.js';
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
@@ -19,7 +20,7 @@ export class Autocomplete {
close: () => void;
} | null;
private textarea: HTMLInputElement | HTMLTextAreaElement;
- private currentType: string;
+ private currentType: keyof CompleteInfo | undefined;
private textRef: Ref<string | number | null>;
private opening: boolean;
private onlyType: SuggestionType[];
@@ -74,7 +75,7 @@ export class Autocomplete {
* テキスト入力時
*/
private onInput() {
- const caretPos = this.textarea.selectionStart;
+ const caretPos = Number(this.textarea.selectionStart);
const text = this.text.substring(0, caretPos).split('\n').pop()!;
const mentionIndex = text.lastIndexOf('@');
@@ -101,6 +102,8 @@ export class Autocomplete {
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam.includes(' ');
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
const isEmoji = emojiIndex !== -1 && text.split(/:[\p{Letter}\p{Number}\p{Mark}_+-]+:/u).pop()!.includes(':');
+ // :ok:などを🆗にするたいおぷ
+ const isEmojiCompleteToUnicode = !isEmoji && emojiIndex === text.length - 1;
let opened = false;
@@ -137,6 +140,14 @@ export class Autocomplete {
}
}
+ if (isEmojiCompleteToUnicode && !opened && this.onlyType.includes('emoji')) {
+ const emoji = text.substring(text.lastIndexOf(':', text.length - 2) + 1, text.length - 1);
+ if (!emoji.includes(' ')) {
+ this.open('emojiComplete', emoji);
+ opened = true;
+ }
+ }
+
if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) {
const mfmTag = text.substring(mfmTagIndex + 1);
if (!mfmTag.includes(' ')) {
@@ -164,7 +175,7 @@ export class Autocomplete {
/**
* サジェストを提示します。
*/
- private async open(type: string, q: any) {
+ private async open<T extends keyof CompleteInfo>(type: T, q: CompleteInfo[T]['query']) {
if (type !== this.currentType) {
this.close();
}
@@ -231,10 +242,10 @@ export class Autocomplete {
/**
* オートコンプリートする
*/
- private complete({ type, value }) {
+ private complete<T extends keyof CompleteInfo>({ type, value }: { type: T; value: CompleteInfo[T]['payload'] }) {
this.close();
- const caret = this.textarea.selectionStart;
+ const caret = Number(this.textarea.selectionStart);
if (type === 'user') {
const source = this.text;
@@ -286,6 +297,22 @@ export class Autocomplete {
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
});
+ } else if (type === 'emojiComplete') {
+ const source = this.text;
+
+ const before = source.substring(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf(':', before.length - 2));
+ const after = source.substring(caret);
+
+ // 挿入
+ this.text = trimmedBefore + value + after;
+
+ // キャレットを戻す
+ nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + value.length;
+ this.textarea.setSelectionRange(pos, pos);
+ });
} else if (type === 'mfmTag') {
const source = this.text;
diff --git a/packages/frontend/src/utility/search-emoji.ts b/packages/frontend/src/utility/search-emoji.ts
index 4192a2df8f..8e6d657310 100644
--- a/packages/frontend/src/utility/search-emoji.ts
+++ b/packages/frontend/src/utility/search-emoji.ts
@@ -104,3 +104,33 @@ export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30)
.slice(0, max)
.map(it => it.emoji);
}
+
+export function searchEmojiExact(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
+ if (!query) {
+ return [];
+ }
+
+ const matched = new Map<string, EmojiScore>();
+ // 完全一致(エイリアスなし)
+ emojiDb.some(x => {
+ if (x.name === query && !x.aliasOf) {
+ matched.set(x.name, { emoji: x, score: query.length + 3 });
+ }
+ return matched.size === max;
+ });
+
+ // 完全一致(エイリアス込み)
+ if (matched.size < max) {
+ emojiDb.some(x => {
+ if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
+ matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
+ }
+ return matched.size === max;
+ });
+ }
+
+ return [...matched.values()]
+ .sort((x, y) => y.score - x.score)
+ .slice(0, max)
+ .map(it => it.emoji);
+}
diff --git a/packages/frontend/src/utility/touch.ts b/packages/frontend/src/utility/touch.ts
index adc2e4c093..361246b328 100644
--- a/packages/frontend/src/utility/touch.ts
+++ b/packages/frontend/src/utility/touch.ts
@@ -18,5 +18,5 @@ if (isTouchSupported && !isTouchUsing) {
}, { passive: true });
}
-/** (MkHorizontalSwipe) 横スワイプ中か? */
+/** (MkSwiper) 横スワイプ中か? */
export const isHorizontalSwipeSwiping = ref(false);