diff --git a/CLAUDE.md b/CLAUDE.md
index 169a022..d53b284 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -64,8 +64,8 @@ docs/ — Additional documentation
- **`core.py`** — `Component(tag_name, attributes, children)`: the fundamental builder. `_render_element()` is `@lru_cache`-memoized (4096 entries, always active). Attribute values are always HTML-escaped; children are escaped unless they are `SafeText`. `randomid()` generates stable hash-based IDs.
- **`primitives.py`** — Generic HTML: `A()`, `Button()` (with color/size/icon params), `ButtonGroup()`, `Div()`, `Span()`, `Label()`, `Input()`, `Icon()`, `Popover()`, `PopoverTruncated()`, `SearchField()`, `H1()`, `Modal()`, `SimpleTable()`, `TableRow()`, `TableTd()`, `TableHeader()`, `paginated_table_content()`, `AddForm()`, `Pill()`, `CsrfInput()`, `ModuleScript()`
- **`domain.py`** — Domain-specific: `GameLink()`, `GameStatus()` (colored dot + label), `GameStatusSelector()` (Alpine.js PATCH dropdown), `SessionDeviceSelector()` (Alpine.js PATCH dropdown), `LinkedPurchase()`, `NameWithIcon()`, `PriceConverted()`, `PurchasePrice()`
-- **`filters.py`** — Filter UI: `FilterBar()`, `SessionFilterBar()`, `PurchaseFilterBar()`, `SelectableFilter()` (clickable include/exclude chips)
-- **`search_select.py`** — `SearchSelect()` + `SearchSelectOption`: search-as-you-type dropdown with removable pill selection, wired by `games/static/js/search_select.js`
+- **`filters.py`** — Filter UI: `FilterBar()`, `SessionFilterBar()`, `PurchaseFilterBar()` (built from `FilterSelect` widgets)
+- **`search_select.py`** — `SearchSelect()` (form combobox) + `FilterSelect()` (include/exclude filter combobox with pinned Any/None modifiers) + `SearchSelectOption`, all built on a shared `_combobox_shell`; wired by `games/static/js/search_select.js`
**Filter system** (`games/filters.py` + `common/criteria.py`): Stash-inspired structured filtering.
@@ -118,8 +118,7 @@ Only a small number of HTML templates remain (platform icon snippets and partial
- **Tailwind CSS** — utility classes, compiled from `common/input.css` → `games/static/base.css`
- **Custom JS** in `games/static/js/`:
- `toast.js` — Alpine.js toast store (listens for `show-toast` HTMX event)
- - `selectable_filter.js` — SelectableFilter widget interaction
- - `search_select.js` — SearchSelect widget (search-as-you-type, pills)
+ - `search_select.js` — SearchSelect/FilterSelect widgets (search-as-you-type, pills, include/exclude filter mode)
- `utils.js` — shared helpers (e.g., `fetchWithHtmxTriggers`)
### Deployment
diff --git a/common/components/__init__.py b/common/components/__init__.py
index 99b535f..cefdd5a 100644
--- a/common/components/__init__.py
+++ b/common/components/__init__.py
@@ -59,7 +59,6 @@ from common.components.domain import (
from common.components.filters import (
FilterBar,
PurchaseFilterBar,
- SelectableFilter,
SessionFilterBar,
)
@@ -109,6 +108,5 @@ __all__ = [
"_resolve_name_with_icon",
"FilterBar",
"PurchaseFilterBar",
- "SelectableFilter",
"SessionFilterBar",
]
diff --git a/common/components/filters.py b/common/components/filters.py
index e14fa87..f00ce3d 100644
--- a/common/components/filters.py
+++ b/common/components/filters.py
@@ -1,4 +1,4 @@
-"""Stash-style filter bars and the SelectableFilter widget."""
+"""Stash-style filter bars, built from FilterSelect widgets."""
from typing import NamedTuple
@@ -12,7 +12,7 @@ from common.components.search_select import FilterSelect
class FilterChoice(NamedTuple):
- """Parsed state of a SelectableFilter widget from a filter JSON blob."""
+ """Parsed include/exclude/modifier state of a filter field from filter JSON."""
selected: list[str]
excluded: list[str]
@@ -84,20 +84,6 @@ def _parse_bool(existing: dict, key: str) -> bool:
return bool(field.get("value", False))
-def _get_filter_options(model_class, order_by="name") -> list[tuple[str, str]]:
- """Return (value, label) pairs for a SelectableFilter from model rows.
-
- Uses values_list for efficiency (only fetches needed columns),
- but unpacks each row into readable local variables.
- """
- options: list[tuple[str, str]] = []
- for object_id, object_name in model_class.objects.order_by(order_by).values_list(
- "id", order_by
- ):
- options.append((str(object_id), object_name))
- return options
-
-
# ── FilterSelect adapters ────────────────────────────────────────────────────
# Each list filter is a FilterSelect. Enum fields pre-render their small, fixed
# option set; model-backed fields fetch from a search endpoint and only resolve
@@ -742,189 +728,6 @@ def FilterBar(
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
-def _selectable_filter_tag(
- value: str, label: str, *, excluded: bool = False
-) -> SafeText:
- """A selected (\u2713) or excluded (\u2717) value pill in the SelectableFilter."""
- checkmark = "\u2717" if excluded else "\u2713"
- css = "sf-tag sf-excluded" if excluded else "sf-tag"
- return Span(
- attributes=[
- ("class", css),
- ("data-value", value),
- ("data-type", "exclude" if excluded else "include"),
- ],
- children=[
- Span(
- attributes=[("class", "sf-tag-text")],
- children=[f"{checkmark} {label}"],
- ),
- Component(
- tag_name="button",
- attributes=[
- ("type", "button"),
- ("class", "sf-remove"),
- ("aria-label", "Remove"),
- ],
- children=["\u00d7"],
- ),
- ],
- )
-
-
-def _selectable_filter_modifier_tag(modifier: str, label: str) -> SafeText:
- """An active modifier pill ((Any) / (None)) in the SelectableFilter."""
- return Span(
- attributes=[
- ("class", "sf-modifier-tag active"),
- ("data-modifier", modifier),
- ],
- children=[label],
- )
-
-
-def _selectable_filter_modifier_option(modifier: str, label: str) -> SafeText:
- """A modifier choice in the SelectableFilter dropdown list."""
- return Component(
- tag_name="div",
- attributes=[
- ("class", "sf-option sf-modifier-option"),
- ("data-modifier", modifier),
- ("data-label", label),
- ],
- children=[
- Span(
- attributes=[("class", "sf-option-label")],
- children=[label],
- ),
- ],
- )
-
-
-def _selectable_filter_option(value: str, label: str) -> SafeText:
- """An option row with include (+) and exclude (\u2212) buttons."""
- return Component(
- tag_name="div",
- attributes=[
- ("class", "sf-option"),
- ("data-value", value),
- ("data-label", label),
- ],
- children=[
- Span(
- attributes=[("class", "sf-option-label")],
- children=[label],
- ),
- Span(
- attributes=[("class", "sf-option-buttons")],
- children=[
- Component(
- tag_name="button",
- attributes=[
- ("type", "button"),
- ("class", "sf-btn-include"),
- ("data-action", "include"),
- ("title", "Include"),
- ],
- children=["+"],
- ),
- Component(
- tag_name="button",
- attributes=[
- ("type", "button"),
- ("class", "sf-btn-exclude"),
- ("data-action", "exclude"),
- ("title", "Exclude"),
- ],
- children=["\u2212"],
- ),
- ],
- ),
- ],
- )
-
-
-def SelectableFilter(
- field_name: str,
- options: list[tuple[str, str]],
- selected: list[str] | None = None,
- excluded: list[str] | None = None,
- modifier: str = "",
- nullable: bool = True,
-) -> "SafeText":
- """Stash-style selectable filter with search, include/exclude, modifier tags."""
- selected = selected or []
- excluded = excluded or []
-
- modifier_options = [("NOT_NULL", "(Any)")]
- if nullable:
- modifier_options.append(("IS_NULL", "(None)"))
-
- active_modifier_tag = ""
- inactive_modifier_options: list[SafeText] = []
- for modifier_value, modifier_label in modifier_options:
- if modifier == modifier_value:
- active_modifier_tag = _selectable_filter_modifier_tag(
- modifier_value, modifier_label
- )
- else:
- inactive_modifier_options.append(
- _selectable_filter_modifier_option(modifier_value, modifier_label)
- )
-
- selected_tags: list[SafeText] = []
- for value in selected:
- selected_tags.append(
- _selectable_filter_tag(value, _find_label(options, value), excluded=False)
- )
- for value in excluded:
- selected_tags.append(
- _selectable_filter_tag(value, _find_label(options, value), excluded=True)
- )
-
- option_rows: list[SafeText] = []
- for value, label in options:
- option_rows.append(_selectable_filter_option(value, label))
-
- selected_area_children: list[SafeText] = []
- if active_modifier_tag:
- selected_area_children.append(active_modifier_tag)
- selected_area_children.extend(selected_tags)
-
- options_area_children: list[SafeText] = []
- options_area_children.extend(inactive_modifier_options)
- options_area_children.extend(option_rows)
-
- return Component(
- tag_name="div",
- attributes=[
- ("class", "sf-container"),
- ("data-selectable-filter", field_name),
- *([("data-modifier", modifier)] if modifier else []),
- ],
- children=[
- Component(
- tag_name="div",
- attributes=[("class", "sf-selected")],
- children=selected_area_children,
- ),
- Component(
- tag_name="input",
- attributes=[
- ("type", "text"),
- ("class", "sf-search"),
- ("placeholder", "Search\u2026"),
- ],
- ),
- Component(
- tag_name="div",
- attributes=[("class", "sf-options")],
- children=options_area_children,
- ),
- ],
- )
-
-
def _find_label(options: list[tuple[str, str]], value: str) -> str:
for v, label in options:
if str(v) == str(value):
diff --git a/common/criteria.py b/common/criteria.py
index 663fa74..ba4406b 100644
--- a/common/criteria.py
+++ b/common/criteria.py
@@ -299,7 +299,7 @@ class MultiCriterion(_Criterion):
class ChoiceCriterion(_Criterion):
"""Filter on a choice/enum field with multi-select include/exclude.
- Used by SelectableFilter widgets for status, ownership_type, etc.
+ Used by FilterSelect widgets for status, ownership_type, etc.
Supports INCLUDES, EXCLUDES, EQUALS, IS_NULL, NOT_NULL modifiers.
"""
diff --git a/common/input.css b/common/input.css
index ab07998..d299746 100644
--- a/common/input.css
+++ b/common/input.css
@@ -232,48 +232,3 @@ textarea:disabled {
}
}
-/* SelectableFilter widget styling */
-.sf-container {
- @apply border border-default-medium rounded-base bg-neutral-secondary-medium;
-}
-.sf-selected {
- @apply flex flex-wrap gap-1 p-2 min-h-[2rem];
-}
-.sf-tag {
- @apply inline-flex items-center gap-1 px-2 py-0.5 text-sm rounded bg-brand/15 text-heading;
-}
-.sf-tag.sf-excluded {
- @apply bg-red-500/15 text-red-600 line-through decoration-red-400;
-}
-.sf-remove {
- @apply ml-1 text-body hover:text-heading font-bold cursor-pointer;
-}
-.sf-modifier-tag {
- @apply inline-flex items-center px-2 py-0.5 text-sm rounded bg-amber-500/15 text-amber-600 cursor-pointer;
-}
-.sf-search {
- @apply block w-full border-0 border-t border-default-medium bg-transparent text-sm text-heading p-2;
- &:focus {
- @apply ring-0 outline-hidden;
- }
-}
-.sf-options {
- @apply max-h-40 overflow-y-auto p-1 text-body;
-}
-.sf-option {
- @apply flex items-center justify-between px-2 py-1 rounded text-sm hover:bg-neutral-secondary-strong cursor-pointer;
-}
-.sf-option-label {
- @apply truncate;
-}
-.sf-option-buttons {
- @apply flex gap-1 ml-2 shrink-0;
-}
-.sf-btn-include,
-.sf-btn-exclude {
- @apply w-5 h-5 flex items-center justify-center text-xs font-bold rounded border border-default-medium hover:bg-brand hover:text-white hover:border-brand;
-}
-.sf-modifier-option {
- @apply px-2 py-1 text-sm text-body hover:bg-neutral-secondary-strong cursor-pointer;
-}
-
diff --git a/games/static/base.css b/games/static/base.css
index 21d46a8..e6ebb04 100644
--- a/games/static/base.css
+++ b/games/static/base.css
@@ -4429,171 +4429,6 @@ form input:disabled, select:disabled, textarea:disabled {
padding: calc(var(--spacing) * 4);
}
}
-.sf-container {
- border-radius: var(--radius-base);
- border-style: var(--tw-border-style);
- border-width: 1px;
- border-color: var(--color-default-medium);
- background-color: var(--color-neutral-secondary-medium);
-}
-.sf-selected {
- display: flex;
- min-height: 2rem;
- flex-wrap: wrap;
- gap: calc(var(--spacing) * 1);
- padding: calc(var(--spacing) * 2);
-}
-.sf-tag {
- display: inline-flex;
- align-items: center;
- gap: calc(var(--spacing) * 1);
- border-radius: var(--radius);
- background-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 15%, transparent);
- @supports (color: color-mix(in lab, red, red)) {
- background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
- }
- padding-inline: calc(var(--spacing) * 2);
- padding-block: calc(var(--spacing) * 0.5);
- font-size: var(--text-sm);
- line-height: var(--tw-leading, var(--text-sm--line-height));
- color: var(--color-heading);
-}
-.sf-tag.sf-excluded {
- background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 15%, transparent);
- @supports (color: color-mix(in lab, red, red)) {
- background-color: color-mix(in oklab, var(--color-red-500) 15%, transparent);
- }
- color: var(--color-red-600);
- text-decoration-line: line-through;
- text-decoration-color: var(--color-red-400);
-}
-.sf-remove {
- margin-left: calc(var(--spacing) * 1);
- cursor: pointer;
- --tw-font-weight: var(--font-weight-bold);
- font-weight: var(--font-weight-bold);
- color: var(--color-body);
- &:hover {
- @media (hover: hover) {
- color: var(--color-heading);
- }
- }
-}
-.sf-modifier-tag {
- display: inline-flex;
- cursor: pointer;
- align-items: center;
- border-radius: var(--radius);
- background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent);
- @supports (color: color-mix(in lab, red, red)) {
- background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent);
- }
- padding-inline: calc(var(--spacing) * 2);
- padding-block: calc(var(--spacing) * 0.5);
- font-size: var(--text-sm);
- line-height: var(--tw-leading, var(--text-sm--line-height));
- color: var(--color-amber-600);
-}
-.sf-search {
- display: block;
- width: 100%;
- border-style: var(--tw-border-style);
- border-width: 0px;
- border-top-style: var(--tw-border-style);
- border-top-width: 1px;
- border-color: var(--color-default-medium);
- background-color: transparent;
- padding: calc(var(--spacing) * 2);
- font-size: var(--text-sm);
- line-height: var(--tw-leading, var(--text-sm--line-height));
- color: var(--color-heading);
- &:focus {
- --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
- --tw-outline-style: none;
- outline-style: none;
- @media (forced-colors: active) {
- outline: 2px solid transparent;
- outline-offset: 2px;
- }
- }
-}
-.sf-options {
- max-height: calc(var(--spacing) * 40);
- overflow-y: auto;
- padding: calc(var(--spacing) * 1);
- color: var(--color-body);
-}
-.sf-option {
- display: flex;
- cursor: pointer;
- align-items: center;
- justify-content: space-between;
- border-radius: var(--radius);
- padding-inline: calc(var(--spacing) * 2);
- padding-block: calc(var(--spacing) * 1);
- font-size: var(--text-sm);
- line-height: var(--tw-leading, var(--text-sm--line-height));
- &:hover {
- @media (hover: hover) {
- background-color: var(--color-neutral-secondary-strong);
- }
- }
-}
-.sf-option-label {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.sf-option-buttons {
- margin-left: calc(var(--spacing) * 2);
- display: flex;
- flex-shrink: 0;
- gap: calc(var(--spacing) * 1);
-}
-.sf-btn-include, .sf-btn-exclude {
- display: flex;
- height: calc(var(--spacing) * 5);
- width: calc(var(--spacing) * 5);
- align-items: center;
- justify-content: center;
- border-radius: var(--radius);
- border-style: var(--tw-border-style);
- border-width: 1px;
- border-color: var(--color-default-medium);
- font-size: var(--text-xs);
- line-height: var(--tw-leading, var(--text-xs--line-height));
- --tw-font-weight: var(--font-weight-bold);
- font-weight: var(--font-weight-bold);
- &:hover {
- @media (hover: hover) {
- border-color: var(--color-brand);
- }
- }
- &:hover {
- @media (hover: hover) {
- background-color: var(--color-brand);
- }
- }
- &:hover {
- @media (hover: hover) {
- color: var(--color-white);
- }
- }
-}
-.sf-modifier-option {
- cursor: pointer;
- padding-inline: calc(var(--spacing) * 2);
- padding-block: calc(var(--spacing) * 1);
- font-size: var(--text-sm);
- line-height: var(--tw-leading, var(--text-sm--line-height));
- color: var(--color-body);
- &:hover {
- @media (hover: hover) {
- background-color: var(--color-neutral-secondary-strong);
- }
- }
-}
@layer base {
input:where([type='text']),input:where(:not([type])),input:where([type='email']),input:where([type='url']),input:where([type='password']),input:where([type='number']),input:where([type='date']),input:where([type='datetime-local']),input:where([type='month']),input:where([type='search']),input:where([type='tel']),input:where([type='time']),input:where([type='week']),select:where([multiple]),textarea,select {
appearance: none;
diff --git a/games/static/js/search_select.js b/games/static/js/search_select.js
index 813881f..3f823d5 100644
--- a/games/static/js/search_select.js
+++ b/games/static/js/search_select.js
@@ -12,8 +12,8 @@
* pills. Filter widgets have no hidden inputs; readSearchSelect serialises their
* state into data-included / data-excluded / data-modifier for the filter bar.
*
- * Mirrors selectable_filter.js: initAll() on DOMContentLoaded + htmx:afterSwap,
- * each widget guarded with el._ssInit.
+ * initAll() runs on DOMContentLoaded + htmx:afterSwap, each widget guarded with
+ * el._ssInit.
*
* The pill / option class strings below are kept byte-identical to the Python
* Pill / SearchSelect / FilterSelect components so Tailwind generates the classes
@@ -496,8 +496,8 @@
// Serialise each widget's current state onto data-* attributes for the caller.
// Form widgets expose data-values (the submitted hidden-input values); filter
- // widgets (parallel to readSelectableFilters) expose data-included /
- // data-excluded / data-modifier for the filter bar to read.
+ // widgets expose data-included / data-excluded / data-modifier for the filter
+ // bar to read.
window.readSearchSelect = function (form) {
form.querySelectorAll("[data-search-select]").forEach(function (container) {
var pills = container.querySelector("[data-ss-pills]");
diff --git a/games/static/js/selectable_filter.js b/games/static/js/selectable_filter.js
deleted file mode 100644
index c6c8e52..0000000
--- a/games/static/js/selectable_filter.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * SelectableFilter widget — Stash-style choice filter with search,
- * include/exclude buttons, and modifier tags (Any / None).
- */
-(function () {
- "use strict";
-
- function initAll() {
- document.querySelectorAll("[data-selectable-filter]").forEach(function (el) {
- if (el._sfInit) return;
- el._sfInit = true;
- initWidget(el);
- });
- }
-
- function initWidget(container) {
- var search = container.querySelector(".sf-search");
- var options = container.querySelector(".sf-options");
- var selectedArea = container.querySelector(".sf-selected");
-
- if (!search || !options || !selectedArea) return;
-
- // ── Search ──
- search.addEventListener("input", function () {
- var q = search.value.toLowerCase();
- options.querySelectorAll(".sf-option").forEach(function (item) {
- var label = (item.getAttribute("data-label") || "").toLowerCase();
- item.style.display = label.indexOf(q) !== -1 ? "" : "none";
- });
- });
-
- // ── Include / Exclude clicks ──
- options.addEventListener("click", function (e) {
- var btn = e.target.closest("button");
- if (btn) {
- var action = btn.getAttribute("data-action");
- var itemEl = btn.closest(".sf-option");
- if (!itemEl) return;
- var value = itemEl.getAttribute("data-value");
- var label = itemEl.getAttribute("data-label");
- if (!value) return;
- if (action === "include") addTag(container, value, label, "include");
- else if (action === "exclude") addTag(container, value, label, "exclude");
- return;
- }
-
- // Click on modifier option (not a button)
- var modOption = e.target.closest(".sf-modifier-option");
- if (modOption) {
- var modVal = modOption.getAttribute("data-modifier");
- setModifier(container, modVal);
- }
- });
-
- // ── Remove selected tag ──
- selectedArea.addEventListener("click", function (e) {
- var removeBtn = e.target.closest(".sf-remove");
- if (removeBtn) {
- removeBtn.closest(".sf-tag").remove();
- return;
- }
-
- // Click on active modifier tag → deselect it
- var modTag = e.target.closest(".sf-modifier-tag");
- if (modTag) {
- clearModifier(container);
- }
- });
- }
-
- /** Add a tag to the selected area and clear modifier. */
- function addTag(container, value, label, type) {
- clearModifier(container);
- var selectedArea = container.querySelector(".sf-selected");
-
- // Check if already present
- var existing = selectedArea.querySelector('.sf-tag[data-value="' + value + '"]');
- if (existing) {
- if (existing.getAttribute("data-type") !== type) {
- existing.setAttribute("data-type", type);
- existing.classList.toggle("sf-excluded", type === "exclude");
- var text = existing.querySelector(".sf-tag-text");
- if (text) text.textContent = (type === "exclude" ? "✗ " : "✓ ") + label;
- }
- return;
- }
-
- var tag = document.createElement("span");
- tag.className = "sf-tag" + (type === "exclude" ? " sf-excluded" : "");
- tag.setAttribute("data-value", value);
- tag.setAttribute("data-type", type);
- tag.innerHTML =
- '' + (type === "exclude" ? "✗ " : "✓ ") + label + "" +
- '';
- selectedArea.appendChild(tag);
- }
-
- /** Set a modifier (Any / None) — clears all tags. */
- function setModifier(container, modVal) {
- var selectedArea = container.querySelector(".sf-selected");
-
- // Clear all tags
- selectedArea.querySelectorAll(".sf-tag").forEach(function (t) { t.remove(); });
-
- // Clear existing modifier tag
- selectedArea.querySelectorAll(".sf-modifier-tag").forEach(function (t) { t.remove(); });
-
- // Add new modifier tag
- var label = modVal === "NOT_NULL" ? "(Any)" : "(None)";
- var tag = document.createElement("span");
- tag.className = "sf-modifier-tag active";
- tag.setAttribute("data-modifier", modVal);
- tag.textContent = label;
- selectedArea.appendChild(tag);
-
- container.setAttribute("data-modifier", modVal);
- }
-
- /** Clear any active modifier, removing the tag. */
- function clearModifier(container) {
- var selectedArea = container.querySelector(".sf-selected");
- selectedArea.querySelectorAll(".sf-modifier-tag").forEach(function (t) { t.remove(); });
- container.removeAttribute("data-modifier");
- }
-
- // Read selections for form submission
- window.readSelectableFilters = function (form) {
- form.querySelectorAll("[data-selectable-filter]").forEach(function (container) {
- var modifier = container.getAttribute("data-modifier");
- var modTag = container.querySelector(".sf-modifier-tag.active");
- if (modTag) modifier = modTag.getAttribute("data-modifier");
-
- var included = [];
- var excluded = [];
- container.querySelectorAll(".sf-tag").forEach(function (tag) {
- var val = tag.getAttribute("data-value");
- if (tag.getAttribute("data-type") === "exclude") excluded.push(val);
- else included.push(val);
- });
-
- container.setAttribute("data-included", JSON.stringify(included));
- container.setAttribute("data-excluded", JSON.stringify(excluded));
- if (modifier) container.setAttribute("data-modifier", modifier);
- });
- };
-
- document.addEventListener("DOMContentLoaded", initAll);
- document.addEventListener("htmx:afterSwap", initAll);
-})();
diff --git a/games/views/game.py b/games/views/game.py
index 06bdada..12ae14a 100644
--- a/games/views/game.py
+++ b/games/views/game.py
@@ -149,7 +149,7 @@ def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse:
content,
title="Manage games",
scripts=ModuleScript("range_slider.js")
- + ModuleScript("selectable_filter.js")
+ + ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)
diff --git a/games/views/purchase.py b/games/views/purchase.py
index 70309e5..9b292f9 100644
--- a/games/views/purchase.py
+++ b/games/views/purchase.py
@@ -142,7 +142,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
content,
title="Manage purchases",
scripts=ModuleScript("range_slider.js")
- + ModuleScript("selectable_filter.js")
+ + ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)
diff --git a/games/views/session.py b/games/views/session.py
index cc25005..becdaa3 100644
--- a/games/views/session.py
+++ b/games/views/session.py
@@ -182,7 +182,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
content,
title="Manage sessions",
scripts=ModuleScript("range_slider.js")
- + ModuleScript("selectable_filter.js")
+ + ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)