query params data search
This commit is contained in:
parent
e9117a1adf
commit
af88c88aa6
|
|
@ -642,6 +642,11 @@ const rows = computed<RowVm[]>(() => {
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- Scroll element (exposed for parent scroll tracking / restoration) ---
|
||||||
|
|
||||||
|
const listEl = ref<HTMLElement | null>(null)
|
||||||
|
defineExpose({ listEl })
|
||||||
|
|
||||||
// ---- Infinite scroll ---------------------------------------------------
|
// ---- Infinite scroll ---------------------------------------------------
|
||||||
|
|
||||||
const sentinel = ref<HTMLElement | null>(null)
|
const sentinel = ref<HTMLElement | null>(null)
|
||||||
|
|
@ -659,7 +664,7 @@ useIntersectionObserver(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="overflow-y-auto divide-y divide-default flex-1">
|
<div ref="listEl" class="overflow-y-auto divide-y divide-default flex-1">
|
||||||
<div
|
<div
|
||||||
v-if="loading && !rows.length"
|
v-if="loading && !rows.length"
|
||||||
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { watch, onBeforeUnmount } from 'vue'
|
||||||
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface UrlSearchState {
|
||||||
|
query: string
|
||||||
|
page: number
|
||||||
|
scroll: number
|
||||||
|
selectedId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseSearchUrlSyncOptions {
|
||||||
|
query: Ref<string>
|
||||||
|
page: Ref<number>
|
||||||
|
/** Computed or ref yielding the ID of the currently selected item */
|
||||||
|
selectedId: Ref<string | null> | ComputedRef<string | null>
|
||||||
|
/** The scrollable list container, if any (for scroll tracking & restoration) */
|
||||||
|
scrollEl?: Ref<HTMLElement | null> | ComputedRef<HTMLElement | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Read initial state from URL ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads search state from the current route's query params.
|
||||||
|
* Safe to call during SSR (falls back to empty defaults).
|
||||||
|
*
|
||||||
|
* Call this BEFORE creating your reactive refs and initialise them with
|
||||||
|
* the returned values so the first search uses the URL state directly —
|
||||||
|
* this avoids a redundant re-search triggered by the debounced watcher.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
* const query = ref(q0) // debouncedQuery starts at q0 → watcher won't re-fire
|
||||||
|
* const activePage = ref(p0)
|
||||||
|
*/
|
||||||
|
export function useSearchUrlState(): UrlSearchState {
|
||||||
|
const route = useRoute()
|
||||||
|
return {
|
||||||
|
query: String(route.query.q ?? ''),
|
||||||
|
page: Math.max(1, parseInt(String(route.query.page ?? '1'), 10) || 1),
|
||||||
|
scroll: Math.max(0, parseInt(String(route.query.scroll ?? '0'), 10) || 0),
|
||||||
|
selectedId: route.query.selected ? String(route.query.selected) : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Keep state in sync with URL ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches query, page, selectedId and (optionally) scroll on a container
|
||||||
|
* element, writing each change to the URL via history.replaceState so the
|
||||||
|
* state can be copied and restored by any user.
|
||||||
|
*
|
||||||
|
* Uses replaceState (not pushState) so the browser history stack stays clean.
|
||||||
|
* Existing URL params unrelated to search are preserved.
|
||||||
|
*/
|
||||||
|
export function useSearchUrlSync(options: UseSearchUrlSyncOptions): void {
|
||||||
|
if (!import.meta.client) return
|
||||||
|
|
||||||
|
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let queryTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
// ── Write a partial patch to the current URL ────────────────────────────
|
||||||
|
|
||||||
|
function patchUrl(params: Record<string, string | null>) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
for (const [key, val] of Object.entries(params)) {
|
||||||
|
if (val !== null && val !== '') {
|
||||||
|
url.searchParams.set(key, val)
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history.replaceState(history.state ?? null, '', url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Watchers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Query: debounce to avoid thrashing on every keystroke.
|
||||||
|
// Also resets page and scroll since a new query starts fresh.
|
||||||
|
watch(options.query, (q) => {
|
||||||
|
if (queryTimer) clearTimeout(queryTimer)
|
||||||
|
queryTimer = setTimeout(
|
||||||
|
() => patchUrl({ q: q || null, page: null, scroll: null }),
|
||||||
|
150,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Page: immediate — user navigated intentionally.
|
||||||
|
// Scroll position is no longer meaningful when page changes.
|
||||||
|
watch(options.page, (p) => {
|
||||||
|
patchUrl({ page: p > 1 ? String(p) : null, scroll: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Selected item: immediate.
|
||||||
|
watch(options.selectedId, (id) => {
|
||||||
|
patchUrl({ selected: id || null })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Scroll tracking ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (options.scrollEl) {
|
||||||
|
function handleScroll(ev: Event) {
|
||||||
|
if (scrollTimer) clearTimeout(scrollTimer)
|
||||||
|
scrollTimer = setTimeout(() => {
|
||||||
|
const top = (ev.target as HTMLElement).scrollTop
|
||||||
|
patchUrl({ scroll: top > 0 ? String(Math.round(top)) : null })
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
options.scrollEl,
|
||||||
|
(el, oldEl) => {
|
||||||
|
;(oldEl as HTMLElement | null)?.removeEventListener('scroll', handleScroll)
|
||||||
|
;(el as HTMLElement | null)?.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
;(options.scrollEl!.value as HTMLElement | null)
|
||||||
|
?.removeEventListener('scroll', handleScroll)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (scrollTimer) clearTimeout(scrollTimer)
|
||||||
|
if (queryTimer) clearTimeout(queryTimer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scroll restoration helper ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a scroll position on an element after a short settling delay.
|
||||||
|
* Call this inside onMounted, after awaiting the first search, so the list
|
||||||
|
* has had time to render its items.
|
||||||
|
*/
|
||||||
|
export function restoreScrollPosition(
|
||||||
|
scrollEl: HTMLElement | null | undefined,
|
||||||
|
scrollY: number,
|
||||||
|
delayMs = 80,
|
||||||
|
): void {
|
||||||
|
if (!scrollY || !scrollEl || !import.meta.client) return
|
||||||
|
setTimeout(() => { scrollEl.scrollTop = scrollY }, delayMs)
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,10 @@ const REQUEST_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
const query = ref('')
|
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
||||||
|
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
|
||||||
|
const query = ref(q0)
|
||||||
const debouncedQuery = useDebounce(query, 150)
|
const debouncedQuery = useDebounce(query, 150)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
|
|
@ -22,7 +25,7 @@ const meili = useMeiliSearchRef()
|
||||||
|
|
||||||
const hits = ref<SearchHit[]>([])
|
const hits = ref<SearchHit[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const activePage = ref(1)
|
const activePage = ref(p0)
|
||||||
|
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize)))
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize)))
|
||||||
|
|
||||||
|
|
@ -100,19 +103,7 @@ function retry() {
|
||||||
runSearch(query.value, activePage.value, false)
|
runSearch(query.value, activePage.value, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => runSearch('', 1, false))
|
// ── Selección ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
abortController?.abort()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(debouncedQuery, (q) => {
|
|
||||||
hits.value = []
|
|
||||||
total.value = 0
|
|
||||||
activePage.value = 1
|
|
||||||
runSearch(q, 1, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedActivity = ref<SearchHit | null>(null)
|
const selectedActivity = ref<SearchHit | null>(null)
|
||||||
|
|
||||||
const isActivityPanelOpen = computed({
|
const isActivityPanelOpen = computed({
|
||||||
|
|
@ -120,6 +111,43 @@ const isActivityPanelOpen = computed({
|
||||||
set(value: boolean) { if (!value) selectedActivity.value = null }
|
set(value: boolean) { if (!value) selectedActivity.value = null }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ID del item seleccionado para sincronizar con la URL
|
||||||
|
const selectedId = computed(() => selectedActivity.value?._id?.toString() ?? null)
|
||||||
|
|
||||||
|
// ── Scroll ref expuesto desde InboxList ────────────────────────────────────
|
||||||
|
const inboxListRef = ref<{ listEl: HTMLElement | null } | null>(null)
|
||||||
|
const listEl = computed(() => inboxListRef.value?.listEl ?? null)
|
||||||
|
|
||||||
|
// ── Sincronización con URL ─────────────────────────────────────────────────
|
||||||
|
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listEl })
|
||||||
|
|
||||||
|
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
||||||
|
onMounted(async () => {
|
||||||
|
await runSearch(q0, p0, false)
|
||||||
|
|
||||||
|
// Restaurar scroll
|
||||||
|
restoreScrollPosition(listEl.value, s0)
|
||||||
|
|
||||||
|
// Restaurar item seleccionado
|
||||||
|
if (sid0) {
|
||||||
|
const found = hits.value.find(h => String(h._id) === sid0)
|
||||||
|
if (found) selectedActivity.value = found
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
abortController?.abort()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Query debounced: solo dispara cuando el usuario cambia la búsqueda,
|
||||||
|
// NO en la carga inicial (debouncedQuery ya arranca en q0).
|
||||||
|
watch(debouncedQuery, (q) => {
|
||||||
|
hits.value = []
|
||||||
|
total.value = 0
|
||||||
|
activePage.value = 1
|
||||||
|
runSearch(q, 1, false)
|
||||||
|
})
|
||||||
|
|
||||||
watch(hits, () => {
|
watch(hits, () => {
|
||||||
if (!selectedActivity.value) return
|
if (!selectedActivity.value) return
|
||||||
const stillThere = hits.value.find(h => h._id === selectedActivity.value?._id)
|
const stillThere = hits.value.find(h => h._id === selectedActivity.value?._id)
|
||||||
|
|
@ -176,6 +204,7 @@ useDetailHistory(isActivityPanelOpen, isMobile)
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InboxList
|
<InboxList
|
||||||
|
ref="inboxListRef"
|
||||||
v-model="selectedActivity"
|
v-model="selectedActivity"
|
||||||
:activities="hits"
|
:activities="hits"
|
||||||
:query="debouncedQuery"
|
:query="debouncedQuery"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ const REQUEST_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
const query = ref('')
|
// ── Restore state from URL before creating refs ────────────────────────────
|
||||||
|
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
|
||||||
|
const query = ref(q0)
|
||||||
const debouncedQuery = useDebounce(query, 150)
|
const debouncedQuery = useDebounce(query, 150)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
|
|
@ -22,7 +25,7 @@ const meili = useMeiliSearchRef()
|
||||||
|
|
||||||
const hits = ref<SearchHit[]>([])
|
const hits = ref<SearchHit[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const activePage = ref(1)
|
const activePage = ref(p0)
|
||||||
|
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize)))
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize)))
|
||||||
|
|
||||||
|
|
@ -100,19 +103,7 @@ function retry() {
|
||||||
runSearch(query.value, activePage.value, false)
|
runSearch(query.value, activePage.value, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => runSearch('', 1, false))
|
// ── Selección ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
abortController?.abort()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(debouncedQuery, (q) => {
|
|
||||||
hits.value = []
|
|
||||||
total.value = 0
|
|
||||||
activePage.value = 1
|
|
||||||
runSearch(q, 1, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
const selected = ref<SearchHit | null>(null)
|
const selected = ref<SearchHit | null>(null)
|
||||||
|
|
||||||
const isPanelOpen = computed({
|
const isPanelOpen = computed({
|
||||||
|
|
@ -120,6 +111,43 @@ const isPanelOpen = computed({
|
||||||
set(value: boolean) { if (!value) selected.value = null }
|
set(value: boolean) { if (!value) selected.value = null }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ID del item seleccionado para sincronizar con la URL
|
||||||
|
const selectedId = computed(() => selected.value?._id?.toString() ?? null)
|
||||||
|
|
||||||
|
// ── Scroll ref expuesto desde InboxList ────────────────────────────────────
|
||||||
|
const inboxListRef = ref<{ listEl: HTMLElement | null } | null>(null)
|
||||||
|
const listEl = computed(() => inboxListRef.value?.listEl ?? null)
|
||||||
|
|
||||||
|
// ── Sincronización con URL ─────────────────────────────────────────────────
|
||||||
|
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listEl })
|
||||||
|
|
||||||
|
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
||||||
|
onMounted(async () => {
|
||||||
|
await runSearch(q0, p0, false)
|
||||||
|
|
||||||
|
// Restaurar scroll
|
||||||
|
restoreScrollPosition(listEl.value, s0)
|
||||||
|
|
||||||
|
// Restaurar item seleccionado
|
||||||
|
if (sid0) {
|
||||||
|
const found = hits.value.find(h => String(h._id) === sid0)
|
||||||
|
if (found) selected.value = found
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
abortController?.abort()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Query debounced: solo dispara cuando el usuario cambia la búsqueda,
|
||||||
|
// NO en la carga inicial (debouncedQuery ya arranca en q0).
|
||||||
|
watch(debouncedQuery, (q) => {
|
||||||
|
hits.value = []
|
||||||
|
total.value = 0
|
||||||
|
activePage.value = 1
|
||||||
|
runSearch(q, 1, false)
|
||||||
|
})
|
||||||
|
|
||||||
watch(hits, () => {
|
watch(hits, () => {
|
||||||
if (!selected.value) return
|
if (!selected.value) return
|
||||||
const stillThere = hits.value.find(h => h._id === selected.value?._id)
|
const stillThere = hits.value.find(h => h._id === selected.value?._id)
|
||||||
|
|
@ -180,6 +208,7 @@ useDetailHistory(isPanelOpen, isMobile)
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InboxList
|
<InboxList
|
||||||
|
ref="inboxListRef"
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
:activities="hits"
|
:activities="hits"
|
||||||
:query="debouncedQuery"
|
:query="debouncedQuery"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ const REQUEST_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
const query = ref('')
|
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
||||||
|
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
|
||||||
|
const query = ref(q0)
|
||||||
const debouncedQuery = useDebounce(query, 150)
|
const debouncedQuery = useDebounce(query, 150)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
@ -84,7 +87,7 @@ interface TypesenseSearchResponse {
|
||||||
const hits = ref<TypesenseHit[]>([])
|
const hits = ref<TypesenseHit[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const activePage = ref(1)
|
const activePage = ref(p0)
|
||||||
|
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize)))
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize)))
|
||||||
|
|
||||||
|
|
@ -183,8 +186,6 @@ function retry() {
|
||||||
runSearch(query.value, activePage.value, false)
|
runSearch(query.value, activePage.value, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => runSearch('', 1, false))
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timeoutId) clearTimeout(timeoutId)
|
if (timeoutId) clearTimeout(timeoutId)
|
||||||
})
|
})
|
||||||
|
|
@ -214,6 +215,29 @@ watch(hits, () => {
|
||||||
if (!stillThere) selected.value = null
|
if (!stillThere) selected.value = null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ID del item seleccionado para sincronizar con la URL
|
||||||
|
const selectedId = computed(() => selected.value?.id?.toString() ?? null)
|
||||||
|
|
||||||
|
// Contenedor scrollable de la lista
|
||||||
|
const listEl = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// ── Sincronización con URL ─────────────────────────────────────────────────
|
||||||
|
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listEl })
|
||||||
|
|
||||||
|
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
||||||
|
onMounted(async () => {
|
||||||
|
await runSearch(q0, p0, false)
|
||||||
|
|
||||||
|
// Restaurar scroll
|
||||||
|
restoreScrollPosition(listEl.value, s0)
|
||||||
|
|
||||||
|
// Restaurar item seleccionado
|
||||||
|
if (sid0) {
|
||||||
|
const found = hits.value.find(h => h.document.id === sid0)
|
||||||
|
if (found) selected.value = found.document
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
const isMobile = breakpoints.smaller('lg')
|
const isMobile = breakpoints.smaller('lg')
|
||||||
|
|
||||||
|
|
@ -320,7 +344,7 @@ function highlightedFor(hit: TypesenseHit, field: string): string | null {
|
||||||
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="overflow-y-auto divide-y divide-default flex-1">
|
<div ref="listEl" class="overflow-y-auto divide-y divide-default flex-1">
|
||||||
<div
|
<div
|
||||||
v-if="loading && !hits.length"
|
v-if="loading && !hits.length"
|
||||||
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ const REQUEST_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
const query = ref('')
|
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
||||||
|
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
|
||||||
|
const query = ref(q0)
|
||||||
const debouncedQuery = useDebounce(query, 150)
|
const debouncedQuery = useDebounce(query, 150)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
|
|
@ -158,7 +161,7 @@ const displayGroups = computed((): DisplayGroup[] => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Paginación activa (compartida entre browse y search)
|
// Paginación activa (compartida entre browse y search)
|
||||||
const activePage = ref(1)
|
const activePage = ref(p0)
|
||||||
|
|
||||||
const displayTotal = computed(() =>
|
const displayTotal = computed(() =>
|
||||||
debouncedQuery.value.trim() ? total.value : browseTotal.value
|
debouncedQuery.value.trim() ? total.value : browseTotal.value
|
||||||
|
|
@ -389,7 +392,6 @@ function retry() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => runBrowse(1, false))
|
|
||||||
onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
||||||
|
|
||||||
watch(debouncedQuery, (q) => {
|
watch(debouncedQuery, (q) => {
|
||||||
|
|
@ -526,6 +528,39 @@ watch(groupedHits, () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ID del documento seleccionado para sincronizar con la URL
|
||||||
|
const selectedId = computed(() => selectedDocId.value)
|
||||||
|
|
||||||
|
// ── Sincronización con URL ─────────────────────────────────────────────────
|
||||||
|
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listContainer })
|
||||||
|
|
||||||
|
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
||||||
|
onMounted(async () => {
|
||||||
|
// Carga inicial: browse si no hay query, search si hay query
|
||||||
|
if (q0.trim()) {
|
||||||
|
await runSearch(q0, p0, false)
|
||||||
|
} else {
|
||||||
|
await runBrowse(p0, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restaurar scroll
|
||||||
|
restoreScrollPosition(listContainer.value, s0)
|
||||||
|
|
||||||
|
// Restaurar documento seleccionado
|
||||||
|
if (sid0) {
|
||||||
|
const group = displayGroups.value.find(g => g.docId === sid0)
|
||||||
|
if (group) {
|
||||||
|
selectGroup(group)
|
||||||
|
} else {
|
||||||
|
// El documento puede no estar en la lista visible (p.ej. browse con paginación)
|
||||||
|
// Lo cargamos directamente por ID
|
||||||
|
selectedDocId.value = sid0
|
||||||
|
fetchFullDocument(sid0)
|
||||||
|
fetchDocumentParagraphs(sid0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
const isMobile = breakpoints.smaller('lg')
|
const isMobile = breakpoints.smaller('lg')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue