cjm-fasthtml-sortable-queue
A reusable FastHTML component for Sortable.js-enhanced ordered queue panels with drag-and-drop reorder, keyboard navigation, and HTMX-powered mutations.
Install
pip install cjm_fasthtml_sortable_queueProject Structure
nbs/
├── config.ipynb # Configuration dataclass for sortable queue instances.
├── handlers.ipynb # Tier 1 handler functions for queue reorder, remove, and clear operations.
├── html_ids.ipynb # HTML element ID generators for sortable queue components.
├── keyboard.ipynb # Self-contained keyboard system for sortable queue navigation and actions.
├── models.ipynb # URL bundle and reorder utility functions for sortable queues.
├── rendering.ipynb # Queue panel and item rendering with callback-based custom content.
├── router.ipynb # Tier 2 convenience router with auto-wired reorder, remove, and clear endpoints.
└── sortable_js.ipynb # Sortable.js CDN loading and initialization script generation.
Total: 8 notebooks
Module Dependencies
graph LR
config[config<br/>config]
handlers[handlers<br/>handlers]
html_ids[html_ids<br/>html_ids]
keyboard[keyboard<br/>keyboard]
models[models<br/>models]
rendering[rendering<br/>rendering]
router[router<br/>router]
sortable_js[sortable_js<br/>sortable_js]
handlers --> models
handlers --> html_ids
handlers --> config
handlers --> rendering
keyboard --> models
keyboard --> html_ids
keyboard --> config
rendering --> config
rendering --> models
rendering --> html_ids
router --> models
router --> html_ids
router --> handlers
router --> config
14 cross-module dependencies detected
CLI Reference
No CLI commands found in this project.
Module Overview
Detailed documentation for each module in the project:
config (config.ipynb)
Configuration dataclass for sortable queue instances.
Import
from cjm_fasthtml_sortable_queue.config import (
SortableQueueConfig,
get_item_key
)Functions
def get_item_key(
config: SortableQueueConfig, # Queue configuration
item: dict, # Queue item dict
) -> str: # Unique key string for the item
"Extract the unique key from a queue item using the config's key_fn or key_field."Classes
@dataclass
class SortableQueueConfig:
"Configuration for a sortable queue instance."
prefix: str # HTML ID prefix (e.g., "sq", "sd", "tss")
key_field: str = 'id' # Field name to extract item key (simple case)
key_fn: Optional[Callable[[dict], str]] # Custom key extraction (overrides key_field if set)
item_class: str = 'queue-item' # CSS class for queue items (keyboard selector + styling target)
handle_class: str = 'drag-handle' # CSS class for Sortable.js drag handle
animation_ms: int = 150 # Sortable.js drag animation duration in milliseconds
queue_title: str = 'Selected' # Header title texthandlers (handlers.ipynb)
Tier 1 handler functions for queue reorder, remove, and clear operations.
Import
from cjm_fasthtml_sortable_queue.handlers import (
handle_reorder,
handle_reorder_by_direction,
handle_remove,
handle_clear
)Functions
def handle_reorder(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints
items: List[dict], # Current item list
new_key_order: List[str], # Keys in new order (from form_data.getlist("item"))
render_content: Callable[[dict, int], Any], # Custom content callback
**render_kwargs, # Additional kwargs passed to render_sortable_queue
) -> tuple: # (reordered_items, rendered_panel)
"Reorder items by key order and return the updated list and rendered panel."def handle_reorder_by_direction(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints
items: List[dict], # Current item list
item_key: str, # Key of item to move
direction: str, # "up" or "down"
render_content: Callable[[dict, int], Any], # Custom content callback
**render_kwargs, # Additional kwargs passed to render_sortable_queue
) -> tuple: # (reordered_items, rendered_panel)
"Move an item up or down and return the updated list and rendered panel."def handle_remove(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints
items: List[dict], # Current item list
remove_key: str, # Key of item to remove
render_content: Callable[[dict, int], Any], # Custom content callback
**render_kwargs, # Additional kwargs passed to render_sortable_queue
) -> tuple: # (updated_items, rendered_panel)
"Remove an item by key and return the updated list and rendered panel."def handle_clear(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints
render_content: Callable[[dict, int], Any], # Custom content callback
**render_kwargs, # Additional kwargs passed to render_sortable_queue
) -> tuple: # (empty_list, rendered_panel)
"Clear all items and return an empty list and rendered panel."html_ids (html_ids.ipynb)
HTML element ID generators for sortable queue components.
Import
from cjm_fasthtml_sortable_queue.html_ids import (
SortableQueueHtmlIds
)Functions
def _safe_id(
value: str, # Raw value to sanitize
) -> str: # HTML-safe ID fragment
"Replace non-alphanumeric characters with hyphens for use in HTML IDs."Classes
@dataclass
class SortableQueueHtmlIds:
"HTML element ID generators for a sortable queue instance."
prefix: str # Instance prefix (e.g., "sq", "sd", "tss")
def container(self) -> str: # Outer container div ID
"""Queue panel container."""
return f"{self.prefix}-queue-container"
@property
def list(self) -> str: # Sortable <ul> ID
"Queue panel container."
def list(self) -> str: # Sortable <ul> ID
"""Sortable queue list element."""
return f"{self.prefix}-queue-list"
@property
def empty(self) -> str: # Empty state div ID
"Sortable queue list element."
def empty(self) -> str: # Empty state div ID
"""Empty state placeholder."""
return f"{self.prefix}-queue-empty"
@property
def header(self) -> str: # Header section ID
"Empty state placeholder."
def header(self) -> str: # Header section ID
"""Queue header section."""
return f"{self.prefix}-queue-header"
@property
def remove_btn(self) -> str: # Hidden keyboard remove button ID
"Queue header section."
def remove_btn(self) -> str: # Hidden keyboard remove button ID
"""Hidden button for keyboard-triggered remove."""
return f"{self.prefix}-queue-remove-btn"
@property
def reorder_up_btn(self) -> str: # Hidden keyboard reorder-up button ID
"Hidden button for keyboard-triggered remove."
def reorder_up_btn(self) -> str: # Hidden keyboard reorder-up button ID
"""Hidden button for keyboard-triggered reorder up."""
return f"{self.prefix}-queue-reorder-up-btn"
@property
def reorder_down_btn(self) -> str: # Hidden keyboard reorder-down button ID
"Hidden button for keyboard-triggered reorder up."
def reorder_down_btn(self) -> str: # Hidden keyboard reorder-down button ID
"""Hidden button for keyboard-triggered reorder down."""
return f"{self.prefix}-queue-reorder-down-btn"
@property
def system_id(self) -> str: # Keyboard system ID for hierarchy wiring
"Hidden button for keyboard-triggered reorder down."
def system_id(self) -> str: # Keyboard system ID for hierarchy wiring
"Keyboard system identifier for coordinator registration."
def item(
"Per-item HTML ID."
def as_selector(
self,
html_id: str, # An HTML ID string
) -> str: # CSS selector string
"Convert an HTML ID to a CSS selector."keyboard (keyboard.ipynb)
Self-contained keyboard system for sortable queue navigation and actions.
Import
from cjm_fasthtml_sortable_queue.keyboard import (
create_queue_keyboard_system
)Functions
def create_queue_keyboard_system(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints for HTMX actions
zone_focus_classes: tuple = (str(ring(0)),), # CSS classes when queue zone is active
item_focus_classes: Optional[tuple] = None, # CSS classes on focused item (default: bg-base-300)
data_attributes: tuple = (), # Data attributes to extract (e.g., ("record-id", "provider-id"))
on_focus_change: Optional[str] = None, # JS callback on item focus change
hidden_input_prefix: Optional[str] = None, # Prefix for hidden state inputs
system_id: Optional[str] = None, # Keyboard system ID (auto-generated from ids.system_id if not set)
show_hints: bool = False, # Show keyboard hints UI
) -> KeyboardSystem: # Complete rendered keyboard system
"""
Create a self-contained keyboard system for the sortable queue.
Returns a rendered KeyboardSystem with a single FocusZone (LinearVertical
navigation) and built-in actions for Delete/Backspace remove and
Shift+Arrow reorder. Works standalone or as a child in a hierarchy
via `coord.setParent(system_id, parent_id)`.
"""Variables
_DEFAULT_ITEM_FOCUS_CLASSESmodels (models.ipynb)
URL bundle and reorder utility functions for sortable queues.
Import
from cjm_fasthtml_sortable_queue.models import (
SortableQueueUrls,
reorder_by_keys,
reorder_by_direction
)Functions
def reorder_by_keys(
items: List[dict], # Current item list
new_key_order: List[str], # Keys in desired order (from Sortable.js form data)
key_fn: Callable[[dict], str], # Function to extract key from item
) -> List[dict]: # Reordered item list
"Reorder items to match the key order from Sortable.js form data."def reorder_by_direction(
items: List[dict], # Current item list
item_key: str, # Key of item to move
direction: str, # "up" or "down"
key_fn: Callable[[dict], str], # Function to extract key from item
) -> List[dict]: # Reordered item list
"Move an item up or down by swapping with its neighbor."Classes
@dataclass
class SortableQueueUrls:
"URL endpoints for sortable queue HTMX operations."
reorder: str # POST — Sortable.js drag-end reorder
remove: str # POST — Remove item from queue
clear: str # POST — Clear all itemsrendering (rendering.ipynb)
Queue panel and item rendering with callback-based custom content.
Import
from cjm_fasthtml_sortable_queue.rendering import (
render_queue_item,
render_sortable_queue
)Functions
def render_queue_item(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints
item: dict, # Queue item data
index: int, # 0-based position in queue
render_content: Callable[[dict, int], Any], # Callback for custom item content
extra_attrs: Optional[dict] = None, # Additional HTML attributes per item
) -> Any: # Li element
"Render a single queue item with drag handle, position, custom content, and remove button."def _render_default_empty() -> Any: # Empty state element
"Default empty state when no items are in the queue."def render_sortable_queue(
config: SortableQueueConfig, # Queue configuration
ids: SortableQueueHtmlIds, # HTML ID generators
urls: SortableQueueUrls, # URL endpoints
queue_items: List[dict], # Ordered list of queue item dicts
render_content: Callable[[dict, int], Any], # Callback for custom item content
render_empty: Optional[Callable[[], Any]] = None, # Custom empty state (default provided)
render_header_actions: Optional[Callable[[List[dict], SortableQueueUrls], Any]] = None, # Custom header actions
render_footer: Optional[Callable[[List[dict]], Any]] = None, # Optional footer content
extra_item_attrs: Optional[Callable[[dict], dict]] = None, # Additional data-* attributes per item
container_classes: tuple = (), # Additional classes on container div
) -> Any: # Queue panel element
"Render the complete sortable queue panel."router (router.ipynb)
Tier 2 convenience router with auto-wired reorder, remove, and clear endpoints.
Import
from cjm_fasthtml_sortable_queue.router import (
init_sortable_queue_router
)Functions
def init_sortable_queue_router(
config: SortableQueueConfig, # Queue configuration
get_items: Callable[[str], List[dict]], # (session_id) -> current items
set_items: Callable[[str, List[dict]], None], # (session_id, items) -> persist
render_content: Callable[[dict, int], Any], # Custom content callback
on_mutate: Optional[Callable[[str, List[dict], str], tuple]] = None, # (mutation_type, items, sess) -> OOB elements
prefix: str = "/queue", # Route prefix
**render_kwargs, # Additional kwargs passed to render_sortable_queue
) -> Tuple[APIRouter, SortableQueueUrls]: # (router, urls) tuple
"""
Initialize a router with reorder, remove, and clear endpoints.
The `on_mutate` callback receives the mutation type ("reorder", "remove",
"clear"), the updated items list, and the session ID. It should return a
tuple of OOB elements to append to the response, or an empty tuple.
"""sortable_js (sortable_js.ipynb)
Sortable.js CDN loading and initialization script generation.
Import
from cjm_fasthtml_sortable_queue.sortable_js import (
SORTABLE_JS_CDN,
sortable_js_headers,
generate_sortable_init_script
)Functions
def sortable_js_headers() -> tuple: # Tuple of Script elements for app headers
"CDN script tag for Sortable.js, suitable for FastHTML app headers."def generate_sortable_init_script(
handle_class: str = "drag-handle", # CSS class for drag handle elements
animation_ms: int = 150, # Drag animation duration in milliseconds
) -> Script: # Script element with Sortable.js initialization
"""
Generate htmx.onLoad script that initializes Sortable.js on `.sortable` elements.
Includes the disable-on-drag/re-enable-on-swap pattern to prevent
double-firing during HTMX response processing.
"""Variables
SORTABLE_JS_CDN = 'https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js'