From 9dccdfbde0c4190bffcbdfe5c6a5416a27066113 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 28 Mar 2026 20:35:40 -0400 Subject: [PATCH] Fix load filter dropdown positioning on bookshelf page The load filter dropdown was being cut off when the button was positioned on the left side of the screen due to static right-0 alignment. This became more problematic as the button position changes with window resize. Changes: - Added dynamic dropdown alignment calculation based on button position and available viewport space - Implemented smart positioning logic that checks available space on both left and right sides before deciding alignment - Added window resize listener using requestAnimationFrame to dynamically update dropdown position while open - Added data-load-filter-btn attribute for reliable DOM querying - Changed from static right-0 to dynamic :class binding for left/right alignment Technical details: - Alpine.js state: dropdownAlign tracks current alignment (left/right) - calculateAlignment() method computes button position and available space - Uses getBoundingClientRect() to measure button position relative to viewport - Prefers side with >=320px space, otherwise chooses larger side - requestAnimationFrame ensures smooth updates during resize without performance degradation Fixes issue where dropdown extends beyond viewport edge when button is near left or right edge of screen. --- templates/bookshelf.templ | 4 ++- templates/bookshelf_templ.go | 10 ++++---- web/src/bookshelf.ts | 47 +++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/templates/bookshelf.templ b/templates/bookshelf.templ index 307969b..a38b3d5 100644 --- a/templates/bookshelf.templ +++ b/templates/bookshelf.templ @@ -247,6 +247,7 @@ templ BookShelf(

Saved Filters

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
= 2) fetchAuthorValues($el)\">
= 2) fetchTagValues($el)\">
= 2) fetchSeriesValues($el)\">
= 2) fetchLanguageValues($el)\">

Saved Filters

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -140,7 +140,7 @@ func BookShelf( var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(uuidToString(filter.ID)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 275, Col: 173} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 277, Col: 173} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -153,7 +153,7 @@ func BookShelf( var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(filter.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 277, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 279, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -194,7 +194,7 @@ func BookShelf( var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 311, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 313, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -227,7 +227,7 @@ func BookShelf( var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 330, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 332, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { diff --git a/web/src/bookshelf.ts b/web/src/bookshelf.ts index 3412db4..d76b17c 100644 --- a/web/src/bookshelf.ts +++ b/web/src/bookshelf.ts @@ -80,6 +80,8 @@ Alpine.data("bookshelf", () => ({ showSaveModal: false, showFiltersDropdown: false, hasCoverState: null as boolean | null, + dropdownAlign: "right" as "left" | "right", + resizeTimeout: null as number | null, // Standalone function references (don't access component state) clearFilters, @@ -89,9 +91,52 @@ Alpine.data("bookshelf", () => ({ showSaveFilterModal() { this.showSaveModal = true; }, - // Toggle the filters dropdown + // Helper method to calculate alignment + calculateAlignment() { + const button = document.querySelector("[data-load-filter-btn]"); + if (!button) return; + + const rect = button.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const dropdownWidth = 320; + + const spaceOnLeft = rect.left; + const spaceOnRight = viewportWidth - rect.right; + + if (spaceOnRight >= dropdownWidth) { + this.dropdownAlign = "left"; + } else if (spaceOnLeft >= dropdownWidth) { + this.dropdownAlign = "right"; + } else { + this.dropdownAlign = spaceOnLeft > spaceOnRight ? "right" : "left"; + } + }, + toggleFiltersDropdown() { this.showFiltersDropdown = !this.showFiltersDropdown; + + if (this.showFiltersDropdown) { + this.$nextTick(() => { + this.calculateAlignment(); + }); + } + }, + // Alpine lifecycle hook - runs when component initializes + init() { + let rafId: number | null = null; + + window.addEventListener("resize", () => { + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + + rafId = requestAnimationFrame(() => { + if (this.showFiltersDropdown) { + this.calculateAlignment(); + } + rafId = null; + }); + }); }, // Cycle through has_cover states: null -> true -> false -> null cycleHasCover(): void {