Fix pagination controls, move mixin to common app, fix pagination invocation on all views, and other random bug fixes

This commit is contained in:
badblocks 2025-04-01 23:01:05 -07:00
parent 7edefe23c3
commit 6a61b79bbe
425 changed files with 51656 additions and 243 deletions

View file

@ -0,0 +1 @@
(()=>{function g(n){n.directive("collapse",e),e.inline=(t,{modifiers:i})=>{i.includes("min")&&(t._x_doShow=()=>{},t._x_doHide=()=>{})};function e(t,{modifiers:i}){let r=l(i,"duration",250)/1e3,h=l(i,"min",0),u=!i.includes("min");t._x_isShown||(t.style.height=`${h}px`),!t._x_isShown&&u&&(t.hidden=!0),t._x_isShown||(t.style.overflow="hidden");let c=(d,s)=>{let o=n.setStyles(d,s);return s.height?()=>{}:o},f={transitionProperty:"height",transitionDuration:`${r}s`,transitionTimingFunction:"cubic-bezier(0.4, 0.0, 0.2, 1)"};t._x_transition={in(d=()=>{},s=()=>{}){u&&(t.hidden=!1),u&&(t.style.display=null);let o=t.getBoundingClientRect().height;t.style.height="auto";let a=t.getBoundingClientRect().height;o===a&&(o=h),n.transition(t,n.setStyles,{during:f,start:{height:o+"px"},end:{height:a+"px"}},()=>t._x_isShown=!0,()=>{Math.abs(t.getBoundingClientRect().height-a)<1&&(t.style.overflow=null)})},out(d=()=>{},s=()=>{}){let o=t.getBoundingClientRect().height;n.transition(t,c,{during:f,start:{height:o+"px"},end:{height:h+"px"}},()=>t.style.overflow="hidden",()=>{t._x_isShown=!1,t.style.height==`${h}px`&&u&&(t.style.display="none",t.hidden=!0)})}}}}function l(n,e,t){if(n.indexOf(e)===-1)return t;let i=n[n.indexOf(e)+1];if(!i)return t;if(e==="duration"){let r=i.match(/([0-9]+)ms/);if(r)return r[1]}if(e==="min"){let r=i.match(/([0-9]+)px/);if(r)return r[1]}return i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(g)});})();

Binary file not shown.

5
staticfiles/js/alpinejs@3.14.8.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

146
staticfiles/js/base.js Normal file
View file

@ -0,0 +1,146 @@
/* global window, document, localStorage */
const $ = selector => Array.from(document.querySelectorAll(selector));
const $$ = selector => Array.from(document.querySelector(selector));
(() => {
"use strict";
/**
* Initialize the theme toggle button functionality.
* Toggles between 'dark' and 'light' themes and persists the state in localStorage.
*/
function initThemeToggle() {
const themeToggleButton = document.getElementById("theme-toggle-btn");
if (!themeToggleButton) return;
themeToggleButton.classList.toggle("btn-ghost", !("theme" in localStorage));
themeToggleButton.addEventListener("click", () => {
const documentRoot = document.documentElement;
const isSystemTheme = themeToggleButton.classList.contains("btn-ghost");
const isDarkTheme = documentRoot.classList.contains("dark");
const newTheme = isSystemTheme ? "dark" : (isDarkTheme ? "light" : "system");
if (newTheme === "system") {
documentRoot.classList.toggle("dark", window.matchMedia("(prefers-color-scheme: dark)").matches);
documentRoot.setAttribute("data-theme", window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
localStorage.removeItem("theme");
} else {
if (newTheme === "light") {
documentRoot.classList.remove("dark");
} else if (newTheme === "dark") {
documentRoot.classList.add("dark");
}
documentRoot.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}
themeToggleButton.classList.toggle("btn-ghost", newTheme === "system");
});
}
/**
* Initialize event listeners for forms containing multiselect fields.
* When the form is submitted, process each 'card-multiselect' to create hidden inputs.
*/
function initCardMultiselectHandling() {
const forms = document.querySelectorAll("form");
forms.forEach(form => {
if (form.querySelector("select.card-multiselect")) {
form.addEventListener("submit", () => {
processMultiselectForm(form);
});
}
});
}
/**
* Process multiselect fields within a form before submission by:
* - Creating hidden inputs for each selected option with value in 'card_id:quantity' format.
* - Removing the original name attribute to avoid duplicate submissions.
*
* @param {HTMLFormElement} form - The form element to process.
*/
function processMultiselectForm(form) {
const multiselectFields = form.querySelectorAll("select.card-multiselect");
multiselectFields.forEach(selectField => {
const originalFieldName =
selectField.getAttribute("data-original-name") || selectField.getAttribute("name");
if (!originalFieldName) return;
selectField.setAttribute("data-original-name", originalFieldName);
// Remove any previously generated hidden inputs for this multiselect.
form
.querySelectorAll(`input[data-generated-for-card-multiselect="${originalFieldName}"]`)
.forEach(input => input.remove());
// For each selected option, create a hidden input.
selectField.querySelectorAll("option:checked").forEach(option => {
const cardId = option.value;
const quantity = option.getAttribute("data-quantity") || "1";
const hiddenInput = document.createElement("input");
hiddenInput.type = "hidden";
hiddenInput.name = originalFieldName;
hiddenInput.value = `${cardId}:${quantity}`;
hiddenInput.setAttribute("data-generated-for-card-multiselect", originalFieldName);
form.appendChild(hiddenInput);
});
// Prevent the browser from submitting the select field directly.
selectField.removeAttribute("name");
});
}
/**
* Reset stale selections in all card multiselect fields.
* This is triggered on the window's 'pageshow' event to clear any lingering selections.
*/
function resetCardMultiselectState() {
const multiselectFields = document.querySelectorAll("select.card-multiselect");
multiselectFields.forEach(selectField => {
// Deselect all options.
selectField.querySelectorAll("option").forEach(option => {
option.selected = false;
});
// If the select field has an associated Choices.js instance, clear its selection.
if (selectField.choicesInstance) {
const activeSelections = selectField.choicesInstance.getValue(true);
if (activeSelections.length > 0) {
selectField.choicesInstance.removeActiveItemsByValue(activeSelections);
}
selectField.choicesInstance.setValue([]);
}
});
}
// On DOMContentLoaded, initialize theme toggling and form processing.
document.addEventListener("DOMContentLoaded", () => {
initThemeToggle();
initCardMultiselectHandling();
});
// On pageshow, only reset multiselect state if the page was loaded from bfcache.
window.addEventListener("pageshow", function(event) {
if (event.persisted) {
resetCardMultiselectState();
}
});
// Expose tradeOfferCard globally if not already defined.
if (!window.tradeOfferCard) {
window.tradeOfferCard = function() {
return {
flipped: false,
badgeExpanded: false,
acceptanceExpanded: false,
/**
* Update the badge's expanded state.
*
* @param {boolean} expanded - The new state of the badge.
*/
setBadge(expanded) {
this.badgeExpanded = expanded;
},
};
};
}
})();

BIN
staticfiles/js/base.js.gz Normal file

Binary file not shown.

2
staticfiles/js/choices.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

2
staticfiles/js/hovercards.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

123
staticfiles/js/tooltip.js Normal file
View file

@ -0,0 +1,123 @@
/**
* tooltip.js
*
* This script uses FloatingUI to create modern, styled tooltips for elements with the
* custom attribute "data-tooltip-html". The tooltips are styled using Tailwind CSS classes
* to support both light and dark themes and include a dynamically positioned arrow.
*
* Make sure the FloatingUIDOM global is available.
* For example, include in your base template:
* <script src="https://unpkg.com/@floating-ui/dom"></script>
*/
const { computePosition, offset, flip, shift, arrow } = FloatingUIDOM;
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-tooltip-html]').forEach((el) => {
let tooltipContainer = null;
let arrowElement = null;
let fadeOutTimeout;
const showTooltip = () => {
if (tooltipContainer) return; // Tooltip already visible
// Retrieve the custom HTML content from the data attribute
const tooltipContent = el.getAttribute('data-tooltip-html');
// Create a container for the tooltip (with modern styling)
tooltipContainer = document.createElement('div');
tooltipContainer.classList.add(
'bg-black', 'text-white',
'shadow-lg', 'rounded-lg', 'p-2',
// Transition classes for simple fade in/out
'transition-opacity', 'duration-200', 'opacity-0'
);
tooltipContainer.style.position = 'absolute';
tooltipContainer.style.zIndex = '9999';
// Set the HTML content for the tooltip
tooltipContainer.innerHTML = '<div class="p-2">' + tooltipContent + '</div>';
// Create the arrow element. The arrow is styled as a small rotated square.
arrowElement = document.createElement('div');
arrowElement.classList.add(
'w-3', 'h-3',
'bg-black',
'transform', 'rotate-45'
);
arrowElement.style.position = 'absolute';
// Append the arrow into the tooltip container
tooltipContainer.appendChild(arrowElement);
// Append the tooltip container to the document body
document.body.appendChild(tooltipContainer);
// Use Floating UI to position the tooltip, including the arrow middleware
computePosition(el, tooltipContainer, {
middleware: [
offset(8),
flip(),
shift({ padding: 5 }),
arrow({ element: arrowElement })
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(tooltipContainer.style, {
left: `${x}px`,
top: `${y}px`
});
// Position the arrow using the arrow middleware data
const { x: arrowX, y: arrowY } = middlewareData.arrow || {};
// Reset any previous inline values
arrowElement.style.left = '';
arrowElement.style.top = '';
arrowElement.style.right = '';
arrowElement.style.bottom = '';
// Adjust the arrow's position according to the placement
if (placement.startsWith('top')) {
arrowElement.style.bottom = '-4px';
arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%';
} else if (placement.startsWith('bottom')) {
arrowElement.style.top = '-4px';
arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%';
} else if (placement.startsWith('left')) {
arrowElement.style.right = '-4px';
arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%';
} else if (placement.startsWith('right')) {
arrowElement.style.left = '-4px';
arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%';
}
});
// Trigger a fade-in by moving from opacity-0 to opacity-100
requestAnimationFrame(() => {
tooltipContainer.classList.remove('opacity-0');
tooltipContainer.classList.add('opacity-100');
});
};
const hideTooltip = () => {
if (tooltipContainer) {
tooltipContainer.classList.remove('opacity-100');
tooltipContainer.classList.add('opacity-0');
// Remove the tooltip from the DOM after the transition duration
fadeOutTimeout = setTimeout(() => {
if (tooltipContainer && tooltipContainer.parentNode) {
tooltipContainer.parentNode.removeChild(tooltipContainer);
}
tooltipContainer = null;
arrowElement = null;
}, 200); // Matches the duration-200 class (200ms)
}
};
// Attach event listeners to show/hide the tooltip
el.addEventListener('mouseenter', showTooltip);
el.addEventListener('mouseleave', hideTooltip);
el.addEventListener('focus', showTooltip);
el.addEventListener('blur', hideTooltip);
});
});

Binary file not shown.