refactor: complete professional redesign of dice roller modal for mobile
BREAKING CHANGES: Dice roller now uses modern ES6 class architecture Features: - Mobile-first CSS with fluid responsive units (clamp, min, max) - ES6 DiceModal class with proper state management (IDLE, ROLLING, SHOWING_RESULT) - Semantic HTML with ARIA attributes for accessibility - CSS state classes (.is-open, .is-closing, .is-animating) - Touch-friendly 44px minimum tap targets - Desktop enhancement with @media (min-width: 1001px) Fixes: - Fixed mobile viewport overflow with min-height: 0 on flex children - Reduced max-height to 70vh for guaranteed mobile fit - Removed all jQuery .show()/.hide() inline style injections - Removed !important CSS hacks from mobile media query - Fixed transparent modal background (80% opaque neutral gray) - Darkened backdrop overlay (85% opaque black) Technical: - Backdrop uses ::before pseudo-element (no wrapper div needed) - Flattened HTML structure with proper semantic elements - Backwards compatible wrapper functions preserved - Grid layout for inputs (stacked mobile, side-by-side desktop) - Proper CSS specificity hierarchy (no !important needed) - Removed .rpg-dice-popup-overlay div dependency Accessibility: - role="dialog" with aria-modal="true" - aria-labelledby for dialog title - aria-live regions for dynamic content - aria-busy for loading states - Proper focus management on open/close
This commit is contained in:
@@ -651,22 +651,174 @@ async function sendPlotProgression(type) {
|
||||
$('#rpg-plot-random, #rpg-plot-natural').prop('disabled', false).css('opacity', '1');
|
||||
}, 1000);
|
||||
}
|
||||
}/**
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern DiceModal ES6 Class
|
||||
* Manages dice roller modal with proper state management and CSS classes
|
||||
*/
|
||||
class DiceModal {
|
||||
constructor() {
|
||||
this.modal = document.getElementById('rpg-dice-popup');
|
||||
this.animation = document.getElementById('rpg-dice-animation');
|
||||
this.result = document.getElementById('rpg-dice-result');
|
||||
this.resultValue = document.getElementById('rpg-dice-result-value');
|
||||
this.resultDetails = document.getElementById('rpg-dice-result-details');
|
||||
this.rollBtn = document.getElementById('rpg-dice-roll-btn');
|
||||
|
||||
this.state = 'IDLE'; // IDLE, ROLLING, SHOWING_RESULT
|
||||
this.isAnimating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the modal with proper animation
|
||||
*/
|
||||
open() {
|
||||
if (this.isAnimating) return;
|
||||
|
||||
// Apply theme
|
||||
const theme = extensionSettings.theme;
|
||||
this.modal.setAttribute('data-theme', theme);
|
||||
|
||||
// Apply custom theme if needed
|
||||
if (theme === 'custom') {
|
||||
this._applyCustomTheme();
|
||||
}
|
||||
|
||||
// Reset to initial state
|
||||
this._setState('IDLE');
|
||||
|
||||
// Open modal with CSS class
|
||||
this.modal.classList.add('is-open');
|
||||
this.modal.classList.remove('is-closing');
|
||||
|
||||
// Focus management
|
||||
this.modal.querySelector('#rpg-dice-popup-close')?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the modal with animation
|
||||
*/
|
||||
close() {
|
||||
if (this.isAnimating) return;
|
||||
|
||||
this.isAnimating = true;
|
||||
this.modal.classList.add('is-closing');
|
||||
this.modal.classList.remove('is-open');
|
||||
|
||||
// Wait for animation to complete
|
||||
setTimeout(() => {
|
||||
this.modal.classList.remove('is-closing');
|
||||
this.isAnimating = false;
|
||||
|
||||
// Clear pending roll
|
||||
pendingDiceRoll = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the rolling animation
|
||||
*/
|
||||
startRolling() {
|
||||
this._setState('ROLLING');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the result
|
||||
* @param {number} total - The total roll value
|
||||
* @param {Array<number>} rolls - Individual roll values
|
||||
*/
|
||||
showResult(total, rolls) {
|
||||
this._setState('SHOWING_RESULT');
|
||||
|
||||
// Update result values
|
||||
this.resultValue.textContent = total;
|
||||
this.resultValue.classList.add('is-animating');
|
||||
|
||||
// Remove animation class after it completes
|
||||
setTimeout(() => {
|
||||
this.resultValue.classList.remove('is-animating');
|
||||
}, 500);
|
||||
|
||||
// Show details if multiple rolls
|
||||
if (rolls && rolls.length > 1) {
|
||||
this.resultDetails.textContent = `Rolls: ${rolls.join(', ')}`;
|
||||
} else {
|
||||
this.resultDetails.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages modal state changes
|
||||
* @private
|
||||
*/
|
||||
_setState(newState) {
|
||||
this.state = newState;
|
||||
|
||||
switch (newState) {
|
||||
case 'IDLE':
|
||||
this.rollBtn.hidden = false;
|
||||
this.animation.hidden = true;
|
||||
this.result.hidden = true;
|
||||
break;
|
||||
|
||||
case 'ROLLING':
|
||||
this.rollBtn.hidden = true;
|
||||
this.animation.hidden = false;
|
||||
this.result.hidden = true;
|
||||
this.animation.setAttribute('aria-busy', 'true');
|
||||
break;
|
||||
|
||||
case 'SHOWING_RESULT':
|
||||
this.rollBtn.hidden = true;
|
||||
this.animation.hidden = true;
|
||||
this.result.hidden = false;
|
||||
this.animation.setAttribute('aria-busy', 'false');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies custom theme colors
|
||||
* @private
|
||||
*/
|
||||
_applyCustomTheme() {
|
||||
const content = this.modal.querySelector('.rpg-dice-popup-content');
|
||||
if (content && extensionSettings.customColors) {
|
||||
content.style.setProperty('--rpg-bg', extensionSettings.customColors.bg);
|
||||
content.style.setProperty('--rpg-accent', extensionSettings.customColors.accent);
|
||||
content.style.setProperty('--rpg-text', extensionSettings.customColors.text);
|
||||
content.style.setProperty('--rpg-highlight', extensionSettings.customColors.highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
let diceModal = null;
|
||||
|
||||
/**
|
||||
* Sets up the dice roller functionality.
|
||||
*/
|
||||
function setupDiceRoller() {
|
||||
// Initialize DiceModal instance
|
||||
diceModal = new DiceModal();
|
||||
// Click dice display to open popup
|
||||
$('#rpg-dice-display').on('click', function() {
|
||||
openDicePopup();
|
||||
});
|
||||
|
||||
// Close popup
|
||||
$('#rpg-dice-popup-close, .rpg-dice-popup-overlay').on('click', function() {
|
||||
// Discard pending roll without saving
|
||||
pendingDiceRoll = null;
|
||||
// Close popup - handle both close button and backdrop clicks
|
||||
$('#rpg-dice-popup-close').on('click', function() {
|
||||
closeDicePopup();
|
||||
});
|
||||
|
||||
// Close on backdrop click (clicking outside content)
|
||||
$('#rpg-dice-popup').on('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDicePopup();
|
||||
}
|
||||
});
|
||||
|
||||
// Roll dice button
|
||||
$('#rpg-dice-roll-btn').on('click', async function() {
|
||||
await rollDice();
|
||||
@@ -709,54 +861,45 @@ function clearDiceRoll() {
|
||||
|
||||
/**
|
||||
* Opens the dice rolling popup.
|
||||
* Backwards compatible wrapper for DiceModal class.
|
||||
*/
|
||||
function openDicePopup() {
|
||||
// Apply current theme to popup
|
||||
const theme = extensionSettings.theme;
|
||||
$('#rpg-dice-popup').attr('data-theme', theme);
|
||||
|
||||
$('#rpg-dice-popup').fadeIn(200);
|
||||
$('#rpg-dice-animation').hide();
|
||||
$('#rpg-dice-result').hide();
|
||||
$('#rpg-dice-roll-btn').show();
|
||||
|
||||
// Apply custom theme if selected
|
||||
if (theme === 'custom') {
|
||||
applyCustomThemeToPopup();
|
||||
if (diceModal) {
|
||||
diceModal.open();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies custom theme colors to the dice popup.
|
||||
* Closes the dice rolling popup.
|
||||
* Backwards compatible wrapper for DiceModal class.
|
||||
*/
|
||||
function applyCustomThemeToPopup() {
|
||||
const $popup = $('#rpg-dice-popup');
|
||||
$popup.find('.rpg-dice-popup-content').css({
|
||||
'--rpg-bg': extensionSettings.customColors.bg,
|
||||
'--rpg-accent': extensionSettings.customColors.accent,
|
||||
'--rpg-text': extensionSettings.customColors.text,
|
||||
'--rpg-highlight': extensionSettings.customColors.highlight
|
||||
});
|
||||
function closeDicePopup() {
|
||||
if (diceModal) {
|
||||
diceModal.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dice rolling popup.
|
||||
* @deprecated Legacy function - use diceModal._applyCustomTheme() instead
|
||||
*/
|
||||
function closeDicePopup() {
|
||||
$('#rpg-dice-popup').fadeOut(200);
|
||||
function applyCustomThemeToPopup() {
|
||||
if (diceModal) {
|
||||
diceModal._applyCustomTheme();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls the dice and displays result.
|
||||
* Refactored to use DiceModal class.
|
||||
*/
|
||||
async function rollDice() {
|
||||
if (!diceModal) return;
|
||||
|
||||
const count = parseInt(String($('#rpg-dice-count').val())) || 1;
|
||||
const sides = parseInt(String($('#rpg-dice-sides').val())) || 20;
|
||||
|
||||
// Hide roll button and show animation
|
||||
$('#rpg-dice-roll-btn').hide();
|
||||
$('#rpg-dice-animation').show();
|
||||
$('#rpg-dice-result').hide();
|
||||
// Start rolling animation
|
||||
diceModal.startRolling();
|
||||
|
||||
// Wait for animation (simulate rolling)
|
||||
await new Promise(resolve => setTimeout(resolve, 1200));
|
||||
@@ -777,16 +920,8 @@ async function rollDice() {
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Hide animation and show result
|
||||
$('#rpg-dice-animation').hide();
|
||||
$('#rpg-dice-result').show();
|
||||
$('#rpg-dice-result-value').text(total);
|
||||
|
||||
if (rolls.length > 1) {
|
||||
$('#rpg-dice-result-details').text(`Rolls: ${rolls.join(', ')}`);
|
||||
} else {
|
||||
$('#rpg-dice-result-details').text('');
|
||||
}
|
||||
// Show result
|
||||
diceModal.showResult(total, rolls);
|
||||
|
||||
// Don't update sidebar display yet - only update when user clicks "Save Roll"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user