assets_js_controllers_member-mobile-card-menu-controller.js
import MobileControllerBase from "./mobile-controller-base.js";
/**
* MemberMobileCardMenu Stimulus Controller
*
* Manages mobile-optimized menu interface for PWA member cards.
* Extends MobileControllerBase for centralized connection handling.
*
* Features:
* - Floating action button (FAB) menu interface
* - Plugin-registered menu items with icons and badges
* - Online/offline state management
* - Expandable/collapsible menu system
*/
class MemberMobileCardMenu extends MobileControllerBase {
static targets = ["fab", "menu", "menuItem", "badge"]
static values = {
menuItems: String
}
initialize() {
super.initialize();
this.menuOpen = false;
this.items = [];
this.authCardUrl = null;
}
/**
* Called after base class connect
*/
onConnect() {
console.log("MemberMobileCardMenu connected");
this.loadMenuItems();
this.renderMenu();
// Register outside click handler
this._handleOutsideClick = this.bindHandler('outsideClick', this.handleOutsideClick);
document.addEventListener('click', this._handleOutsideClick);
document.addEventListener('touchstart', this._handleOutsideClick);
// Register connection status handler for auth card URL
this._handleConnectionStatus = this.bindHandler('connectionStatus', this.handleConnectionStatusEvent);
document.addEventListener('connection-status-changed', this._handleConnectionStatus);
// Update initial offline state
this.updateOfflineState();
}
/**
* Called when connection state changes (from base class)
*/
onConnectionStateChanged(isOnline) {
this.updateOfflineState();
}
/**
* Called after base class disconnect
*/
onDisconnect() {
document.removeEventListener('click', this._handleOutsideClick);
document.removeEventListener('touchstart', this._handleOutsideClick);
document.removeEventListener('connection-status-changed', this._handleConnectionStatus);
console.log("MemberMobileCardMenu disconnected");
}
/**
* Load menu items from JSON value
* Parses and validates plugin-registered menu items
*/
loadMenuItems() {
try {
if (this.menuItemsValue) {
this.items = JSON.parse(this.menuItemsValue);
// Sort by order
this.items.sort((a, b) => (a.order || 999) - (b.order || 999));
console.log("Loaded menu items:", this.items);
}
} catch (error) {
console.error("Error parsing menu items:", error);
this.items = [];
}
}
/**
* Render menu items into DOM
* Creates button elements for each menu item with icons and badges
*/
renderMenu() {
if (!this.hasMenuTarget || this.items.length === 0) {
return;
}
// Clear existing menu items
this.menuTarget.innerHTML = '';
// Create menu items
this.items.forEach(item => {
const menuItem = this.createMenuItem(item);
this.menuTarget.appendChild(menuItem);
});
}
/**
* Create menu item DOM element
* Generates button with icon, label, and optional badge
*
* @param {Object} item Menu item configuration
* @returns {HTMLElement} Menu item button element
*/
createMenuItem(item) {
const button = document.createElement('a');
button.href = item.url;
button.className = `btn btn-${item.color || 'primary'} btn-lg w-100 mb-2 d-flex align-items-center justify-content-between mobile-menu-item`;
button.setAttribute('data-member-mobile-card-menu-target', 'menuItem');
button.setAttribute('data-action', 'click->member-mobile-card-menu#closeMenu');
button.setAttribute('role', 'button');
button.setAttribute('aria-label', item.label);
// Store the item data for offline state management
button.dataset.itemLabel = item.label;
button.dataset.itemUrl = item.url;
// Create content wrapper
const content = document.createElement('span');
content.className = 'd-flex align-items-center';
// Add icon
if (item.icon) {
const icon = document.createElement('i');
icon.className = `bi ${item.icon} me-2`;
icon.setAttribute('aria-hidden', 'true');
content.appendChild(icon);
}
// Add label
const label = document.createElement('span');
label.textContent = item.label;
content.appendChild(label);
button.appendChild(content);
// Add badge if present
if (item.badge !== null && item.badge !== undefined && item.badge > 0) {
const badge = document.createElement('span');
badge.className = 'badge bg-danger rounded-pill';
badge.setAttribute('data-member-mobile-card-menu-target', 'badge');
badge.textContent = item.badge;
button.appendChild(badge);
}
return button;
}
/**
* Toggle menu open/closed state
* Handles FAB button click to show/hide menu
*
* @param {Event} event Click event
*/
toggleMenu(event) {
event.preventDefault();
if (this.menuOpen) {
this.closeMenu();
} else {
this.openMenu();
}
}
/**
* Open menu display
* Shows menu items with animation
*/
openMenu() {
if (!this.hasMenuTarget) return;
this.menuOpen = true;
this.menuTarget.hidden = false;
// Add animation class
this.menuTarget.classList.add('menu-opening');
// Update FAB appearance
if (this.hasFabTarget) {
this.fabTarget.classList.add('menu-active');
}
// Remove animation class after animation completes
setTimeout(() => {
this.menuTarget.classList.remove('menu-opening');
}, 300);
}
/**
* Close menu display
* Hides menu items with animation
*/
closeMenu() {
if (!this.hasMenuTarget) return;
this.menuOpen = false;
// Add closing animation class
this.menuTarget.classList.add('menu-closing');
// Update FAB appearance
if (this.hasFabTarget) {
this.fabTarget.classList.remove('menu-active');
}
// Hide after animation completes
setTimeout(() => {
this.menuTarget.hidden = true;
this.menuTarget.classList.remove('menu-closing');
}, 300);
}
/**
* Handle clicks outside menu to close it
*
* @param {Event} event Click event
*/
handleOutsideClick(event) {
if (!this.menuOpen) return;
// Check if click is outside menu and FAB
const clickedOutside = !this.element.contains(event.target);
if (clickedOutside) {
this.closeMenu();
}
}
/**
* Handle connection status event from PWA controller (for auth card URL)
*/
handleConnectionStatusEvent(event) {
this.authCardUrl = event.detail.authCardUrl;
this.updateOfflineState();
}
/**
* Update menu items based on offline state
* Uses base class online property instead of duplicate state
*/
updateOfflineState() {
if (!this.hasMenuItemTarget) return;
// Items that should remain enabled when offline
const offlineAllowedLabels = ['Auth Card', 'My RSVPs'];
this.menuItemTargets.forEach(item => {
const itemUrl = item.dataset.itemUrl;
const itemLabel = item.dataset.itemLabel;
// Check if this item should be allowed offline
const isAllowedOffline = offlineAllowedLabels.includes(itemLabel) ||
(this.authCardUrl && itemUrl && itemUrl.includes('viewMobileCard'));
if (!this.online && !isAllowedOffline) {
// Offline and not allowed - disable
item.classList.add('disabled');
item.style.opacity = '0.5';
item.style.pointerEvents = 'none';
item.setAttribute('aria-disabled', 'true');
} else {
// Online or allowed offline - enable
item.classList.remove('disabled');
item.style.opacity = '1';
item.style.pointerEvents = 'auto';
item.removeAttribute('aria-disabled');
}
});
}
}
// Register controller globally
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["member-mobile-card-menu"] = MemberMobileCardMenu;