공개 검색 한글 입력 처리 개선
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* @typedef {{ name: string, slug: string }} SearchTagHit
|
||||
* @typedef {{ slug: string, title: string, excerpt: string }} SearchPostHit
|
||||
* @typedef {{ slug: string, title: string, excerpt: string }} SearchPageHit
|
||||
*/
|
||||
|
||||
const open = defineModel({ type: Boolean, default: false })
|
||||
@@ -14,6 +15,16 @@ const isComposing = ref(false)
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
let debounceTimer = null
|
||||
|
||||
/**
|
||||
* 검색에 사용할 입력값을 정규화한다.
|
||||
* @param {string} value - 원본 입력값
|
||||
* @returns {string} 정규화된 검색어
|
||||
*/
|
||||
const normalizeSearchQuery = (value) => String(value ?? '')
|
||||
.normalize('NFC')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
/**
|
||||
* 입력값을 디바운스로 검색어에 반영한다.
|
||||
* @param {string} value - 현재 입력값
|
||||
@@ -22,11 +33,14 @@ let debounceTimer = null
|
||||
const scheduleDebouncedQuery = (value) => {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
debouncedQuery.value = value
|
||||
debouncedQuery.value = normalizeSearchQuery(value)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
watch(query, (value) => {
|
||||
if (isComposing.value) {
|
||||
return
|
||||
}
|
||||
scheduleDebouncedQuery(value)
|
||||
})
|
||||
|
||||
@@ -36,7 +50,11 @@ const { data, pending } = await useFetch('/api/search', {
|
||||
key: fetchKey,
|
||||
query: { q: debouncedQuery },
|
||||
watch: [debouncedQuery],
|
||||
default: () => ({ tags: /** @type {SearchTagHit[]} */ ([]), posts: /** @type {SearchPostHit[]} */ ([]) })
|
||||
default: () => ({
|
||||
tags: /** @type {SearchTagHit[]} */ ([]),
|
||||
posts: /** @type {SearchPostHit[]} */ ([]),
|
||||
pages: /** @type {SearchPageHit[]} */ ([])
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -54,7 +72,7 @@ const escapeRegExp = (value) => value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
||||
*/
|
||||
const highlightParts = (text, needle) => {
|
||||
const base = String(text ?? '')
|
||||
const q = String(needle ?? '').trim()
|
||||
const q = normalizeSearchQuery(needle)
|
||||
if (!q) {
|
||||
return [{ text: base, hit: false }]
|
||||
}
|
||||
@@ -110,32 +128,40 @@ const clearQuery = () => {
|
||||
*/
|
||||
const onCompositionStart = () => {
|
||||
isComposing.value = true
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
|
||||
/**
|
||||
* IME 조합 중에도 입력창 value를 기반으로 검색어를 갱신한다.
|
||||
* 일부 환경에서는 v-model 업데이트가 조합 종료까지 지연될 수 있다.
|
||||
* @param {CompositionEvent} event - 조합 업데이트 이벤트
|
||||
* 입력 이벤트에서 최종 검색어를 갱신한다.
|
||||
* @param {InputEvent} event - 입력 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onCompositionUpdate = (event) => {
|
||||
const onInput = (event) => {
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return
|
||||
}
|
||||
const value = target.value
|
||||
query.value = value
|
||||
scheduleDebouncedQuery(value)
|
||||
query.value = target.value
|
||||
if (isComposing.value || event.isComposing) {
|
||||
clearTimeout(debounceTimer)
|
||||
return
|
||||
}
|
||||
scheduleDebouncedQuery(target.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 한글/일본어 등 IME 조합 종료 처리(종료 시점에만 검색 갱신)
|
||||
* @param {CompositionEvent} event - 조합 종료 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onCompositionEnd = () => {
|
||||
const onCompositionEnd = (event) => {
|
||||
isComposing.value = false
|
||||
const target = event.target
|
||||
if (target instanceof HTMLInputElement) {
|
||||
query.value = target.value
|
||||
}
|
||||
clearTimeout(debounceTimer)
|
||||
debouncedQuery.value = query.value
|
||||
debouncedQuery.value = normalizeSearchQuery(query.value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,8 +234,8 @@ onBeforeUnmount(() => {
|
||||
class="site-search-modal__input min-w-0 flex-1 bg-transparent py-2 text-lg outline-none placeholder:text-[var(--site-soft)] focus-visible:ring-0 sm:text-[1.35rem]"
|
||||
placeholder="글 제목, 본문, 태그 검색"
|
||||
@compositionstart="onCompositionStart"
|
||||
@compositionupdate="onCompositionUpdate"
|
||||
@compositionend="onCompositionEnd"
|
||||
@input="onInput"
|
||||
/>
|
||||
<button type="button" class="site-search-modal__cancel shrink-0 rounded-md px-2 py-1 text-sm text-[var(--site-soft)] hover:text-[var(--site-text)] sm:hidden" @click="open = false">
|
||||
취소
|
||||
|
||||
Reference in New Issue
Block a user