build fixes and static files fix, closes #28
This commit is contained in:
parent
bff2525c65
commit
6a44ef30a3
26 changed files with 91 additions and 39 deletions
48
static/css/base.css
Normal file
48
static/css/base.css
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
[x-cloak] { display: none !important; }
|
||||
|
||||
/* Beta Badge */
|
||||
#navbar-logo::after {
|
||||
content: 'BETA';
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--color-base-content);
|
||||
background-color: var(--color-base-300);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gravatar-hovercard .gravatar-hovercard__inner {
|
||||
background-color: var(--color-base-100) !important;
|
||||
border-color: var(--color-base-300) !important;
|
||||
color: var(--color-base-content) !important;
|
||||
}
|
||||
|
||||
.gravatar-hovercard .gravatar-hovercard__inner,
|
||||
.gravatar-hovercard .gravatar-hovercard__header-image,
|
||||
.gravatar-hovercard .gravatar-hovercard__header,
|
||||
.gravatar-hovercard .gravatar-hovercard__avatar-link,
|
||||
.gravatar-hovercard .gravatar-hovercard__avatar,
|
||||
.gravatar-hovercard .gravatar-hovercard__personal-info-plink,
|
||||
.gravatar-hovercard .gravatar-hovercard__name,
|
||||
.gravatar-hovercard .gravatar-hovercard__job,
|
||||
.gravatar-hovercard .gravatar-hovercard__location,
|
||||
.gravatar-hovercard .gravatar-hovercard__body,
|
||||
.gravatar-hovercard .gravatar-hovercard__description,
|
||||
.gravatar-hovercard .gravatar-hovercard__social-links,
|
||||
.gravatar-hovercard .gravatar-hovercard__buttons,
|
||||
.gravatar-hovercard .gravatar-hovercard__button,
|
||||
.gravatar-hovercard .gravatar-hovercard__button:hover,
|
||||
.gravatar-hovercard .gravatar-hovercard__footer,
|
||||
.gravatar-hovercard .gravatar-hovercard__profile-url,
|
||||
.gravatar-hovercard .gravatar-hovercard__profile-link,
|
||||
.gravatar-hovercard .gravatar-hovercard__profile-color {
|
||||
color: var(--color-base-content) !important;
|
||||
}
|
||||
|
||||
.gravatar-hovercard .gravatar-hovercard__location {
|
||||
color: var(--color-base-content) !important;
|
||||
}
|
||||
|
||||
.dark .gravatar-hovercard .gravatar-hovercard__social-icon {
|
||||
filter: invert(1) !important;
|
||||
}
|
||||
88
static/css/card-multiselect.css
Normal file
88
static/css/card-multiselect.css
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
select.card-multiselect {
|
||||
height: calc(var(--spacing) * 35);
|
||||
/*background-image: linear-gradient(45deg, #0000 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, #0000 50%); */
|
||||
background-image: none;
|
||||
}
|
||||
.choices.is-disabled .choices__inner,
|
||||
.choices.is-disabled .choices__input {
|
||||
background-color: var(--color-neutral);
|
||||
}
|
||||
.choices[data-type*=select-one] .choices__input {
|
||||
border-bottom: 1px solid var(--btn-shadow);
|
||||
background-color: var(--color-base-100);
|
||||
}
|
||||
.choices[data-type*=select-one] .choices__button:focus {
|
||||
box-shadow: 0 0 0 2px #005F75;
|
||||
}
|
||||
.choices__inner {
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid var(--btn-shadow);
|
||||
}
|
||||
.is-focused .choices__inner, .is-open .choices__inner {
|
||||
border-color: var(--btn-shadow);
|
||||
}
|
||||
.choices__list--multiple .choices__item {
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid var(--btn-shadow);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.choices__list--multiple .choices__item.is-highlighted {
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid var(--btn-shadow);
|
||||
}
|
||||
.is-disabled .choices__list--multiple .choices__item {
|
||||
background-color: var(--color-neutral);
|
||||
border: 1px solid var(--btn-shadow);
|
||||
}
|
||||
.choices__list--dropdown, .choices__list[aria-expanded] {
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid var(--btn-shadow);
|
||||
}
|
||||
.is-open .choices__list--dropdown, .is-open .choices__list[aria-expanded] {
|
||||
border-color: var(--btn-shadow);
|
||||
}
|
||||
.choices__list--dropdown .choices__item--selectable.is-highlighted, .choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid var(--btn-shadow);
|
||||
}
|
||||
.choices__heading {
|
||||
border-bottom: 1px solid var(--btn-shadow);
|
||||
color: var(--color-neutral);
|
||||
}
|
||||
.choices__input {
|
||||
background-color: var(--color-base-100);
|
||||
}
|
||||
.choices.select {
|
||||
height: inherit;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
.choices__inner {
|
||||
border: 1px solid var(--color-gray-500) !important;
|
||||
}
|
||||
.choices__list {
|
||||
border: none !important;
|
||||
}
|
||||
.choices__list--dropdown {
|
||||
border-left: 1px solid var(--color-gray-500) !important;
|
||||
border-right: 1px solid var(--color-gray-500) !important;
|
||||
border-bottom: 1px solid var(--color-gray-500) !important;
|
||||
border-top: none !important;
|
||||
}
|
||||
.choices.select[data-type*="select-one"]::after {
|
||||
display: none;
|
||||
}
|
||||
.choices__inner.bg-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
border: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.choices__item.mx-auto.w-max:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
.choices__input,
|
||||
.choices__input--cloned {
|
||||
width: 100% !important;
|
||||
}
|
||||
.choices__list--dropdown span.card-quantity-badge {
|
||||
display: none;
|
||||
}
|
||||
1
static/css/choices.min.css
vendored
Normal file
1
static/css/choices.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
static/css/hovercards.min.css
vendored
Normal file
3
static/css/hovercards.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/images/favicon.ico
Normal file
BIN
static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 549 B |
1
static/js/alpinejs.collapse@3.14.8.min.js
vendored
Normal file
1
static/js/alpinejs.collapse@3.14.8.min.js
vendored
Normal 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)});})();
|
||||
5
static/js/alpinejs@3.14.8.min.js
vendored
Normal file
5
static/js/alpinejs@3.14.8.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
146
static/js/base.js
Normal file
146
static/js/base.js
Normal 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([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all elements with the 'marquee' class.
|
||||
* For each element, if its content is overflowing (using isElementOverflowing),
|
||||
* wrap its innerHTML within a <marquee> tag and remove the 'marquee' class.
|
||||
*/
|
||||
function processMarqueeElements() {
|
||||
document.querySelectorAll('.marquee-calc').forEach(element => {
|
||||
if (element.offsetWidth >= 148 || element.offsetWidth < element.scrollWidth) {
|
||||
element.innerHTML = '<marquee behavior="scroll" direction="left" scrolldelay="80">' + element.innerHTML + '</marquee>';
|
||||
}
|
||||
|
||||
element.classList.remove('marquee-calc');
|
||||
});
|
||||
}
|
||||
|
||||
// Expose processMarqueeElements to be available for AJAX-loaded partial updates.
|
||||
window.processMarqueeElements = processMarqueeElements;
|
||||
|
||||
// On DOMContentLoaded, initialize theme toggling, form processing, and marquee wrapping.
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initThemeToggle();
|
||||
initCardMultiselectHandling();
|
||||
processMarqueeElements();
|
||||
});
|
||||
|
||||
// On pageshow, only reset multiselect state if the page was loaded from bfcache.
|
||||
window.addEventListener("pageshow", function(event) {
|
||||
if (event.persisted) {
|
||||
resetCardMultiselectState();
|
||||
}
|
||||
});
|
||||
})();
|
||||
219
static/js/card-multiselect.js
Normal file
219
static/js/card-multiselect.js
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters = function() {
|
||||
const selects = document.querySelectorAll('.card-multiselect');
|
||||
|
||||
// Rebuild global selections and rarity filtering.
|
||||
const globalSelectedIds = [];
|
||||
let globalRarity = null;
|
||||
|
||||
selects.forEach(select => {
|
||||
const selectedValues = select.choicesInstance ? select.choicesInstance.getValue(true) : [];
|
||||
selectedValues.forEach(cardId => {
|
||||
if (cardId && globalSelectedIds.indexOf(cardId) === -1) {
|
||||
globalSelectedIds.push(cardId);
|
||||
}
|
||||
});
|
||||
if (selectedValues.length > 0 && globalRarity === null) {
|
||||
const option = select.querySelector('option[value="${selectedValues[0]}"]');
|
||||
if (option) {
|
||||
globalRarity = option.getAttribute('data-rarity');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
selects.forEach(select => {
|
||||
if (select.choicesInstance && select.choicesInstance.dropdown.element) {
|
||||
// Reset all options to enabled.
|
||||
select.querySelectorAll('option').forEach(function(option) {
|
||||
option.disabled = false;
|
||||
});
|
||||
// Reset all items to visible.
|
||||
select.choicesInstance.dropdown.element.querySelectorAll('[data-card-id]').forEach(function(item) {
|
||||
item.style.display = '';
|
||||
});
|
||||
// Filter out options/items that do not match the global rarity.
|
||||
if (globalRarity) {
|
||||
select.querySelectorAll('option[data-rarity]:not([data-rarity="'+globalRarity+'"])').forEach(function(option) {
|
||||
option.disabled = true;
|
||||
});
|
||||
select.choicesInstance.dropdown.element.querySelectorAll('[data-rarity]:not([data-rarity="'+globalRarity+'"])').forEach(function(item) {
|
||||
item.style.display = 'none';
|
||||
});
|
||||
}
|
||||
// Filter out options/items that match the global selected card IDs.
|
||||
for (const cardId of globalSelectedIds) {
|
||||
select.choicesInstance.dropdown.element.querySelectorAll('[data-card-id="' + cardId + '"]').forEach(function(item) {
|
||||
item.style.display = 'none';
|
||||
});
|
||||
select.querySelectorAll('option[data-card-id="'+cardId+'"]:not(option[selected])').forEach(function(option) {
|
||||
option.disabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.updateOptionQuantity) {
|
||||
window.updateOptionQuantity = function(item, quantity) {
|
||||
const cardId = item.getAttribute('data-card-id');
|
||||
const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]');
|
||||
if (option) {
|
||||
option.setAttribute('data-quantity', quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.getOptionQuantity) {
|
||||
window.getOptionQuantity = function(item) {
|
||||
const cardId = item.getAttribute('data-card-id');
|
||||
const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]');
|
||||
return option ? parseInt(option.getAttribute('data-quantity')) : "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const selectFields = document.querySelectorAll('.card-multiselect');
|
||||
selectFields.forEach(selectField => {
|
||||
const placeholder = selectField.getAttribute('data-placeholder') || '';
|
||||
|
||||
const choicesInstance = new Choices(selectField, {
|
||||
removeItemButton: false,
|
||||
placeholderValue: placeholder,
|
||||
searchEnabled: true,
|
||||
shouldSort: false,
|
||||
allowHTML: true,
|
||||
closeDropdownOnSelect: true,
|
||||
removeItemButton: true,
|
||||
searchFields: ['label'],
|
||||
resetScrollPosition: false,
|
||||
callbackOnCreateTemplates: function(template) {
|
||||
const getCardContent = (data) => {
|
||||
let htmlContent = (data.element && data.element.getAttribute('data-html-content')) || data.label;
|
||||
let quantity = data.element.getAttribute('data-quantity');
|
||||
quantity = quantity ? parseInt(quantity) : 1;
|
||||
htmlContent = htmlContent.replace('__QUANTITY__', quantity);
|
||||
return htmlContent;
|
||||
};
|
||||
|
||||
const renderCard = (classNames, data, type) => {
|
||||
const rarity = data.element ? data.element.getAttribute('data-rarity') : '';
|
||||
const cardId = data.element ? data.element.getAttribute('data-card-id') : 0;
|
||||
const cardname = data.element ? data.element.getAttribute('data-name') : '';
|
||||
const content = getCardContent(data);
|
||||
if (type === 'item') {
|
||||
return template(`
|
||||
<div class="${classNames.item} mx-auto w-max ${data.highlighted ? classNames.highlightedState : ''} relative"
|
||||
data-id="${data.id}"
|
||||
data-card-id="${cardId}"
|
||||
data-item
|
||||
data-rarity="${rarity}"
|
||||
data-name="${cardname}"
|
||||
aria-selected="true"
|
||||
style="cursor: pointer; padding: 1rem;">
|
||||
<button type="button" class="decrement absolute left-[-1.5rem] top-1/2 transform -translate-y-1/2 bg-base-300 text-base-content px-2">-</button>
|
||||
<button type="button" class="increment absolute right-[-1.5rem] top-1/2 transform -translate-y-1/2 bg-base-300 text-base-content px-2">+</button>
|
||||
<div class="card-content">${content}</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
const extraAttributes = `data-select-text="${this.config.itemSelectText}" data-choice ${
|
||||
data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'
|
||||
}`;
|
||||
const extraClasses = classNames.itemChoice;
|
||||
return template(`
|
||||
<div class="${classNames.item} ${extraClasses} ${data.highlighted ? classNames.highlightedState : ''} mx-auto w-max"
|
||||
${extraAttributes}
|
||||
data-id="${data.id}"
|
||||
data-card-id="${cardId}"
|
||||
data-name="${cardname}"
|
||||
data-choice
|
||||
data-rarity="${rarity}"
|
||||
style="cursor: pointer;">
|
||||
${content}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
choice: function(classNames, data) {
|
||||
return renderCard(classNames, data, 'choice');
|
||||
},
|
||||
item: function(classNames, data) {
|
||||
return renderCard(classNames, data, 'item');
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Associate the Choices instance with the select field.
|
||||
selectField.choicesInstance = choicesInstance;
|
||||
|
||||
if (!window.cardMultiselectInstances) {
|
||||
window.cardMultiselectInstances = [];
|
||||
}
|
||||
window.cardMultiselectInstances.push(selectField);
|
||||
|
||||
selectField.addEventListener('change', function() {
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
});
|
||||
|
||||
if (choicesInstance.getValue(true).length > 0 && window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
|
||||
// Listen for increment/decrement clicks (scoped to the choices container).
|
||||
const choicesContainer = selectField.closest('.choices') || document;
|
||||
|
||||
choicesContainer.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('increment')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const container = e.target.closest('[data-item]');
|
||||
if (container) {
|
||||
let quantityBadge = container.querySelector('.card-quantity-badge');
|
||||
let quantity = window.getOptionQuantity(container);
|
||||
quantity = quantity + 1;
|
||||
quantityBadge.innerText = quantity;
|
||||
window.updateOptionQuantity(container, quantity);
|
||||
}
|
||||
}
|
||||
if (e.target.classList.contains('decrement')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const container = e.target.closest('[data-item]');
|
||||
if (container) {
|
||||
let quantityBadge = container.querySelector('.card-quantity-badge');
|
||||
let quantity = window.getOptionQuantity(container);
|
||||
const cardId = container.getAttribute('data-card-id');
|
||||
if (quantity === 1) {
|
||||
const option = selectField.querySelector('option[value="' + cardId + '"]');
|
||||
if (option) {
|
||||
choicesInstance.removeActiveItemsByValue(option.value);
|
||||
option.selected = false;
|
||||
}
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
} else {
|
||||
quantity = quantity - 1;
|
||||
quantityBadge.innerText = quantity;
|
||||
window.updateOptionQuantity(container, quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.target.closest('[data-item]') &&
|
||||
!e.target.classList.contains('increment') &&
|
||||
!e.target.classList.contains('decrement')) {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
2
static/js/choices.min.js
vendored
Normal file
2
static/js/choices.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/floating-ui_core@1.6.9.9.min.js
vendored
Normal file
1
static/js/floating-ui_core@1.6.9.9.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/floating-ui_dom@1.6.13.13.min.js
vendored
Normal file
1
static/js/floating-ui_dom@1.6.13.13.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
static/js/hovercards.min.js
vendored
Normal file
2
static/js/hovercards.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
123
static/js/tooltip.js
Normal file
123
static/js/tooltip.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue