justbean/beango/js/beango.js
2025-04-17 21:49:04 -06:00

1437 lines
69 KiB
JavaScript

let currentItems = []; // Holds the original list of items provided by the user
let displayedItems = []; // Holds the items currently displayed on the board
let notificationTimeout = null; // To manage hiding notifications
// --- localStorage Keys ---
const LS_BOARD_SIZE = 'beango_boardSize';
const LS_CELL_ITEMS = 'beango_cellItems'; // Original items from textarea/file
const LS_DISPLAYED_ITEMS = 'beango_displayedItems'; // Items currently shown on the board
const LS_MARKED_INDICES = 'beango_markedIndices';
const LS_CONFIG_OPEN = 'beango_configOpen'; // Changed from minimized
const LS_BACKGROUND_TYPE = 'beango_backgroundType'; // 'solid' or 'gradient'
const LS_SOLID_COLOR = 'beango_solidColor';
const LS_SOLID_COLOR_OPACITY = 'beango_solidColorOpacity'; // New
const LS_GRADIENT_COLOR_1 = 'beango_gradientColor1';
const LS_GRADIENT_COLOR_1_OPACITY = 'beango_gradientColor1Opacity'; // New
const LS_GRADIENT_COLOR_2 = 'beango_gradientColor2';
const LS_GRADIENT_COLOR_2_OPACITY = 'beango_gradientColor2Opacity'; // New
const LS_GRADIENT_DIRECTION = 'beango_gradientDirection';
const LS_ORIGINAL_ITEMS = 'beango_originalItems'; // User's raw input
const LS_HEADER_TEXT = 'beango_headerText';
const LS_HEADER_IMAGE_URL = 'beango_headerImageUrl';
const LS_HEADER_TEXT_COLOR = 'beango_headerTextColor';
const LS_HEADER_TEXT_COLOR_OPACITY = 'beango_headerTextColorOpacity'; // New
const LS_MARKED_COLOR = 'beango_markedColor';
const LS_MARKED_COLOR_OPACITY = 'beango_markedColorOpacity'; // New (replaces LS_MARKED_OPACITY)
const LS_MARKED_IMAGE_URL = 'beango_markedImageUrl';
const LS_MARKED_IMAGE_OPACITY = 'beango_markedImageOpacity'; // New
const LS_CELL_BORDER_COLOR = 'beango_cellBorderColor';
const LS_CELL_BORDER_OPACITY = 'beango_cellBorderOpacity'; // New
const LS_CELL_BG_COLOR = 'beango_cellBgColor';
const LS_CELL_BG_OPACITY = 'beango_cellBgOpacity'; // New
const LS_CELL_BG_IMAGE_URL = 'beango_cellBgImageUrl';
const LS_CELL_BG_IMAGE_OPACITY = 'beango_cellBgImageOpacity'; // New
const LS_MARKED_BORDER_COLOR = 'beango_markedBorderColor';
const LS_MARKED_BORDER_OPACITY = 'beango_markedBorderOpacity'; // New
const LS_BOARD_BG_COLOR = 'beango_boardBgColor';
const LS_BOARD_BG_COLOR_OPACITY = 'beango_boardBgColorOpacity'; // New (replaces LS_BOARD_BG_OPACITY)
const LS_BOARD_BG_IMAGE_URL = 'beango_boardBgImageUrl';
// --- Default Values ---
const DEFAULT_SOLID_COLOR = '#ff7e5f'; // Default to first color of gradient
const DEFAULT_SOLID_COLOR_OPACITY = 100; // New
const DEFAULT_GRADIENT_COLOR_1 = '#ff7e5f'; // From main page gradient
const DEFAULT_GRADIENT_COLOR_1_OPACITY = 100; // New
const DEFAULT_GRADIENT_COLOR_2 = '#feb47b'; // From main page gradient
const DEFAULT_GRADIENT_COLOR_2_OPACITY = 100; // New
const DEFAULT_GRADIENT_DIRECTION = '135deg'; // From main page gradient
const DEFAULT_HEADER_TEXT = 'Beango!'; // REVERTED default back to Beango!
const DEFAULT_HEADER_IMAGE_URL = ''; // No default image
const DEFAULT_HEADER_TEXT_COLOR = '#15803d'; // Tailwind green-700 (approx)
const DEFAULT_HEADER_TEXT_COLOR_OPACITY = 100; // New
const DEFAULT_MARKED_COLOR = '#fde047'; // Default yellow
const DEFAULT_MARKED_COLOR_OPACITY = 80; // New (replaces DEFAULT_MARKED_OPACITY)
const DEFAULT_MARKED_IMAGE_URL = '';
const DEFAULT_MARKED_IMAGE_OPACITY = 100; // New
const DEFAULT_CELL_BORDER_COLOR = '#8B4513'; // Default brown
const DEFAULT_CELL_BORDER_OPACITY = 100; // New
const DEFAULT_CELL_BG_COLOR = '#F5F5DC'; // Default beige
const DEFAULT_CELL_BG_OPACITY = 100; // New
const DEFAULT_CELL_BG_IMAGE_URL = ''; // Default no image
const DEFAULT_CELL_BG_IMAGE_OPACITY = 100; // New
const DEFAULT_MARKED_BORDER_COLOR = '#ca8a04'; // Default darker yellow/orange (Tailwind yellow-600)
const DEFAULT_MARKED_BORDER_OPACITY = 100; // New
const DEFAULT_BOARD_BG_COLOR = '#ffffff'; // Default white
const DEFAULT_BOARD_BG_COLOR_OPACITY = 100; // New (replaces DEFAULT_BOARD_BG_OPACITY)
const DEFAULT_BOARD_BG_IMAGE_URL = ''; // Default no image
// --- Notification Function ---
function showNotification(message, type = 'info', duration = 3000) {
const notificationArea = document.getElementById('notification-area');
if (!notificationArea) return;
// Clear any existing timeout
if (notificationTimeout) {
clearTimeout(notificationTimeout);
}
notificationArea.textContent = message;
// Apply Tailwind classes based on type
notificationArea.className = 'fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 px-4 py-2 rounded shadow-lg text-white text-sm transition-opacity duration-300'; // Reset classes
switch (type) {
case 'success':
notificationArea.classList.add('bg-green-500');
break;
case 'warning':
notificationArea.classList.add('bg-yellow-500');
break;
case 'error':
notificationArea.classList.add('bg-red-500');
break;
default: // info
notificationArea.classList.add('bg-blue-500');
break;
}
// Make it visible
notificationArea.classList.add('opacity-100');
// Set timeout to hide
notificationTimeout = setTimeout(() => {
notificationArea.classList.remove('opacity-100');
notificationArea.classList.add('opacity-0');
}, duration);
}
// --- Helper Function ---
function hexToRgba(hex, opacityPercent = 100) {
// Ensure opacityPercent is a number before using it
let numericOpacity = parseInt(opacityPercent, 10); // Try parsing
// If parsing failed (e.g., input was not a number string) or input was null/undefined, default to 100
if (isNaN(numericOpacity)) {
numericOpacity = 100;
}
// Remove hash if it exists
hex = hex.replace('#', '');
// Handle short hex codes
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
// Ensure hex is 6 digits
if (hex.length !== 6) {
console.warn(`Invalid hex color: ${hex}. Using fallback.`);
hex = '000000'; // Default to black on error
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Clamp and normalize opacity using the correctly parsed/defaulted value
const clampedOpacityPercent = Math.max(0, Math.min(100, numericOpacity));
const opacity = clampedOpacityPercent / 100;
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
// --- Functions ---
// --- Function to set the HTML background (solid or gradient) ---
function setBackground() {
const backgroundType = document.querySelector('input[name="background-type"]:checked').value;
const solidColorPicker = document.getElementById('background-color-picker');
const gradientColor1Picker = document.getElementById('gradient-color-1');
const gradientColor2Picker = document.getElementById('gradient-color-2');
const gradientDirectionSelect = document.getElementById('gradient-direction');
const htmlStyle = document.documentElement.style;
let backgroundValue = '';
if (backgroundType === 'solid') {
const color = solidColorPicker.value;
const opacity = parseInt(document.getElementById('background-color-opacity-slider').value, 10);
backgroundValue = hexToRgba(color, opacity);
htmlStyle.background = backgroundValue; // Set solid color as background
htmlStyle.backgroundRepeat = 'no-repeat';
htmlStyle.backgroundAttachment = 'fixed';
} else { // gradient
const color1 = gradientColor1Picker.value;
const opacity1 = parseInt(document.getElementById('gradient-color-1-opacity-slider').value, 10);
const color2 = gradientColor2Picker.value;
const opacity2 = parseInt(document.getElementById('gradient-color-2-opacity-slider').value, 10);
const direction = gradientDirectionSelect.value;
const rgba1 = hexToRgba(color1, opacity1);
const rgba2 = hexToRgba(color2, opacity2);
let gradientValue;
if (direction === 'radial') {
gradientValue = `radial-gradient(circle, ${rgba1}, ${rgba2})`;
} else { // Handles 'deg' and direction keywords
gradientValue = `linear-gradient(${direction}, ${rgba1}, ${rgba2})`;
}
backgroundValue = gradientValue;
htmlStyle.background = gradientValue;
htmlStyle.backgroundRepeat = 'no-repeat';
htmlStyle.backgroundAttachment = 'fixed';
}
// htmlStyle.background = backgroundValue; // Already set inside if/else
return backgroundValue; // Return the CSS value for injection (might need adjustment?)
}
// --- Function to manage visibility of background controls ---
function toggleBackgroundControls() {
const backgroundType = document.querySelector('input[name="background-type"]:checked').value;
const solidSettings = document.getElementById('solid-color-settings');
const gradientSettings = document.getElementById('gradient-color-settings');
if (backgroundType === 'solid') {
solidSettings.style.display = 'block';
gradientSettings.style.display = 'none';
} else {
solidSettings.style.display = 'none';
gradientSettings.style.display = 'block';
}
}
// --- Save current background settings to localStorage ---
function saveBackgroundSettings() {
const backgroundType = document.querySelector('input[name="background-type"]:checked').value;
localStorage.setItem(LS_BACKGROUND_TYPE, backgroundType);
if (backgroundType === 'solid') {
localStorage.setItem(LS_SOLID_COLOR, document.getElementById('background-color-picker').value);
localStorage.setItem(LS_SOLID_COLOR_OPACITY, document.getElementById('background-color-opacity-slider').value);
// Optionally remove gradient keys
localStorage.removeItem(LS_GRADIENT_COLOR_1);
localStorage.removeItem(LS_GRADIENT_COLOR_1_OPACITY);
localStorage.removeItem(LS_GRADIENT_COLOR_2);
localStorage.removeItem(LS_GRADIENT_COLOR_2_OPACITY);
localStorage.removeItem(LS_GRADIENT_DIRECTION);
} else {
localStorage.setItem(LS_GRADIENT_COLOR_1, document.getElementById('gradient-color-1').value);
localStorage.setItem(LS_GRADIENT_COLOR_1_OPACITY, document.getElementById('gradient-color-1-opacity-slider').value);
localStorage.setItem(LS_GRADIENT_COLOR_2, document.getElementById('gradient-color-2').value);
localStorage.setItem(LS_GRADIENT_COLOR_2_OPACITY, document.getElementById('gradient-color-2-opacity-slider').value);
localStorage.setItem(LS_GRADIENT_DIRECTION, document.getElementById('gradient-direction').value);
// Optionally remove solid key
localStorage.removeItem(LS_SOLID_COLOR);
localStorage.removeItem(LS_SOLID_COLOR_OPACITY);
}
}
function toggleConfig() {
const pane = document.getElementById('config-pane');
const isOpen = pane.classList.contains('config-pane-open');
if (isOpen) {
pane.classList.remove('config-pane-open');
pane.classList.add('config-pane-closed');
localStorage.setItem(LS_CONFIG_OPEN, 'false');
} else {
pane.classList.remove('config-pane-closed');
pane.classList.add('config-pane-open');
localStorage.setItem(LS_CONFIG_OPEN, 'true');
}
}
document.getElementById('file-input').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('cell-contents').value = e.target.result;
showNotification('File loaded. Click Generate/Update to use items.', 'success');
};
reader.onerror = function() {
showNotification('Error reading file.', 'error');
};
reader.readAsText(file);
}
});
function getItemsFromInput() {
const text = document.getElementById('cell-contents').value.trim();
return text ? text.split('\n').map(item => item.trim()).filter(item => item) : [];
}
// --- Save current marked style settings to localStorage ---
function saveMarkedStyleSettings() {
localStorage.setItem(LS_MARKED_COLOR, document.getElementById('marked-color-picker').value);
localStorage.setItem(LS_MARKED_COLOR_OPACITY, document.getElementById('marked-color-opacity-slider').value);
localStorage.setItem(LS_MARKED_IMAGE_URL, document.getElementById('marked-image-url-input').value);
localStorage.setItem(LS_MARKED_IMAGE_OPACITY, document.getElementById('marked-image-opacity-slider').value); // Save image opacity
// Save marked border color and its opacity
localStorage.setItem(LS_MARKED_BORDER_COLOR, document.getElementById('marked-border-color-picker').value);
localStorage.setItem(LS_MARKED_BORDER_OPACITY, document.getElementById('marked-border-opacity-slider').value);
}
// --- Apply saved marked styles to a cell ---
function applyMarkedCellStyle(cell) {
if (!cell) return;
const isMarked = cell.classList.contains('marked');
const color = localStorage.getItem(LS_MARKED_COLOR) || DEFAULT_MARKED_COLOR;
const colorOpacity = parseInt(localStorage.getItem(LS_MARKED_COLOR_OPACITY) || DEFAULT_MARKED_COLOR_OPACITY, 10);
const imageUrl = localStorage.getItem(LS_MARKED_IMAGE_URL) || DEFAULT_MARKED_IMAGE_URL;
const imageOpacity = parseInt(localStorage.getItem(LS_MARKED_IMAGE_OPACITY) || DEFAULT_MARKED_IMAGE_OPACITY, 10);
const borderColor = localStorage.getItem(LS_MARKED_BORDER_COLOR) || DEFAULT_MARKED_BORDER_COLOR;
const borderOpacity = parseInt(localStorage.getItem(LS_MARKED_BORDER_OPACITY) || DEFAULT_MARKED_BORDER_OPACITY, 10);
// Reset potentially conflicting styles before applying new ones
cell.style.backgroundColor = '';
cell.style.backgroundImage = '';
// Don't reset border color here, let applyCellStyle handle default if unmarked
if (isMarked) {
// Apply marked border color + opacity
cell.style.borderColor = hexToRgba(borderColor, borderOpacity);
// Always apply the background color + opacity
cell.style.backgroundColor = hexToRgba(color, colorOpacity);
// Apply background image if URL exists via custom property
if (imageUrl && imageUrl.trim() !== '') {
cell.style.setProperty('--bg-image-url', `url('${imageUrl}')`);
cell.style.setProperty('--bg-image-opacity', imageOpacity / 100);
cell.style.backgroundSize = 'cover';
cell.style.backgroundPosition = 'center center';
cell.style.backgroundRepeat = 'no-repeat';
} else {
// Explicitly clear background image custom property
cell.style.removeProperty('--bg-image-url');
}
} else {
// If unmarked, re-apply default styles (which resets bg color, bg image, border color)
applyCellStyle(cell);
}
}
// --- Re-apply styles to all currently marked cells ---
function refreshMarkedCellStyles() {
const markedCells = document.querySelectorAll('#bingo-board .bingo-cell.marked');
markedCells.forEach(cell => {
applyMarkedCellStyle(cell); // Re-apply based on current settings
});
}
// --- Save current header settings to localStorage ---
function saveHeaderSettings() {
localStorage.setItem(LS_HEADER_TEXT, document.getElementById('header-text-input').value);
localStorage.setItem(LS_HEADER_IMAGE_URL, document.getElementById('header-image-url-input').value);
localStorage.setItem(LS_HEADER_TEXT_COLOR, document.getElementById('header-text-color-picker').value);
localStorage.setItem(LS_HEADER_TEXT_COLOR_OPACITY, document.getElementById('header-text-color-opacity-slider').value);
}
// --- Function to update the header display ---
function updateHeaderDisplay() {
// Read text, applying default ONLY if the key is missing
let headerText = localStorage.getItem(LS_HEADER_TEXT);
if (headerText === null) { // Check if key exists
headerText = DEFAULT_HEADER_TEXT; // Apply default only if key missing
}
// Read image URL, applying default if missing OR empty (standard || pattern)
const headerImageUrl = localStorage.getItem(LS_HEADER_IMAGE_URL) || DEFAULT_HEADER_IMAGE_URL;
const headerContainer = document.getElementById("custom-header-content");
if (!headerContainer) return;
// Clear existing content
headerContainer.innerHTML = "";
let hasText = headerText && headerText.trim() !== "";
let hasCustomImage = headerImageUrl && headerImageUrl.trim() !== "";
// Add text if it exists
if (hasText) {
const h1 = document.createElement("h1");
h1.className = "text-4xl font-bold"; // Keep other styles
h1.textContent = headerText;
let headerTextColor = localStorage.getItem(LS_HEADER_TEXT_COLOR);
if (headerTextColor === null) { // Check if key exists before applying default
headerTextColor = DEFAULT_HEADER_TEXT_COLOR;
}
let headerTextOpacity = localStorage.getItem(LS_HEADER_TEXT_COLOR_OPACITY);
if (headerTextOpacity === null) {
headerTextOpacity = DEFAULT_HEADER_TEXT_COLOR_OPACITY;
}
h1.style.color = hexToRgba(headerTextColor, parseInt(headerTextOpacity, 10));
headerContainer.appendChild(h1);
}
// Add custom image if URL exists
if (hasCustomImage) {
const img = document.createElement("img");
img.src = headerImageUrl;
img.alt = "Custom Header Image";
img.className = "max-h-16 max-w-full object-contain"; // Adjust size as needed
img.onerror = () => {
img.remove(); // Remove broken image placeholder
showNotification("Could not load custom header image.", "warning");
// If there was no text either, maybe add bean?
if (!hasText && !document.getElementById("bean")) {
addDefaultBean(headerContainer);
}
};
headerContainer.appendChild(img);
}
// If there is NO text and NO custom image, add the default bean
else if (!hasText) {
addDefaultBean(headerContainer);
}
// Implicitly, if there IS text but NO custom image, nothing else is added here
// (the bean is not automatically added alongside text anymore unless specified by lack of custom image AND lack of text)
}
// Helper to add the default bean image
function addDefaultBean(container) {
const img = document.createElement('img');
img.id = 'bean';
img.src = '../bean.svg';
img.alt = 'Bean';
img.className = 'w-16 h-16 cursor-pointer'; // Use consistent size
img.onclick = explodeBeans;
container.appendChild(img);
}
// --- Save current default cell style settings to localStorage ---
function saveCellStyleSettings() {
localStorage.setItem(LS_CELL_BORDER_COLOR, document.getElementById('cell-border-color-picker').value);
localStorage.setItem(LS_CELL_BORDER_OPACITY, document.getElementById('cell-border-opacity-slider').value);
localStorage.setItem(LS_CELL_BG_COLOR, document.getElementById('cell-background-color-picker').value);
localStorage.setItem(LS_CELL_BG_OPACITY, document.getElementById('cell-background-opacity-slider').value);
localStorage.setItem(LS_CELL_BG_IMAGE_URL, document.getElementById('cell-background-image-url-input').value);
localStorage.setItem(LS_CELL_BG_IMAGE_OPACITY, document.getElementById('cell-background-image-opacity-slider').value); // Save image opacity
}
// --- Apply saved default cell styles to a cell ---
function applyCellStyle(cell) {
if (!cell || cell.classList.contains('marked')) return; // Only apply to non-marked cells
const borderColor = localStorage.getItem(LS_CELL_BORDER_COLOR) || DEFAULT_CELL_BORDER_COLOR;
const borderOpacity = parseInt(localStorage.getItem(LS_CELL_BORDER_OPACITY) || DEFAULT_CELL_BORDER_OPACITY, 10);
const bgColor = localStorage.getItem(LS_CELL_BG_COLOR) || DEFAULT_CELL_BG_COLOR;
const bgOpacity = parseInt(localStorage.getItem(LS_CELL_BG_OPACITY) || DEFAULT_CELL_BG_OPACITY, 10);
const bgImageUrl = localStorage.getItem(LS_CELL_BG_IMAGE_URL) || DEFAULT_CELL_BG_IMAGE_URL;
const bgImageOpacity = parseInt(localStorage.getItem(LS_CELL_BG_IMAGE_OPACITY) || DEFAULT_CELL_BG_IMAGE_OPACITY, 10);
cell.style.borderColor = hexToRgba(borderColor, borderOpacity);
cell.style.opacity = ''; // Reset direct opacity, handled by color
if (bgImageUrl) {
cell.style.setProperty('--bg-image-url', `url('${bgImageUrl}')`); // NEW: Set CSS custom property
cell.style.setProperty('--bg-image-opacity', bgImageOpacity / 100); // Set image opacity property
cell.style.backgroundSize = 'cover';
cell.style.backgroundPosition = 'center center';
cell.style.backgroundRepeat = 'no-repeat';
// Set fallback color with its specific opacity
cell.style.backgroundColor = hexToRgba(bgColor, bgOpacity);
} else {
// Clear image properties if no URL
cell.style.removeProperty('--bg-image-url'); // NEW: Remove CSS custom property
cell.style.removeProperty('--bg-image-opacity'); // Remove image opacity property
cell.style.backgroundColor = hexToRgba(bgColor, bgOpacity);
}
}
// --- Re-apply default styles to all non-marked cells ---
function refreshCellStyles() {
const cells = document.querySelectorAll('#bingo-board .bingo-cell:not(.marked)');
cells.forEach(cell => {
applyCellStyle(cell); // Re-apply based on current settings
});
}
// --- Save board background settings to localStorage ---
function saveBoardBgSettings() {
localStorage.setItem(LS_BOARD_BG_COLOR, document.getElementById('board-bg-color-picker').value);
localStorage.setItem(LS_BOARD_BG_IMAGE_URL, document.getElementById('board-bg-image-url-input').value);
localStorage.setItem(LS_BOARD_BG_COLOR_OPACITY, document.getElementById('board-bg-color-opacity-slider').value); // Use new ID & key
}
// --- Apply board background style ---
function applyBoardBgStyle() {
const container = document.getElementById('bingo-board-container');
if (!container) return;
const bgColor = localStorage.getItem(LS_BOARD_BG_COLOR) || DEFAULT_BOARD_BG_COLOR;
const bgImageUrl = localStorage.getItem(LS_BOARD_BG_IMAGE_URL) || DEFAULT_BOARD_BG_IMAGE_URL;
const opacityValue = parseInt(localStorage.getItem(LS_BOARD_BG_COLOR_OPACITY) || DEFAULT_BOARD_BG_COLOR_OPACITY, 10);
// Apply opacity - REMOVED direct opacity setting
// container.style.opacity = opacityValue;
// Apply background image or color
if (bgImageUrl) {
container.style.backgroundImage = `url('${bgImageUrl}')`;
container.style.backgroundSize = 'cover';
container.style.backgroundPosition = 'center center';
container.style.backgroundRepeat = 'no-repeat';
// Apply background color with its opacity as fallback / see-through
container.style.backgroundColor = hexToRgba(bgColor, opacityValue);
} else {
container.style.backgroundImage = 'none';
container.style.backgroundColor = hexToRgba(bgColor, opacityValue);
}
}
// Helper to save board state
function saveBoardState() {
const sizeInput = document.getElementById('board-size');
const size = sizeInput ? parseInt(sizeInput.value, 10) : 5; // Default to 5 if input missing
const markedIndices = getMarkedIndices();
const originalUserItems = getItemsFromInput(); // Get current text from textarea
localStorage.setItem(LS_BOARD_SIZE, size);
localStorage.setItem(LS_CELL_ITEMS, JSON.stringify(currentItems)); // Save items potentially on board (padded/sliced)
localStorage.setItem(LS_DISPLAYED_ITEMS, JSON.stringify(displayedItems)); // Save displayed items
localStorage.setItem(LS_MARKED_INDICES, JSON.stringify(markedIndices));
localStorage.setItem(LS_ORIGINAL_ITEMS, JSON.stringify(originalUserItems)); // Save user's raw input from textarea
saveBackgroundSettings(); // Save background settings from inputs
saveHeaderSettings(); // Save header settings from inputs
saveMarkedStyleSettings(); // Save marked cell style settings
saveCellStyleSettings(); // Save default cell style settings
saveBoardBgSettings(); // Save board background settings
}
function generateBoard() {
const sizeInput = document.getElementById('board-size');
const size = parseInt(sizeInput.value, 10);
if (isNaN(size) || size < 1) {
showNotification("Please enter a valid board size (minimum 1).", 'warning');
return;
}
sizeInput.value = size; // Ensure value reflects parsed int
// Get items from input and store them as the canonical list
const originalUserItems = getItemsFromInput();
// localStorage.setItem(LS_ORIGINAL_ITEMS, JSON.stringify(originalUserItems)); // Moved to saveBoardState
const requiredItems = size * size;
let itemsForBoard = [...originalUserItems]; // Start with a copy of the user's raw input
let notificationMessage = 'Board generated successfully!';
let notificationType = 'success';
if (itemsForBoard.length < requiredItems) {
const diff = requiredItems - itemsForBoard.length;
notificationMessage = `Warning: Needed ${requiredItems} items, found ${itemsForBoard.length}. Padding with ${diff} placeholder(s).`;
notificationType = 'warning';
for (let i = 0; i < diff; i++) {
itemsForBoard.push(`Placeholder ${i+1}`);
}
// currentItems will now hold the padded list for the board
currentItems = [...itemsForBoard];
} else if (itemsForBoard.length > requiredItems) {
notificationMessage = `Info: Found ${itemsForBoard.length} items, randomly selecting ${requiredItems}.`;
notificationType = 'info';
itemsForBoard = shuffleArray(itemsForBoard).slice(0, requiredItems);
// currentItems will now hold the selected subset for the board
currentItems = [...itemsForBoard];
} else {
// If exact number, currentItems are the same as originalUserItems
currentItems = [...originalUserItems];
}
// Save the items that are actually available for the board (potentially padded/sliced)
// localStorage.setItem(LS_CELL_ITEMS, JSON.stringify(currentItems)); // Moved to saveBoardState
// Shuffle the items specifically for display (use currentItems which has the right size)
displayedItems = shuffleArray([...currentItems]);
const board = document.getElementById("bingo-board");
board.innerHTML = ""; // Clear previous board
board.style.gridTemplateColumns = `repeat(${size}, minmax(0, 1fr))`;
displayedItems.forEach((item, index) => {
const cell = document.createElement("div");
cell.classList.add("bingo-cell", "cursor-pointer");
// cell.textContent = item; // OLD WAY
cell.dataset.index = index; // Add index for saving marks
cell.onclick = () => selectCell(cell);
applyCellStyle(cell); // Apply default styles upon creation
// NEW: Create span for text content
const textSpan = document.createElement("span");
textSpan.classList.add("bingo-cell-text");
textSpan.textContent = item;
cell.appendChild(textSpan);
board.appendChild(cell);
});
clearMarks(false); // Clear previous marks visually AND their styles, but don't save yet
equalizeCellSizes(); // Add this call
// saveBoardState(); // REMOVED first call - Save the newly generated board state (size, items, displayed, marks)
// Explicitly save background settings before reload - NO, save everything together
// saveBackgroundSettings();
// Get the new background value and inject a style override before reload
const newBackgroundValue = setBackground(); // Apply and get value
let overrideStyle = document.getElementById('background-override-style');
if (!overrideStyle) {
overrideStyle = document.createElement('style');
overrideStyle.id = 'background-override-style';
document.head.appendChild(overrideStyle);
}
// Use !important to try and force application during reload flash
overrideStyle.textContent = `html { background: ${newBackgroundValue} !important; background-repeat: no-repeat !important; background-attachment: fixed !important; }`;
showNotification(notificationMessage, notificationType);
// *** Save the complete final state just before reload ***
saveBoardState(); // Includes size, items, displayed, marks, original items, background, header, marked style
// Reload the page to ensure correct rendering based on saved state
location.reload();
// Reset global variables
currentItems = [];
displayedItems = [];
// Reset form inputs
document.getElementById('board-size').value = 5; // Default size
document.getElementById('cell-contents').value = '';
document.getElementById('file-input').value = ''; // Clear file input
// Reset background inputs to defaults
document.querySelector('input[name="background-type"][value="gradient"]').checked = true; // Default to gradient
document.getElementById('background-color-picker').value = DEFAULT_SOLID_COLOR;
document.getElementById('background-color-opacity-slider').value = DEFAULT_SOLID_COLOR_OPACITY;
document.getElementById('gradient-color-1').value = DEFAULT_GRADIENT_COLOR_1;
document.getElementById('gradient-color-1-opacity-slider').value = DEFAULT_GRADIENT_COLOR_1_OPACITY;
document.getElementById('gradient-color-2').value = DEFAULT_GRADIENT_COLOR_2;
document.getElementById('gradient-color-2-opacity-slider').value = DEFAULT_GRADIENT_COLOR_2_OPACITY;
document.getElementById('gradient-direction').value = DEFAULT_GRADIENT_DIRECTION;
toggleBackgroundControls(); // Ensure correct controls are visible
// Reset header inputs to defaults
document.getElementById('header-text-input').value = DEFAULT_HEADER_TEXT;
document.getElementById('header-image-url-input').value = DEFAULT_HEADER_IMAGE_URL;
document.getElementById('header-text-color-picker').value = DEFAULT_HEADER_TEXT_COLOR;
document.getElementById('header-text-color-opacity-slider').value = DEFAULT_HEADER_TEXT_COLOR_OPACITY;
updateHeaderDisplay(); // Apply default header display
// Reset marked style inputs to defaults
document.getElementById('marked-color-picker').value = DEFAULT_MARKED_COLOR;
document.getElementById('marked-color-opacity-slider').value = DEFAULT_MARKED_COLOR_OPACITY;
document.getElementById('marked-image-url-input').value = DEFAULT_MARKED_IMAGE_URL;
document.getElementById('marked-border-color-picker').value = DEFAULT_MARKED_BORDER_COLOR;
document.getElementById('marked-border-opacity-slider').value = DEFAULT_MARKED_BORDER_OPACITY;
refreshMarkedCellStyles(); // Apply default styles (which is none, effectively)
// Clear the board display
const boardHeader = document.getElementById('board-header');
if (boardHeader) boardHeader.style.maxWidth = '';
setBackground(); // Apply default background
showNotification('Settings and board reset.', 'success');
}
function randomizeBoard() {
const storedItemsRaw = localStorage.getItem(LS_CELL_ITEMS);
const sizeRaw = localStorage.getItem(LS_BOARD_SIZE);
if (!storedItemsRaw || !sizeRaw) {
showNotification("Cannot randomize: Board state not found. Please generate the board first.", 'warning');
return;
}
const storedItems = JSON.parse(storedItemsRaw);
const size = parseInt(sizeRaw, 10);
const requiredItems = size * size;
if (storedItems.length < requiredItems) {
showNotification(`Cannot randomize: Not enough items saved (${storedItems.length}/${requiredItems}). Please generate the board again.`, 'warning');
return;
}
// Ensure we use the correct set if placeholders were added or items were sliced
let itemsToShuffle = [...storedItems];
if (itemsToShuffle.length > requiredItems) {
// This case implies items were sliced during generation, which is stored in currentItems
// Randomizing should still use the items that *could* be on the board
itemsToShuffle = itemsToShuffle.slice(0, requiredItems);
} else if (itemsToShuffle.length < requiredItems) {
// This case shouldn't happen if generateBoard saved padded items
showNotification("Cannot randomize: Item count mismatch. Please regenerate the board.", 'error');
return;
}
displayedItems = shuffleArray([...itemsToShuffle]); // Shuffle the items for display
const board = document.getElementById("bingo-board");
const cells = board.querySelectorAll(".bingo-cell");
if (cells.length !== displayedItems.length) {
showNotification("Cannot randomize: Board size mismatch. Please generate the board again.", 'error');
return;
}
cells.forEach((cell, index) => {
// cell.textContent = displayedItems[index]; // OLD WAY
// NEW: Update or create text span
let textSpan = cell.querySelector(".bingo-cell-text");
if (!textSpan) { // Should exist, but create if missing
textSpan = document.createElement("span");
textSpan.classList.add("bingo-cell-text");
cell.innerHTML =
cell.appendChild(textSpan);
}
textSpan.textContent = displayedItems[index];
cell.classList.remove("marked"); // Clear marks visually
applyMarkedCellStyle(cell); // Reset marked styles (which also calls applyCellStyle)
});
clearMarks(false); // Clear visual marks
saveBoardState(); // Save the new randomized state (including cleared marks)
showNotification('Board randomized!', 'success');
}
function selectCell(cell) {
cell.classList.toggle("marked"); // Toggle the dedicated 'marked' class
applyMarkedCellStyle(cell); // Apply/remove styles based on new state and settings
saveBoardState(); // RE-ADD: Save updated marks immediately after click
}
function clearMarks(save = true) {
const cells = document.querySelectorAll('#bingo-board .bingo-cell');
cells.forEach(cell => {
if (cell.classList.contains('marked')) {
cell.classList.remove('marked');
applyMarkedCellStyle(cell); // Reset styles for this cell
}
// Ensure styles are reset even if class was somehow missing
applyCellStyle(cell); // Ensure default styles are correct after potential mark removal
});
if (save) {
saveBoardState(); // Save cleared marks
showNotification('Marks cleared.', 'info');
}
}
// Fisher-Yates (aka Knuth) Shuffle Algorithm
function shuffleArray(array) {
let currentIndex = array.length, randomIndex;
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
// Function to reset settings and clear board
function resetSettings() {
// Clear localStorage relevant ONLY to the board structure/content
localStorage.removeItem(LS_BOARD_SIZE);
localStorage.removeItem(LS_CELL_ITEMS); // The items configured for the board (padded/sliced)
localStorage.removeItem(LS_DISPLAYED_ITEMS); // The current layout
localStorage.removeItem(LS_MARKED_INDICES);
localStorage.removeItem(LS_ORIGINAL_ITEMS); // User's raw input
// DO NOT clear these:
// localStorage.removeItem(LS_CONFIG_OPEN);
// localStorage.removeItem(LS_BACKGROUND_TYPE);
// localStorage.removeItem(LS_SOLID_COLOR);
// localStorage.removeItem(LS_GRADIENT_COLOR_1);
// localStorage.removeItem(LS_GRADIENT_COLOR_1_OPACITY);
// localStorage.removeItem(LS_GRADIENT_COLOR_2);
// localStorage.removeItem(LS_GRADIENT_COLOR_2_OPACITY);
// localStorage.removeItem(LS_GRADIENT_DIRECTION);
// localStorage.removeItem(LS_HEADER_TEXT);
// localStorage.removeItem(LS_HEADER_IMAGE_URL);
// localStorage.removeItem(LS_MARKED_STYLE_TYPE);
// localStorage.removeItem(LS_MARKED_COLOR);
// localStorage.removeItem(LS_MARKED_IMAGE_URL);
// localStorage.removeItem(LS_MARKED_COLOR_OPACITY);
// localStorage.removeItem(LS_CELL_BORDER_COLOR); // Keep style on board reset
// localStorage.removeItem(LS_CELL_BORDER_OPACITY);
// localStorage.removeItem(LS_CELL_BG_COLOR);
// localStorage.removeItem(LS_CELL_BG_OPACITY);
// localStorage.removeItem(LS_CELL_BG_IMAGE_URL);
// Reset global variables for board content
currentItems = [];
displayedItems = [];
// Reset ONLY board-related form inputs
document.getElementById('board-size').value = 5; // Default size
document.getElementById('cell-contents').value = '';
document.getElementById('file-input').value = ''; // Clear file input
// DO NOT reset background inputs
// DO NOT reset header inputs
// DO NOT reset marked style inputs
// Clear the board display
const board = document.getElementById('bingo-board');
board.innerHTML = '<div class="bingo-cell">Board Reset. Generate a new board!</div>';
board.style.gridTemplateColumns = ''; // Clear grid style
board.style.gridAutoRows = ''; // Also clear gridAutoRows
// Reset container width to default
const boardContainer = document.getElementById('bingo-board-container');
const boardHeader = document.getElementById('board-header');
if (boardContainer) boardContainer.style.maxWidth = ''; // Let it resize naturally or via equalize
if (boardHeader) boardHeader.style.maxWidth = ''; // Let it resize naturally or via equalize
// Call equalize after potentially changing board content/size
equalizeCellSizes();
// DO NOT reset background - keep current style
// setBackground();
// DO NOT reset header display - keep current style
// updateHeaderDisplay();
// DO NOT refresh marked styles - keep current style
// refreshMarkedCellStyles();
showNotification('Board settings reset. Generate a new board to apply.', 'success');
}
// --- Helper to get indices of marked cells ---
function getMarkedIndices() {
const markedCells = document.querySelectorAll('#bingo-board .bingo-cell.marked');
const indices = [];
markedCells.forEach(cell => {
// Ensure dataset.index exists and is a valid number
if (cell.dataset && typeof cell.dataset.index !== 'undefined') {
const index = parseInt(cell.dataset.index, 10);
if (!isNaN(index)) {
indices.push(index);
}
} else {
console.warn('Marked cell found without a valid data-index:', cell);
}
});
return indices;
}
// --- Load State on Page Load ---
function loadFromLocalStorage() {
const savedSize = localStorage.getItem(LS_BOARD_SIZE) || "5"; // Default to 5
const savedItemsText = localStorage.getItem(LS_CELL_ITEMS);
const savedDisplayedItems = localStorage.getItem(LS_DISPLAYED_ITEMS);
const savedMarkedIndices = localStorage.getItem(LS_MARKED_INDICES);
const configIsOpen = localStorage.getItem(LS_CONFIG_OPEN) === "true";
const savedOriginalItemsText = localStorage.getItem(LS_ORIGINAL_ITEMS);
// Load header text, applying default ONLY if key is missing
let savedHeaderText = localStorage.getItem(LS_HEADER_TEXT);
if (savedHeaderText === null) { // Check if key exists
savedHeaderText = DEFAULT_HEADER_TEXT; // Apply default only if key missing
}
const savedHeaderImageUrl = localStorage.getItem(LS_HEADER_IMAGE_URL) || DEFAULT_HEADER_IMAGE_URL;
let savedHeaderTextColor = localStorage.getItem(LS_HEADER_TEXT_COLOR);
if (savedHeaderTextColor === null) {
savedHeaderTextColor = DEFAULT_HEADER_TEXT_COLOR;
}
const savedHeaderTextOpacity = localStorage.getItem(LS_HEADER_TEXT_COLOR_OPACITY) || DEFAULT_HEADER_TEXT_COLOR_OPACITY;
const savedMarkedColor = localStorage.getItem(LS_MARKED_COLOR) || DEFAULT_MARKED_COLOR;
const savedMarkedColorOpacity = localStorage.getItem(LS_MARKED_COLOR_OPACITY) || DEFAULT_MARKED_COLOR_OPACITY;
const savedMarkedImageUrl = localStorage.getItem(LS_MARKED_IMAGE_URL) || DEFAULT_MARKED_IMAGE_URL;
const savedMarkedImageOpacity = localStorage.getItem(LS_MARKED_IMAGE_OPACITY) || DEFAULT_MARKED_IMAGE_OPACITY;
console.log(`LOAD: LS_MARKED_IMAGE_OPACITY = ${localStorage.getItem(LS_MARKED_IMAGE_OPACITY)}, using: ${savedMarkedImageOpacity}`); // DEBUG LOG
const savedCellBorderColor = localStorage.getItem(LS_CELL_BORDER_COLOR) || DEFAULT_CELL_BORDER_COLOR;
const savedCellBorderOpacity = localStorage.getItem(LS_CELL_BORDER_OPACITY) || DEFAULT_CELL_BORDER_OPACITY;
const savedCellBgColor = localStorage.getItem(LS_CELL_BG_COLOR) || DEFAULT_CELL_BG_COLOR;
const savedCellBgOpacity = localStorage.getItem(LS_CELL_BG_OPACITY) || DEFAULT_CELL_BG_OPACITY;
const savedCellBgImageUrl = localStorage.getItem(LS_CELL_BG_IMAGE_URL) || DEFAULT_CELL_BG_IMAGE_URL;
const savedCellBgImageOpacity = localStorage.getItem(LS_CELL_BG_IMAGE_OPACITY) || DEFAULT_CELL_BG_IMAGE_OPACITY;
console.log(`LOAD: LS_CELL_BG_IMAGE_OPACITY = ${localStorage.getItem(LS_CELL_BG_IMAGE_OPACITY)}, using: ${savedCellBgImageOpacity}`); // DEBUG LOG
const savedMarkedBorderColor = localStorage.getItem(LS_MARKED_BORDER_COLOR) || DEFAULT_MARKED_BORDER_COLOR;
const savedMarkedBorderOpacity = localStorage.getItem(LS_MARKED_BORDER_OPACITY) || DEFAULT_MARKED_BORDER_OPACITY;
const savedBoardBgColor = localStorage.getItem(LS_BOARD_BG_COLOR) || DEFAULT_BOARD_BG_COLOR;
const savedBoardBgColorOpacity = localStorage.getItem(LS_BOARD_BG_COLOR_OPACITY) || DEFAULT_BOARD_BG_COLOR_OPACITY;
const savedBoardBgImageUrl = localStorage.getItem(LS_BOARD_BG_IMAGE_URL) || DEFAULT_BOARD_BG_IMAGE_URL;
// Restore Config Pane State
const pane = document.getElementById('config-pane');
if (configIsOpen) {
pane.classList.remove('config-pane-closed');
pane.classList.add('config-pane-open');
} else {
pane.classList.remove('config-pane-open');
pane.classList.add('config-pane-closed');
}
// Restore Config Inputs
const size = parseInt(savedSize, 10); // Parse size here
document.getElementById('board-size').value = savedSize;
// Restore Board only if essential data exists
if (savedDisplayedItems) {
try {
displayedItems = JSON.parse(savedDisplayedItems); // Restore displayed items array
// *** ADDED: Restore currentItems as well ***
if (savedItemsText) {
try {
currentItems = JSON.parse(savedItemsText);
if (!Array.isArray(currentItems)) {
console.warn('Loaded LS_CELL_ITEMS was not an array, resetting.');
currentItems = [];
}
} catch (e) {
showNotification('Error parsing saved cell item pool (LS_CELL_ITEMS). Board may not randomize correctly.', 'warning');
currentItems = []; // Reset on parse error
}
} else {
// If LS_CELL_ITEMS doesn't exist but LS_DISPLAYED_ITEMS does, it's an inconsistent state.
// We could try to reconstruct currentItems from displayedItems, but it might lack padding/slicing info.
// For now, log a warning and reset currentItems.
console.warn('Inconsistent state: LS_DISPLAYED_ITEMS exists but LS_CELL_ITEMS is missing. Randomization might fail.');
currentItems = [];
}
// *** END ADDED section ***
} catch (e) {
showNotification('Error parsing saved board state. Please generate again.', 'error');
localStorage.removeItem(LS_DISPLAYED_ITEMS);
localStorage.removeItem(LS_MARKED_INDICES);
displayedItems = [];
document.getElementById('bingo-board').innerHTML = '<div class="bingo-cell">Error loading board. Generate New.</div>';
return; // Stop board restore
}
const requiredItems = size * size;
// Check if the loaded data is consistent
if (!Array.isArray(displayedItems) || displayedItems.length !== requiredItems) {
showNotification("Saved board data mismatch. Please generate again.", 'warning');
localStorage.removeItem(LS_DISPLAYED_ITEMS);
localStorage.removeItem(LS_MARKED_INDICES);
document.getElementById('bingo-board').innerHTML = '<div class="bingo-cell">Data Mismatch. Generate New.</div>';
return; // Stop loading the board state
}
const board = document.getElementById('bingo-board');
board.innerHTML = ''; // Clear placeholder
board.style.gridTemplateColumns = `repeat(${size}, minmax(0, 1fr))`;
const markedIndices = savedMarkedIndices ? JSON.parse(savedMarkedIndices) : [];
displayedItems.forEach((item, index) => {
const cell = document.createElement('div');
cell.classList.add('bingo-cell', 'cursor-pointer');
// cell.textContent = item; // OLD WAY
cell.dataset.index = index; // Ensure index is set for loading marks correctly
cell.onclick = () => selectCell(cell);
applyCellStyle(cell); // Apply default cell style first
// NEW: Create span for text content
const textSpan = document.createElement("span");
textSpan.classList.add("bingo-cell-text");
textSpan.textContent = item;
cell.appendChild(textSpan);
if (markedIndices.includes(index)) {
cell.classList.add('marked'); // Apply saved mark using \'marked\' class
// Apply dynamic marked styles AFTER adding the class and default styles
applyMarkedCellStyle(cell);
}
board.appendChild(cell);
});
equalizeCellSizes(); // Add this call
} else {
// If no saved board state, show the default message
const board = document.getElementById('bingo-board');
// Check if board div exists and has no children before adding message
if (board && board.children.length === 0) {
board.innerHTML = '<div class="bingo-cell">Generate a board using the config panel!</div>';
}
}
// --- Restore Background Settings ---
const savedBackgroundType = localStorage.getItem(LS_BACKGROUND_TYPE) || 'gradient'; // Default to gradient
const savedSolidColor = localStorage.getItem(LS_SOLID_COLOR) || DEFAULT_SOLID_COLOR;
const savedSolidColorOpacity = localStorage.getItem(LS_SOLID_COLOR_OPACITY) || DEFAULT_SOLID_COLOR_OPACITY;
const savedGradientColor1 = localStorage.getItem(LS_GRADIENT_COLOR_1) || DEFAULT_GRADIENT_COLOR_1;
const savedGradientColor1Opacity = localStorage.getItem(LS_GRADIENT_COLOR_1_OPACITY) || DEFAULT_GRADIENT_COLOR_1_OPACITY;
const savedGradientColor2 = localStorage.getItem(LS_GRADIENT_COLOR_2) || DEFAULT_GRADIENT_COLOR_2;
const savedGradientColor2Opacity = localStorage.getItem(LS_GRADIENT_COLOR_2_OPACITY) || DEFAULT_GRADIENT_COLOR_2_OPACITY;
const savedGradientDirection = localStorage.getItem(LS_GRADIENT_DIRECTION) || DEFAULT_GRADIENT_DIRECTION;
// Set radio button
document.querySelector(`input[name="background-type"][value="${savedBackgroundType}"]`).checked = true;
// Set input values
document.getElementById('background-color-picker').value = savedSolidColor;
document.getElementById('background-color-opacity-slider').value = savedSolidColorOpacity;
document.getElementById('gradient-color-1').value = savedGradientColor1;
document.getElementById('gradient-color-1-opacity-slider').value = savedGradientColor1Opacity;
document.getElementById('gradient-color-2').value = savedGradientColor2;
document.getElementById('gradient-color-2-opacity-slider').value = savedGradientColor2Opacity;
document.getElementById('gradient-direction').value = savedGradientDirection;
// Show/hide controls and apply background
toggleBackgroundControls();
setBackground();
// --- End Restore Background Settings ---
// --- Restore Header Settings ---
document.getElementById('header-text-input').value = savedHeaderText;
document.getElementById('header-image-url-input').value = savedHeaderImageUrl;
document.getElementById('header-text-color-picker').value = savedHeaderTextColor;
document.getElementById('header-text-color-opacity-slider').value = savedHeaderTextOpacity;
if(document.getElementById('header-text-color-opacity-value')) document.getElementById('header-text-color-opacity-value').textContent = savedHeaderTextOpacity; // Update display span
updateHeaderDisplay(); // Apply the loaded header
// --- End Restore Header Settings ---
// --- Restore Marked Style Settings ---
document.getElementById('marked-color-picker').value = savedMarkedColor;
document.getElementById('marked-color-opacity-slider').value = savedMarkedColorOpacity;
if(document.getElementById('marked-color-opacity-value')) document.getElementById('marked-color-opacity-value').textContent = savedMarkedColorOpacity;
document.getElementById('marked-image-url-input').value = savedMarkedImageUrl;
const markedImageOpacitySlider = document.getElementById('marked-image-opacity-slider');
const markedImageOpacityValueSpan = document.getElementById('marked-image-opacity-value');
if (markedImageOpacitySlider) markedImageOpacitySlider.value = savedMarkedImageOpacity;
if (markedImageOpacityValueSpan) markedImageOpacityValueSpan.textContent = savedMarkedImageOpacity;
document.getElementById('marked-border-color-picker').value = savedMarkedBorderColor;
const markedBorderOpacitySlider = document.getElementById('marked-border-opacity-slider');
const markedBorderOpacityValueSpan = document.getElementById('marked-border-opacity-value');
if (markedBorderOpacitySlider) markedBorderOpacitySlider.value = savedMarkedBorderOpacity;
if (markedBorderOpacityValueSpan) markedBorderOpacityValueSpan.textContent = savedMarkedBorderOpacity;
refreshMarkedCellStyles(); // Apply the loaded marked styles
// --- End Restore Marked Style Settings ---
// --- Restore Default Cell Style Settings ---
document.getElementById('cell-border-color-picker').value = savedCellBorderColor;
const cellBorderOpacitySlider = document.getElementById('cell-border-opacity-slider');
const cellBorderOpacityValueSpan = document.getElementById('cell-border-opacity-value');
if (cellBorderOpacitySlider) cellBorderOpacitySlider.value = savedCellBorderOpacity;
if (cellBorderOpacityValueSpan) cellBorderOpacityValueSpan.textContent = savedCellBorderOpacity;
document.getElementById('cell-background-color-picker').value = savedCellBgColor;
const cellBackgroundOpacitySlider = document.getElementById('cell-background-opacity-slider');
const cellBackgroundOpacityValueSpan = document.getElementById('cell-background-opacity-value');
if (cellBackgroundOpacitySlider) cellBackgroundOpacitySlider.value = savedCellBgOpacity;
if (cellBackgroundOpacityValueSpan) cellBackgroundOpacityValueSpan.textContent = savedCellBgOpacity;
document.getElementById('cell-background-image-url-input').value = savedCellBgImageUrl;
const cellBackgroundImageOpacitySlider = document.getElementById('cell-background-image-opacity-slider');
const cellBackgroundImageOpacityValueSpan = document.getElementById('cell-background-image-opacity-value');
if (cellBackgroundImageOpacitySlider) cellBackgroundImageOpacitySlider.value = savedCellBgImageOpacity;
if (cellBackgroundImageOpacityValueSpan) cellBackgroundImageOpacityValueSpan.textContent = savedCellBgImageOpacity;
// Styles are applied during board creation loop above
// --- End Restore Default Cell Style Settings ---
// --- Restore Board Background Settings ---
document.getElementById('board-bg-color-picker').value = savedBoardBgColor;
document.getElementById('board-bg-image-url-input').value = savedBoardBgImageUrl;
document.getElementById('board-bg-color-opacity-slider').value = savedBoardBgColorOpacity;
const boardBgOpacitySlider = document.getElementById('board-bg-color-opacity-slider');
const boardBgOpacityValueSpan = document.getElementById('board-bg-color-opacity-value');
if (boardBgOpacitySlider) boardBgOpacitySlider.value = savedBoardBgColorOpacity;
if (boardBgOpacityValueSpan) boardBgOpacityValueSpan.textContent = savedBoardBgColorOpacity;
applyBoardBgStyle(); // Apply loaded style
// --- End Restore Board Background Settings ---
// Restore the textarea with the original user input if available
if (savedOriginalItemsText) {
try {
const originalItems = JSON.parse(savedOriginalItemsText);
document.getElementById('cell-contents').value = originalItems.join('\n');
} catch (e) {
showNotification('Error parsing saved original items.', 'error');
localStorage.removeItem(LS_ORIGINAL_ITEMS);
// Fallback to cell items if original fails
if (savedItemsText) {
try {
document.getElementById('cell-contents').value = JSON.parse(savedItemsText).join('\n');
} catch { /* ignore inner error */ }
}
}
} else if (savedItemsText) {
// If no original items saved, use the cell items as fallback for textarea
try {
document.getElementById('cell-contents').value = JSON.parse(savedItemsText).join('\n');
} catch (e) {
// Already handled loading currentItems above, just ensure textarea is cleared on error
document.getElementById('cell-contents').value = '';
}
}
// Update container width based on loaded size - REMOVED
// updateBoardContainerMaxWidth(size);
// updateBoardContainerMaxWidth(size, '#board-header');
// --- Add Event Listeners for Default Cell Style Controls ---
document.getElementById('cell-border-color-picker').addEventListener('input', () => {
saveCellStyleSettings();
refreshCellStyles(); // Update all non-marked cells
});
document.getElementById('cell-border-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('cell-border-opacity-value')) document.getElementById('cell-border-opacity-value').textContent = e.target.value;
saveCellStyleSettings();
refreshCellStyles(); // Update all non-marked cells
});
document.getElementById('cell-background-color-picker').addEventListener('input', () => {
saveCellStyleSettings();
refreshCellStyles();
});
document.getElementById('cell-background-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('cell-background-opacity-value')) document.getElementById('cell-background-opacity-value').textContent = e.target.value;
saveCellStyleSettings();
refreshCellStyles();
});
document.getElementById('cell-background-image-url-input').addEventListener('input', () => {
saveCellStyleSettings();
refreshCellStyles();
});
// --- Add Event Listeners for Board Background Controls ---
document.getElementById('board-bg-color-picker').addEventListener('input', () => {
saveBoardBgSettings();
applyBoardBgStyle();
});
document.getElementById('board-bg-image-url-input').addEventListener('input', () => {
saveBoardBgSettings();
applyBoardBgStyle();
});
document.getElementById('board-bg-color-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('board-bg-color-opacity-value')) document.getElementById('board-bg-color-opacity-value').textContent = e.target.value;
saveBoardBgSettings();
applyBoardBgStyle();
});
// --- Search Listener ---
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.trim().toLowerCase();
const cells = document.querySelectorAll('#bingo-board .bingo-cell');
// Split search term into words
const searchWords = searchTerm.split(/\s+/).filter(word => word.length > 0);
cells.forEach(cell => {
const textSpan = cell.querySelector('.bingo-cell-text');
let isMatch = false;
if (textSpan && searchWords.length > 0) {
const cellText = textSpan.textContent.trim().toLowerCase();
// Check if ALL search words are included in the cell text
isMatch = searchWords.every(word => cellText.includes(word));
}
// Add or remove highlight based on match status
if (isMatch) {
cell.classList.add('highlighted');
} else {
cell.classList.remove('highlighted');
}
});
});
}
// --- Other Listeners ---
window.addEventListener('resize', equalizeCellSizes);
equalizeCellSizes(); // Initial call after load
}
// --- Helper to manage board container width --- DEPRECATED
/* function updateBoardContainerMaxWidth(size, element = '#bingo-board-container') {
const container = document.querySelector(element);
if (!container) return;
// Remove existing max-w classes
const classList = container.classList;
const existingMaxWidthClasses = Array.from(classList).filter(cls => cls.startsWith('max-w-'));
classList.remove(...existingMaxWidthClasses);
// Determine and add the new max-w class
let maxWidthClass = 'max-w-2xl'; // Default for 5x5
if (size <= 3) {
maxWidthClass = 'max-w-lg';
} else if (size === 4) {
maxWidthClass = 'max-w-xl';
} else if (size === 5) {
maxWidthClass = 'max-w-2xl';
} else if (size === 6) {
maxWidthClass = 'max-w-3xl';
} else if (size === 7) {
maxWidthClass = 'max-w-4xl';
} else if (size >= 8) {
maxWidthClass = 'max-w-5xl'; // Cap here, rely on scroll for larger
}
container.classList.add(maxWidthClass);
} */
// --- Function to make all cells square and equal size ---
function equalizeCellSizes() {
const board = document.getElementById("bingo-board");
const cells = board?.querySelectorAll(".bingo-cell");
if (!board || !cells || cells.length === 0) return;
let size = 0;
const sizeInput = document.getElementById("board-size");
const sizeFromInput = sizeInput ? parseInt(sizeInput.value, 10) : 0;
if (sizeFromInput && !isNaN(sizeFromInput) && sizeFromInput > 0) {
size = sizeFromInput;
} else {
try {
const gridStyle = window.getComputedStyle(board).gridTemplateColumns;
const parts = gridStyle.split(" ");
if (parts.length > 0 && parts[0] !== "none") {
size = parts.length;
}
} catch (e) {
console.error(
"Could not determine grid size from styles or input.",
e
);
}
}
if (size <= 0) {
// Attempt to get size from a potentially existing grid setup if possible
try {
const gridStyle = window.getComputedStyle(board).gridTemplateColumns;
const parts = gridStyle.split(" ");
if (parts.length > 0 && parts[0] !== "none") {
size = parts.length;
}
} catch {
/* ignore */
}
if (size <= 0) {
console.error("Failed to determine grid size.");
showNotification("Cannot determine valid grid size. Please generate board again.", "warning");
return; // Cannot proceed
}
}
const gap = 4; // From CSS: .bingo-board { gap: 4px; }
const minCellSize = 100; // Minimum width/height for a cell
let maxWidthNeeded = 0;
let maxHeightNeeded = 0;
// 1. Reset grid sizing to allow natural measurement of cell content
board.style.gridTemplateColumns = `repeat(${size}, minmax(0, 1fr))`; // Use fractions initially
board.style.gridAutoRows = "auto"; // Auto height
// 2. Force reflow to apply the reset styles before measuring
void board.offsetHeight;
// 3. First pass: Find the max width and max height based on actual rendered content
cells.forEach((cell) => {
// Reset any explicit cell styles (shouldn\'t be needed, but safe)
cell.style.width = "";
cell.style.height = "";
// Measure the cell\'s natural dimensions after reflow
const width = cell.offsetWidth;
const height = cell.offsetHeight;
maxWidthNeeded = Math.max(maxWidthNeeded, width);
maxHeightNeeded = Math.max(maxHeightNeeded, height);
});
// 4. Determine the target size for square cells
const targetSize = Math.max(maxWidthNeeded, maxHeightNeeded, minCellSize);
// 5. Apply the calculated fixed size back to the grid
board.style.gridTemplateColumns = `repeat(${size}, ${targetSize}px)`;
board.style.gridAutoRows = `${targetSize}px`; // Make cells square
// 6. Calculate total board width needed for the container
const containerPadding = 32; // Based on p-4 class on #bingo-board-container (1rem = 16px, left + right = 32px)
const totalWidth = targetSize * size + gap * (size - 1) + containerPadding;
// 7. Update container max-width
const boardContainer = document.getElementById("bingo-board-container");
const boardHeader = document.getElementById("board-header");
if (boardContainer) {
boardContainer.style.maxWidth = `${totalWidth}px`;
}
if (boardHeader) {
// Keep header consistent with board container width
boardHeader.style.maxWidth = `${totalWidth}px`;
}
}
function explodeBeans() {
const container = document.querySelector('#custom-header-content');
for (let i = 0; i < 20; i++) {
const newBean = document.createElement('img');
newBean.src = '../bean.svg';
newBean.style.position = 'absolute';
newBean.style.width = '25px';
newBean.style.height = '25px';
newBean.style.transition = 'transform 1s ease-out, opacity 1s ease-out';
newBean.style.opacity = '0';
// Initial position at the center of the img tag
newBean.style.transform = 'translate(100px, 0) scale(0.5)';
container.appendChild(newBean);
// Random position around the original bean
const angle = Math.random() * 2 * Math.PI;
const distance = Math.random() * 100 + 50+100;
// Trigger the animation
setTimeout(() => {
newBean.style.opacity = '1';
newBean.style.transform = `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px) scale(1)`;
}, 0);
// Remove the bean after animation
setTimeout(() => {
newBean.style.opacity = '0';
setTimeout(() => {
container.removeChild(newBean);
}, 1000);
}, 1000);
}
}
// --- Consolidated Event Listeners ---
document.addEventListener('DOMContentLoaded', () => {
// Load state first thing
loadFromLocalStorage();
// --- Add Event Listeners for Background Controls ---
document.querySelectorAll('input[name="background-type"]').forEach(radio => {
radio.addEventListener('change', () => {
toggleBackgroundControls();
setBackground();
saveBackgroundSettings();
});
});
document.getElementById('background-color-picker').addEventListener('input', () => {
setBackground();
saveBackgroundSettings();
});
document.getElementById('background-color-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('background-color-opacity-value')) document.getElementById('background-color-opacity-value').textContent = e.target.value;
setBackground();
saveBackgroundSettings();
});
document.getElementById('gradient-color-1').addEventListener('input', () => {
setBackground();
saveBackgroundSettings();
});
document.getElementById('gradient-color-1-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('gradient-color-1-opacity-value')) document.getElementById('gradient-color-1-opacity-value').textContent = e.target.value;
setBackground();
saveBackgroundSettings();
});
document.getElementById('gradient-color-2').addEventListener('input', () => {
setBackground();
saveBackgroundSettings();
});
document.getElementById('gradient-color-2-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('gradient-color-2-opacity-value')) document.getElementById('gradient-color-2-opacity-value').textContent = e.target.value;
setBackground();
saveBackgroundSettings();
});
document.getElementById('gradient-direction').addEventListener('change', () => {
setBackground();
saveBackgroundSettings();
});
// --- Add Event Listeners for Header Controls ---
document.getElementById('header-text-input').addEventListener('input', () => {
saveHeaderSettings(); // Save the new text first
updateHeaderDisplay(); // Then update display from saved state
});
document.getElementById('header-image-url-input').addEventListener('input', () => {
saveHeaderSettings(); // Save the new URL first
updateHeaderDisplay(); // Then update display from saved state
});
document.getElementById('header-text-color-picker').addEventListener('input', () => {
saveHeaderSettings();
updateHeaderDisplay();
});
document.getElementById('header-text-color-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('header-text-color-opacity-value')) document.getElementById('header-text-color-opacity-value').textContent = e.target.value;
saveHeaderSettings();
updateHeaderDisplay();
});
// --- Add Event Listeners for Marked Style Controls ---
document.getElementById('marked-color-picker').addEventListener('input', () => {
saveMarkedStyleSettings();
refreshMarkedCellStyles();
});
document.getElementById('marked-color-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('marked-color-opacity-value')) document.getElementById('marked-color-opacity-value').textContent = e.target.value;
saveMarkedStyleSettings();
refreshMarkedCellStyles();
});
document.getElementById('marked-image-url-input').addEventListener('input', () => {
saveMarkedStyleSettings();
refreshMarkedCellStyles();
});
document.getElementById('marked-image-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('marked-image-opacity-value')) document.getElementById('marked-image-opacity-value').textContent = e.target.value;
saveMarkedStyleSettings();
refreshMarkedCellStyles();
});
document.getElementById('marked-border-color-picker').addEventListener('input', () => {
saveMarkedStyleSettings();
refreshMarkedCellStyles();
});
document.getElementById('marked-border-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('marked-border-opacity-value')) document.getElementById('marked-border-opacity-value').textContent = e.target.value;
saveMarkedStyleSettings();
refreshMarkedCellStyles();
});
// --- Add Event Listeners for Default Cell Style Controls ---
document.getElementById('cell-border-color-picker').addEventListener('input', () => {
saveCellStyleSettings();
refreshCellStyles(); // Update all non-marked cells
});
document.getElementById('cell-border-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('cell-border-opacity-value')) document.getElementById('cell-border-opacity-value').textContent = e.target.value;
saveCellStyleSettings();
refreshCellStyles(); // Update all non-marked cells
});
document.getElementById('cell-background-color-picker').addEventListener('input', () => {
saveCellStyleSettings();
refreshCellStyles();
});
document.getElementById('cell-background-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('cell-background-opacity-value')) document.getElementById('cell-background-opacity-value').textContent = e.target.value;
saveCellStyleSettings();
refreshCellStyles();
});
document.getElementById('cell-background-image-url-input').addEventListener('input', () => {
saveCellStyleSettings();
refreshCellStyles();
});
document.getElementById('cell-background-image-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('cell-background-image-opacity-value')) document.getElementById('cell-background-image-opacity-value').textContent = e.target.value;
saveCellStyleSettings();
refreshCellStyles();
});
// --- Add Event Listeners for Board Background Controls ---
document.getElementById('board-bg-color-picker').addEventListener('input', () => {
saveBoardBgSettings();
applyBoardBgStyle();
});
document.getElementById('board-bg-image-url-input').addEventListener('input', () => {
saveBoardBgSettings();
applyBoardBgStyle();
});
document.getElementById('board-bg-color-opacity-slider').addEventListener('input', (e) => {
if(document.getElementById('board-bg-color-opacity-value')) document.getElementById('board-bg-color-opacity-value').textContent = e.target.value;
saveBoardBgSettings();
applyBoardBgStyle();
});
// --- Other Listeners ---
window.addEventListener('resize', equalizeCellSizes);
equalizeCellSizes(); // Initial call after load
});