- IME 조합 중에도 검색되는 공통 컴포저블 useImeSearchInput 추가 - 글/멤버/미디어/설정/태그 필터/미디어 모달 검색 IME 조합 대응 - 홈 추천글·페이지 섹션 가로 트랙 touch-action pan-x 제거로 모바일 세로 스크롤 복구 - v1.10.29 Co-authored-by: Cursor <cursoragent@cursor.com>
85 lines
2.7 KiB
JavaScript
85 lines
2.7 KiB
JavaScript
import { ref, watch } from 'vue'
|
|
|
|
/**
|
|
* 검색어를 NFC로 정규화한다.
|
|
* @param {string} value - 원본 입력값
|
|
* @returns {string} 정규화된 문자열
|
|
*/
|
|
const normalizeSearchValue = (value) => String(value ?? '').normalize('NFC')
|
|
|
|
/**
|
|
* 한글 등 IME 조합 중에도 검색어가 즉시 반영되는 검색 입력 처리기를 만든다.
|
|
*
|
|
* Vue의 `v-model`은 IME 조합 중에는 값을 갱신하지 않아, 조합이 끝나야(스페이스·다음 글자)
|
|
* 검색이 시작되는 문제가 있다. 이 컴포저블은 `compositionupdate`까지 반영해 조합 도중에도
|
|
* 검색이 되게 하고, 선택적으로 디바운스를 적용한다.
|
|
*
|
|
* @param {import('vue').Ref<string>} targetRef - 검색/필터에 사용할 대상 ref(정규화된 값이 기록됨)
|
|
* @param {Object} [options] - 옵션
|
|
* @param {number} [options.debounce=0] - 디바운스 지연(ms). 0이면 즉시 반영
|
|
* @returns {{ displayValue: import('vue').Ref<string>, onInput: (event: Event) => void, onCompositionEnd: (event: Event) => void }}
|
|
*/
|
|
export const useImeSearchInput = (targetRef, options = {}) => {
|
|
const debounce = Number(options.debounce) || 0
|
|
const displayValue = ref(String(targetRef.value ?? ''))
|
|
|
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
let debounceTimer = null
|
|
|
|
/**
|
|
* 대상 ref에 정규화된 값을 반영한다(디바운스 적용).
|
|
* @param {string} value - 현재 입력값
|
|
* @returns {void}
|
|
*/
|
|
const commit = (value) => {
|
|
if (debounce > 0) {
|
|
clearTimeout(debounceTimer)
|
|
debounceTimer = setTimeout(() => {
|
|
targetRef.value = normalizeSearchValue(value)
|
|
}, debounce)
|
|
return
|
|
}
|
|
targetRef.value = normalizeSearchValue(value)
|
|
}
|
|
|
|
/**
|
|
* 입력 이벤트에서 표시값과 검색어를 갱신한다(IME 조합 중 포함).
|
|
* @param {Event} event - `input`/`compositionupdate` 이벤트
|
|
* @returns {void}
|
|
*/
|
|
const onInput = (event) => {
|
|
const target = event.target
|
|
if (!(target instanceof HTMLInputElement)) {
|
|
return
|
|
}
|
|
displayValue.value = target.value
|
|
commit(target.value)
|
|
}
|
|
|
|
/**
|
|
* IME 조합 종료 시 최종 검색어를 즉시 반영한다.
|
|
* @param {Event} event - `compositionend` 이벤트
|
|
* @returns {void}
|
|
*/
|
|
const onCompositionEnd = (event) => {
|
|
const target = event.target
|
|
if (target instanceof HTMLInputElement) {
|
|
displayValue.value = target.value
|
|
}
|
|
clearTimeout(debounceTimer)
|
|
targetRef.value = normalizeSearchValue(displayValue.value)
|
|
}
|
|
|
|
watch(targetRef, (value) => {
|
|
if (normalizeSearchValue(displayValue.value) !== value) {
|
|
displayValue.value = String(value ?? '')
|
|
}
|
|
})
|
|
|
|
return {
|
|
displayValue,
|
|
onInput,
|
|
onCompositionEnd
|
|
}
|
|
}
|