ObjetoSearch.svelte 6.45 KB
<script>
  import { goto } from '$app/navigation';

  let {
    preserveVista = false,
    vista = 'agregado'
  } = $props();

  let searchInputRef = $state(null);
  let searchVal = $state('');
  let searchFocused = $state(false);
  let isSearching = $state(false);
  let localResults = $state([]);
  let selectedIdx = $state(-1);
  let debounceTimer;

  function parseMetadatos(meta) {
    if (!meta) return {};
    if (typeof meta === 'object') return meta;
    try { return JSON.parse(meta); } catch { return {}; }
  }

  async function doSearch(query) {
    isSearching = true;
    try {
      const params = new URLSearchParams({
        q: query,
        per_page: '20',
        is_class: 'true',
        class_: 'objeto'
      });
      const res = await fetch(`/api/search?${params}`);
      if (!res.ok) throw new Error();
      const data = await res.json();
      localResults = (data.hits || []).map(hit => {
        const meta = parseMetadatos(hit.document.metadatos);
        const codigo = meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || '';
        const nombre = hit.document.texto;
        const highlight = hit.highlights?.[0]?.snippet || nombre;
        return { codigo, nombre, highlight };
      });
      selectedIdx = -1;
    } catch {
      localResults = [];
    }
    isSearching = false;
  }

  function handleSearchInput(e) {
    searchVal = e.target.value;
    clearTimeout(debounceTimer);
    if (searchVal.length >= 2) {
      debounceTimer = setTimeout(() => doSearch(searchVal), 200);
    } else {
      localResults = [];
    }
  }

  function handleSearchKeydown(e) {
    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
      e.preventDefault();
      if (e.key === 'ArrowDown') {
        selectedIdx = selectedIdx < localResults.length - 1 ? selectedIdx + 1 : 0;
      } else {
        selectedIdx = selectedIdx > 0 ? selectedIdx - 1 : localResults.length - 1;
      }
    } else if (e.key === 'Enter' && localResults.length > 0) {
      e.preventDefault();
      const selected = localResults[selectedIdx >= 0 ? selectedIdx : 0];
      if (selected) handleSearchSelect(selected);
    } else if (e.key === 'Escape') {
      searchVal = '';
      localResults = [];
      searchInputRef?.blur();
    }
  }

  function handleSearchSelect(result) {
    const url = preserveVista && vista === 'comparar'
      ? `/objeto/${result.codigo}?vista=comparar`
      : `/objeto/${result.codigo}`;
    goto(url);
    searchVal = '';
    localResults = [];
    searchFocused = false;
  }

  function handleSearchBlur() {
    setTimeout(() => {
      searchFocused = false;
    }, 150);
  }

  function handleClear() {
    localResults = [];
    searchVal = '';
  }
</script>

<div class="objeto-search" class:focused={searchFocused}>
  <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
  </svg>
  <input
    type="text"
    bind:this={searchInputRef}
    bind:value={searchVal}
    oninput={handleSearchInput}
    onkeydown={handleSearchKeydown}
    onfocus={() => searchFocused = true}
    onblur={handleSearchBlur}
    placeholder="Buscar objeto..."
  />
  {#if searchVal}
    <button class="search-clear" onclick={handleClear}>
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
      </svg>
    </button>
  {/if}
  {#if searchFocused && searchVal.length >= 2}
    <div class="search-dropdown">
      {#if isSearching}
        <div class="search-msg">Buscando...</div>
      {:else if localResults.length === 0}
        <div class="search-msg">Sin resultados</div>
      {:else}
        {#each localResults as result, i}
          <button
            class="search-item"
            class:selected={selectedIdx === i}
            onclick={() => handleSearchSelect(result)}
            onmouseenter={() => selectedIdx = i}
          >
            <span class="item-code">{result.codigo}</span>
            <span class="item-name">{@html result.highlight}</span>
          </button>
        {/each}
      {/if}
    </div>
  {/if}
</div>

<style>
  .objeto-search {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: var(--search-bg, #f5f5f5);
    border: none;
    border-radius: 8px;
    padding: 0.625rem 1rem;
    transition: box-shadow 0.2s;
    width: 100%;
  }

  :global(html.dark) .objeto-search {
    --search-bg: rgba(255, 255, 255, 0.08);
  }

  .objeto-search.focused {
    border-color: #6B9FD4;
    box-shadow: 0 0 0 3px rgba(107, 159, 212, 0.15);
  }

  .search-icon {
    color: var(--theme-texto);
    opacity: 0.5;
    flex-shrink: 0;
  }

  input {
    flex: 1;
    border: none;
    background: transparent;
    color: var(--theme-titulo);
    font-size: 0.8125rem;
    outline: none;
    min-width: 120px;
  }

  input::placeholder {
    color: var(--theme-texto);
    opacity: 0.5;
  }

  .search-clear {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.25rem;
    border-radius: 4px;
    border: none;
    background: transparent;
    color: var(--theme-texto);
    cursor: pointer;
    transition: background 0.2s;
  }

  .search-clear:hover {
    background: var(--theme-borde);
  }

  .search-dropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    background: var(--theme-surface);
    border: 1px solid var(--theme-borde);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
    max-height: 280px;
    overflow-y: auto;
    z-index: 100;
  }

  .search-msg {
    padding: 0.75rem 1rem;
    font-size: 0.8125rem;
    color: var(--theme-texto);
    opacity: 0.7;
  }

  .search-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 0.625rem 1rem;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
    transition: background 0.15s;
  }

  .search-item:hover,
  .search-item.selected {
    background: var(--theme-surface-hover);
  }

  .item-code {
    font-family: 'DM Mono', monospace;
    font-size: 0.75rem;
    color: var(--theme-texto);
    min-width: 3.5rem;
  }

  .item-name {
    font-size: 0.8125rem;
    color: var(--theme-titulo);
  }

  :global(.item-name mark) {
    background: rgba(201, 167, 81, 0.3);
    color: inherit;
    border-radius: 2px;
    padding: 0 1px;
  }
</style>