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.
This commit is contained in:
+46
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user