import logging from typing import Callable, Any from fasthtml.components import * from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.core.instances import MultipleInstance, BaseInstance from myfasthtml.core.matching_utils import subsequence_matching, fuzzy_matching logger = logging.getLogger("Search") class Commands(BaseCommands): def search(self): return (Command("Search", f"Search {self._owner.items_names}", self._owner, self._owner.on_search).htmx(target=f"#{self._owner.get_id()}-results", trigger="keyup changed delay:300ms", swap="innerHTML")) class Search(MultipleInstance): """ Represents a component for managing and filtering a list of items. It uses fuzzy matching and subsequence matching to filter items. :ivar items_names: The name of the items used to filter. :type items_names: str :ivar items: The first set of items to filter. :type items: list :ivar filtered: A copy of the `items` list, representing the filtered items after a search operation. :type filtered: list :ivar get_attr: Callable function to extract string values from items for filtering. :type get_attr: Callable[[Any], str] :ivar template: Callable function to define how filtered items are rendered. :type template: Callable[[Any], Any] """ def __init__(self, parent: BaseInstance, _id=None, items_names=None, # what is the name of the items to filter items=None, # first set of items to filter get_attr: Callable[[Any], str] = None, # items is a list of objects: how to get the str to filter template: Callable[[Any], Any] = None): # once filtered, what to render ? """ Represents a component for managing and filtering a list of items based on specific criteria. This class initializes with a session, an optional identifier, a list of item names, a callable for extracting a string value from items, and a template callable for rendering the filtered items. It provides functionality to handle and organize item-based operations. :param _id: Optional identifier for the component. :param items: An optional list of names for the items to be filtered. :param get_attr: Callable function to extract a string value from an item for filtering. Defaults to a function that returns the item as is. :param template: Callable function to render the filtered items. Defaults to a Div rendering function. """ super().__init__(parent, _id=_id) self.items_names = items_names or '' self.items = items or [] self.filtered = self.items.copy() self.get_attr = get_attr or (lambda x: x) self.template = template or Div self.commands = Commands(self) def set_items(self, items): self.items = items self.filtered = self.items.copy() return self def get_items(self): return self.items def get_filtered(self): return self.filtered def on_search(self, query): logger.debug(f"on_search {query=}") self.search(query) return tuple(self._mk_search_results()) def search(self, query): logger.debug(f"search {query=}") if query is None or query.strip() == "": self.filtered = self.items.copy() else: res_seq = subsequence_matching(query, self.items, get_attr=self.get_attr) res_fuzzy = fuzzy_matching(query, self.items, get_attr=self.get_attr) self.filtered = res_seq + res_fuzzy return self.filtered def _mk_search_results(self): return [self.template(item) for item in self.filtered] def render(self): return Div( mk.mk(Input(name="query", id=f"{self._id}-search", type="text", placeholder="Search...", cls="input input-xs"), command=self.commands.search()), Div( *self._mk_search_results(), id=f"{self._id}-results", cls="mf-search-results", ), id=f"{self._id}", ) def __ft__(self): return self.render()