From 94069e7441100cedbb3246c5c590f2039e8df799 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 18 Feb 2024 22:41:20 +0900 Subject: [PATCH] feat: complete `:emoji:` to unicode emoji --- .../src/components/MkAutocomplete.vue | 31 +++++++++++++++---- packages/frontend/src/scripts/autocomplete.ts | 26 ++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 07747d3e5c3f..ea8005439def 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ hashtag }} -
    +
    1. @@ -67,10 +67,16 @@ export type CompleteInfo = { payload: string; query: string; }, + // `:emo` -> `:emoji:` or some unicode emoji emoji: { payload: string; query: string; }, + // like emoji but for `:emoji:` -> unicode emoji + emojiComplete: { + payload: string; + query: string; + }, mfmTag: { payload: string; query: string; @@ -98,8 +104,7 @@ type EmojiDef = { const lib = emojilist.filter(x => x.category !== 'flags'); -const emojiDb = computed(() => { - //#region Unicode Emoji +const unicodeEmojiDB = computed(() => { const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({ @@ -122,6 +127,12 @@ const emojiDb = computed(() => { } unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length); + + return unicodeEmojiDB; +}); + +const emojiDb = computed(() => { + //#region Unicode Emoji //#endregion //#region Custom Emoji @@ -149,7 +160,7 @@ const emojiDb = computed(() => { customEmojiDB.sort((a, b) => a.name.length - b.name.length); //#endregion - return markRaw([...customEmojiDB, ...unicodeEmojiDB]); + return markRaw([...customEmojiDB, ...unicodeEmojiDB.value]); }); export default { @@ -171,7 +182,7 @@ type PropsType = { //const props = defineProps>(); // ↑と同じだけど↓にしないとdiscriminated unionにならない。 // https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions -const props = defineProps | PropsType<'hashtag'> | PropsType<'emoji'> | PropsType<'mfmTag'> | PropsType<'mfmParam'>>(); +const props = defineProps | PropsType<'hashtag'> | PropsType<'emoji'> | PropsType<'emojiComplete'> | PropsType<'mfmTag'> | PropsType<'mfmParam'>>(); const emit = defineEmits<{ (event: 'done', value: { type: T; value: CompleteInfo[T]['payload'] }): void; @@ -194,7 +205,7 @@ const zIndex = os.claimZIndex('high'); function complete(type: T, value: CompleteInfo[T]['payload']) { emit('done', { type, value }); emit('closed'); - if (type === 'emoji') { + if (type === 'emoji' || type === 'emojiComplete') { let recents = defaultStore.state.recentlyUsedEmojis; recents = recents.filter((emoji: any) => emoji !== value); recents.unshift(value); @@ -281,6 +292,14 @@ function exec() { } emojis.value = emojiAutoComplete(props.q, emojiDb.value); + } else if (props.type === 'emojiComplete') { + if (!props.q || props.q === '') { + // 最近使った絵文字をサジェスト + emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => unicodeEmojiDB.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; + return; + } + + emojis.value = emojiAutoComplete(props.q, unicodeEmojiDB.value); } else if (props.type === 'mfmTag') { if (!props.q || props.q === '') { mfmTags.value = MFM_TAGS; diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index 3a4160a3d7ba..577f8c6ecd9c 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -99,6 +99,8 @@ export class Autocomplete { const isMfmParam = mfmParamIndex !== -1 && text.split(/\$\[[a-zA-Z]+/).pop()?.includes('.'); const isMfmTag = mfmTagIndex !== -1 && !isMfmParam; const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); + // :ok:などを🆗にするたいおぷ + const isEmojiCompleteToUnicode = !isEmoji && emojiIndex === text.length - 1; let opened = false; @@ -129,6 +131,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(' ')) { @@ -272,6 +282,22 @@ export class Autocomplete { // 挿入 this.text = trimmedBefore + value + after; + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + 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();