Skip to content

Commit 89fb017

Browse files
committed
fix: 候補リストに同じ文字列が含まれる場合にキーボードによる候補の移動がおかしくなる問題を修正
- fix & refactor: 選択候補の管理をテキストベースから index ベースに変更 - これにより,候補リストに同じ文字列が含まれていても正しく操作できるようになった - refactor: `useWordSuggestionList` の役割を,候補リストの提供のみに限定 - refactor: types の定義を `/@/lib/suggestion/base.ts` にまとめる - refactor: 引数の型をより汎用的にする - refactor: 命名や空白や宣言順序の軽微な改善 - fix: 非推奨の `insert` API の代わりに `setRangeText` を利用する
1 parent ccea15d commit 89fb017

File tree

14 files changed

+235
-234
lines changed

14 files changed

+235
-234
lines changed

dev.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const DEV_SERVER_PROXY_HOST = 'https://q-dev.trapti.tech'
1+
export const DEV_SERVER_PROXY_HOST = 'https://q.trap.jp'

package-lock.json

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"mitt": "^3.0.0",
4444
"party-js": "^2.2.0",
4545
"sortablejs": "^1.15.6",
46-
"text-field-edit": "^4.1.1",
4746
"throttle-debounce": "^5.0.2",
4847
"ts-pattern": "^5.8.0",
4948
"turndown": "^7.2.1",

src/components/Main/MainView/MessageInput/DropdownSuggester/DropdownSuggester.vue

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,9 @@
3131

3232
<script lang="ts" setup>
3333
import { computed, type ComponentPublicInstance } from 'vue'
34-
import type {
35-
Candidate,
36-
WordOrConfirmedPart
37-
} from '../composables/suggestion/useWordSuggester'
3834
import DropdownSuggesterCandidate from './DropdownSuggesterCandidate.vue'
3935
import { isIOS } from '/@/lib/dom/browser'
36+
import type { Candidate, WordOrConfirmedPart } from '/@/lib/suggestion/basic'
4037
4138
const props = withDefaults(
4239
defineProps<{

src/components/Main/MainView/MessageInput/DropdownSuggester/DropdownSuggesterCandidate.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import DropdownSuggesterUserIcon from './DropdownSuggesterUserIcon.vue'
2424
import AStamp from '/@/components/UI/AStamp.vue'
2525
import DropdownSuggesterStampEffect from './DropdownSuggesterStampEffect.vue'
26-
import type { WordOrConfirmedPart } from '../composables/suggestion/useWordSuggester'
26+
import type { WordOrConfirmedPart } from '/@/lib/suggestion/basic'
2727
2828
withDefaults(
2929
defineProps<{

src/components/Main/MainView/MessageInput/MessageInputTextArea.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
:width="suggesterWidth"
3030
:position="suggesterPosition"
3131
:candidates="suggestedCandidates"
32-
:selected-index="selectedCandidateIndex"
32+
:selected-index="selectedIndex"
3333
:confirmed-part="confirmedPart"
3434
@select="onSelect"
3535
/>
@@ -91,7 +91,9 @@ const firefoxFlag = isFirefox()
9191
const { isMobile } = useResponsiveStore()
9292
9393
const textareaAutosizeRef = ref<InstanceType<typeof TextareaAutosize>>()
94-
const textareaRef = computed(() => textareaAutosizeRef.value?.$el)
94+
const textareaRef = computed<HTMLTextAreaElement>(
95+
() => textareaAutosizeRef.value?.$el
96+
)
9597
9698
defineExpose({ textareaAutosizeRef })
9799
@@ -106,10 +108,10 @@ const {
106108
suggesterWidth,
107109
position,
108110
suggestedCandidates,
109-
selectedCandidateIndex,
111+
selectedIndex,
110112
confirmedPart,
111113
onSelect
112-
} = useSuggester(textareaRef, modelValue)
114+
} = useSuggester(textareaRef)
113115
114116
const {
115117
onBeforeInput,

src/components/Main/MainView/MessageInput/composables/suggestion/overrides/channelSuggestion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
2-
import type { Candidate, ConfirmedPart } from '../useWordSuggester'
2+
import type { Candidate, ConfirmedPart } from '/@/lib/suggestion/basic'
33

44
const CHANNEL_PATH_TRIMMING_REGEXP = /^#|\/$/
55

src/components/Main/MainView/MessageInput/composables/suggestion/overrides/stampSuggestion.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
2+
import { useStampHistory } from '/@/store/domain/stampHistory'
23
import type {
34
Candidate,
45
ConfirmedPart,
5-
WordOrConfirmedPart
6-
} from '../useWordSuggester'
7-
import { useStampHistory } from '/@/store/domain/stampHistory'
8-
import type { WordWithId } from '../useWordSuggestionList'
6+
WordOrConfirmedPart,
7+
WordWithId
8+
} from '/@/lib/suggestion/basic'
99

1010
const stampSuggestionOverride = <
1111
Params extends {

src/components/Main/MainView/MessageInput/composables/suggestion/useWordSuggester.ts

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
import type { ComputedRef, Ref } from 'vue'
2-
import { computed, ref, watch } from 'vue'
3-
import type { Word } from './useWordSuggestionList'
1+
import type { MaybeRefOrGetter } from 'vue'
2+
import { computed, ref, toValue, watch } from 'vue'
43
import useWordSuggesterList from './useWordSuggestionList'
54
import useInsertText from '/@/composables/dom/useInsertText'
65
import getCaretPosition from '/@/lib/dom/caretPosition'
7-
import type { Target } from '/@/lib/suggestion/basic'
8-
import { getCurrentWord } from '/@/lib/suggestion/basic'
9-
10-
export type WordOrConfirmedPart =
11-
| Word
12-
| {
13-
type: 'confirmed-part'
14-
text: string
15-
}
16-
17-
export interface Candidate {
18-
word: Word
19-
display?: string
20-
}
21-
22-
export interface ConfirmedPart {
23-
text: string
24-
display?: string
25-
}
6+
import type { Target, WordOrConfirmedPart } from '/@/lib/suggestion/basic'
7+
import {
8+
getCurrentWord,
9+
getNextCandidateIndex,
10+
getPrevCandidateIndex,
11+
getSelectedCandidateIndex
12+
} from '/@/lib/suggestion/basic'
2613

2714
/**
2815
* 補完を表示する最小の文字数
@@ -31,51 +18,77 @@ export interface ConfirmedPart {
3118
const MIN_LENGTH = 3
3219

3320
const useWordSuggester = (
34-
textareaRef: ComputedRef<HTMLTextAreaElement | undefined>,
35-
value: Ref<string>
21+
textareaRef: MaybeRefOrGetter<HTMLTextAreaElement>
3622
) => {
3723
const isSuggesterShown = ref(false)
24+
3825
const target = ref<Target>({
3926
word: '',
4027
begin: 0,
4128
end: 0
4229
})
30+
31+
/**
32+
* nullのときは未選択
33+
* -1のときは確定部分が選択されている
34+
* 0~のときは候補が選択されている
35+
*/
36+
const selectedIndex = ref<number | null>(null)
37+
4338
/**
4439
* targetは補完をしたときに更新されないがこれは更新する
4540
* これはtargetを更新すると候補リストが変わってしまうため
4641
* (補完をしたときは候補リストを変化させたくない)
4742
*/
4843
const currentInputWord = ref('')
44+
45+
const { suggestedCandidateWords, confirmedText } = useWordSuggesterList(
46+
() => target.value.word,
47+
MIN_LENGTH
48+
)
49+
4950
watch(
5051
() => target.value.word,
5152
word => {
5253
currentInputWord.value = word
54+
55+
selectedIndex.value = getSelectedCandidateIndex(
56+
suggestedCandidateWords.value,
57+
confirmedText.value,
58+
word
59+
)
60+
},
61+
{
62+
immediate: true
5363
}
5464
)
5565

66+
const { insertText } = useInsertText(textareaRef, target)
67+
5668
const position = computed(() => {
57-
if (!textareaRef.value) return { top: 0, left: 0 }
58-
return getCaretPosition(textareaRef.value, target.value.begin)
69+
if (!toValue(textareaRef)) return { top: 0, left: 0 }
70+
return getCaretPosition(toValue(textareaRef), target.value.begin)
5971
})
6072

61-
const {
62-
suggestedCandidateWords,
63-
confirmedText,
64-
selectedCandidateIndex,
65-
prevCandidateText,
66-
nextCandidateText
67-
} = useWordSuggesterList(target, currentInputWord, MIN_LENGTH)
73+
const getCandidateTextFromIndex = (i: number) => {
74+
if (i === -1) return confirmedText.value
75+
return suggestedCandidateWords.value[i]?.text ?? ''
76+
}
77+
78+
const insertTextAndMoveTarget = (index: number) => {
79+
selectedIndex.value = index
80+
const text = getCandidateTextFromIndex(index)
6881

69-
const { insertText } = useInsertText(textareaRef, target)
70-
const insertTextAndMoveTarget = (text: string) => {
7182
insertText(text)
7283
target.value.end = target.value.begin + text.length
7384
currentInputWord.value = text
7485
}
7586

7687
const updateTarget = () => {
77-
if (!textareaRef.value) return
78-
target.value = getCurrentWord(textareaRef.value, value.value)
88+
if (!toValue(textareaRef)) return
89+
90+
target.value = getCurrentWord(toValue(textareaRef))
91+
7992
if (target.value.word.length < MIN_LENGTH) {
8093
isSuggesterShown.value = false
8194
return
@@ -88,6 +101,16 @@ const useWordSuggester = (
88101
if (e.isComposing) return
89102
if (!isSuggesterShown.value) return
90103

104+
const prevIndex = getPrevCandidateIndex(
105+
suggestedCandidateWords.value.length,
106+
selectedIndex.value
107+
)
108+
109+
const nextIndex = getNextCandidateIndex(
110+
suggestedCandidateWords.value.length,
111+
selectedIndex.value
112+
)
113+
91114
// Tabによるフォーカスの移動を防止するため、長押しで連続移動できるようにするためにkeyDownで行う必要がある
92115
if (e.key === 'Tab') {
93116
e.preventDefault()
@@ -98,24 +121,25 @@ const useWordSuggester = (
98121

99122
// 未選択状態では確定部分が補完される
100123
if (e.shiftKey) {
101-
insertTextAndMoveTarget(prevCandidateText.value)
124+
insertTextAndMoveTarget(prevIndex)
102125
} else {
103-
insertTextAndMoveTarget(nextCandidateText.value)
126+
insertTextAndMoveTarget(nextIndex)
104127
}
105128
return
106129
}
107130

108131
if (e.key === 'ArrowUp') {
109132
e.preventDefault()
110-
insertTextAndMoveTarget(prevCandidateText.value)
133+
insertTextAndMoveTarget(prevIndex)
111134
return
112135
}
113136
if (e.key === 'ArrowDown') {
114137
e.preventDefault()
115-
insertTextAndMoveTarget(nextCandidateText.value)
138+
insertTextAndMoveTarget(nextIndex)
116139
return
117140
}
118141
}
142+
119143
const onKeyUp = (e: KeyboardEvent) => {
120144
if (e.key === 'Tab' || e.key === 'ArrowUp' || e.key === 'ArrowDown') return
121145
// 文字入力後の状態をとるためkeyUpで行う必要がある
@@ -153,7 +177,7 @@ const useWordSuggester = (
153177
suggesterWidth: 240,
154178
position,
155179
suggestedCandidates,
156-
selectedCandidateIndex,
180+
selectedIndex,
157181
confirmedPart
158182
}
159183
}

0 commit comments

Comments
 (0)