diff options
| author | Marie <Marie@kaifa.ch> | 2023-12-23 02:09:23 +0100 |
|---|---|---|
| committer | Marie <Marie@kaifa.ch> | 2023-12-23 02:09:23 +0100 |
| commit | 5db583a3eb61d50de14d875ebf7ecef20490e313 (patch) | |
| tree | 783dd43d2ac660c32e745a4485d499e9ddc43324 /packages/frontend/src/components/MkAutocomplete.vue | |
| parent | add: Custom MOTDs (diff) | |
| parent | Update CHANGELOG.md (diff) | |
| download | sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.gz sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.bz2 sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.zip | |
merge: upstream
Diffstat (limited to 'packages/frontend/src/components/MkAutocomplete.vue')
| -rw-r--r-- | packages/frontend/src/components/MkAutocomplete.vue | 111 |
1 files changed, 87 insertions, 24 deletions
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 9e92c4bb03..1f819cf601 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -242,29 +242,7 @@ function exec() { return; } - const matched: EmojiDef[] = []; - const max = 30; - - emojiDb.value.some(x => { - if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !x.aliasOf && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x); - return matched.length === max; - }); - - if (matched.length < max) { - emojiDb.value.some(x => { - if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x); - return matched.length === max; - }); - } - - if (matched.length < max) { - emojiDb.value.some(x => { - if (x.name.toLowerCase().includes(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x); - return matched.length === max; - }); - } - - emojis.value = matched; + emojis.value = emojiAutoComplete(props.q.toLowerCase(), emojiDb.value); } else if (props.type === 'mfmTag') { if (!props.q || props.q === '') { mfmTags.value = MFM_TAGS; @@ -275,6 +253,78 @@ function exec() { } } +type EmojiScore = { emoji: EmojiDef, score: number }; + +function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { + if (!query) { + return []; + } + + const matched = new Map<string, EmojiScore>(); + + // 前方一致(エイリアスなし) + emojiDb.some(x => { + if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) { + matched.set(x.name, { emoji: x, score: query.length + 1 }); + } + return matched.size === max; + }); + + // 前方一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + } + + // 部分一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); + } + return matched.size === max; + }); + } + + // 簡易あいまい検索(3文字以上) + if (matched.size < max && query.length > 3) { + const queryChars = [...query]; + const hitEmojis = new Map<string, EmojiScore>(); + + for (const x of emojiDb) { + // 文字列の位置を進めながら、クエリの文字を順番に探す + + let pos = 0; + let hit = 0; + for (const c of queryChars) { + pos = x.name.toLowerCase().indexOf(c, pos); + if (pos <= -1) break; + hit++; + } + + // 半分以上の文字が含まれていればヒットとする + if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { + hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); + } + } + + // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) + [...hitEmojis.values()] + .sort((x, y) => y.score - x.score) + .slice(0, 6) + .forEach(it => matched.set(it.emoji.name, it)); + } + + return [...matched.values()] + .sort((x, y) => y.score - x.score) + .slice(0, max) + .map(it => it.emoji); +} + function onMousedown(event: Event) { if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); } @@ -309,12 +359,25 @@ function onKeydown(event: KeyboardEvent) { } break; - case 'Tab': case 'ArrowDown': cancel(); selectNext(); break; + case 'Tab': + if (event.shiftKey) { + if (select.value !== -1) { + cancel(); + selectPrev(); + } else { + props.close(); + } + } else { + cancel(); + selectNext(); + } + break; + default: event.stopPropagation(); props.textarea.focus(); |