""" Sortable control for drag-and-drop reordering of list items. Wraps SortableJS to enable drag-and-drop on any container, posting the new item order to the server via HTMX after each drag operation. Requires SortableJS to be loaded via create_app(sortable=True). """ import logging from typing import Optional from fasthtml.components import Script from myfasthtml.core.commands import Command from myfasthtml.core.instances import MultipleInstance logger = logging.getLogger("Sortable") class Sortable(MultipleInstance): """ Composable control that enables SortableJS drag-and-drop on a container. Place this inside a render() method alongside the sortable container. Items in the container must have a ``data-sort-id`` attribute identifying each item. After a drag, the new order is POSTed to the server via the provided command. Args: parent: Parent instance that owns this control. command: Command to execute after reordering. Its handler must accept an ``order: list`` parameter receiving the sorted IDs. _id: Optional custom ID suffix. container_id: ID of the DOM element to make sortable. Defaults to ``parent.get_id()`` if not provided. handle: Optional CSS selector for the drag handle within each item. If None, the entire item is draggable. group: Optional SortableJS group name to allow dragging between multiple connected lists. """ def __init__(self, parent, command: Command, _id: Optional[str] = None, container_id: Optional[str] = None, handle: Optional[str] = None, group: Optional[str] = None): super().__init__(parent, _id=_id) self._command = command self._container_id = container_id self._handle = handle self._group = group def render(self): container_id = self._container_id or self._parent.get_id() opts = self._command.ajax_htmx_options() js_opts = ["animation: 150"] if self._handle: js_opts.append(f"handle: '{self._handle}'") if self._group: js_opts.append(f"group: '{self._group}'") existing_values = ", ".join(f'"{k}": "{v}"' for k, v in opts["values"].items()) js_opts.append(f"""onEnd: function(evt) {{ var items = Array.from(document.getElementById('{container_id}').children) .map(function(el) {{ return el.dataset.sortId; }}) .filter(Boolean); htmx.ajax('POST', '{opts["url"]}', {{ target: '{opts["target"]}', swap: '{opts["swap"]}', values: {{ {existing_values}, order: items.join(',') }} }}); }}""") js_opts_str = ",\n ".join(js_opts) script = f"""(function() {{ var container = document.getElementById('{container_id}'); if (!container) {{ return; }} new Sortable(container, {{ {js_opts_str} }}); }})();""" logger.debug(f"Sortable rendered for container={container_id}") return Script(script) def __ft__(self): return self.render()