Files
taxbaik/legacy/smartadmin/scripts/core/smartApp.js
T
kjh2064 40cffb3beb
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s
fix: implement Blazor-native login form to properly update authentication state
Problem: JavaScript login form saved tokens to localStorage but didn't notify
CustomAuthenticationStateProvider, causing [Authorize] pages to remain in
'loading' state indefinitely. The provider only reads tokens when:
1. GetAuthenticationStateAsync() is called (page load)
2. NotifyAuthenticationStateChanged() is triggered (UI updates)

But JavaScript login didn't trigger either, leaving the authentication state
stale.

Solution: Convert AdminLoginForm from HTML+JavaScript to pure Blazor component.
Now the login flow is:
1. User enters credentials in Blazor form
2. HttpClient POST to /api/auth/login
3. Save tokens to localStorage
4. Call CustomAuthenticationStateProvider.LoginAsync() directly
5. Blazor detects auth state change and re-evaluates [Authorize] pages
6. Dashboard [Authorize] page renders successfully

Result: Immediate authentication state update, no loading timeout on protected pages.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-03 13:03:53 +09:00

1830 lines
74 KiB
JavaScript

'use strict';
/* Initialize tooltips: bootstrap.bundle.js */
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
/* Initialize popovers: bootstrap.bundle.js */
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
/* Set default dropdown behavior: bootstrap.bundle.js */
bootstrap.Dropdown.Default.autoClose = 'outside';
// Run gobal scripts: after all other scripts are loaded
/* Initialize the navigation : smartNavigation.js */
let nav;
const navElement = document.querySelector('#js-primary-nav');
if (navElement) {
nav = new Navigation(navElement,
{
accordion: true,
slideUpSpeed: 350,
slideDownSpeed: 470,
closedSign: '<i class="sa sa-chevron-down"></i>',
openedSign: '<i class="sa sa-chevron-up"></i>',
initClass: 'js-nav-built',
debug: false,
instanceId: `nav-${Date.now()}`,
maxDepth: 5,
sanitize: true,
animationTiming: 'easeOutExpo',
debounceTime: 0,
onError: error => console.error('Navigation error:', error)
});
}
/* Waves Effect : waves.js */
if (window.Waves) {
Waves.attach('.btn:not(.js-waves-off):not(.btn-switch):not(.btn-panel):not(.btn-system):not([data-action="playsound"]), .js-waves-on', ['waves-themed']);
Waves.init();
}
/* Initialize the list filter : listFilter.js */
document.addEventListener('DOMContentLoaded', function () {
/* initialize smartApp.js */
appDOM.checkActiveStyles().debug(false);
/* Initialize the list filter */
var listFilter = new ListFilter('#js-nav-menu', '#searchInput',
{
messageSelector: '.js-filter-message',
debounceWait: 200,
minLength: 2,
caseSensitive: false,
onFilter: function (filter) {
console.log('Filtering with:', filter);
},
onReset: function () {
console.log('Filter reset');
}
});
/* Panel Sorting : sortable.js
Initialize Sortable for each column
turn off sortable by adding "sortable-off" class to main-content before init
this will however still load any saved state
remove !mobileOperator() to enable sortable on mobile */
const columns = document.querySelectorAll('.main-content:not(.sortable-off) > .row:not(.sortable-off) > [class^="col-"]');
/* Check if columns exist and Sortable is defined and mobileOperator is false */
if (columns.length > 0 && typeof Sortable !== 'undefined' && !mobileOperator()) {
/* Initialize Sortable for each column */
columns.forEach(column => {
Sortable.create(column,
{
animation: 150,
ghostClass: 'panel-selected',
handle: '.panel-hdr > h2',
filter: '.panel-locked',
draggable: '.panel:not(.panel-locked):not(.panel-fullscreen)',
group: 'sapanels',
onEnd: function () {
savePanelState();
}
});
});
/* Add class to app-content if sortable is active */
document.querySelector('.main-content').classList.add('sortable-active');
} else {
document.querySelector('.main-content').classList.add('sortable-inactive');
}
/* Initialize the custom scrollbar : smartSlimScroll.js */
/* Customized Scrollbar : smartSlimScroll.js */
/* Initialize smartSlimScroll if not on mobile - In mobile we use native scrollbar for better UX */
if (!mobileOperator()) {
/* Initialize smartSlimScroll */
new smartSlimScroll('.custom-scroll',
{
height: '100%',
size: '4px',
position: 'right',
color: '#000',
alwaysVisible: false,
railVisible: true,
railColor: '#222',
railOpacity: 0,
wheelStep: 10,
offsetX: '6px',
offsetY: '8px'
});
} else {
document.getElementsByTagName('BODY')[0].classList.add('no-slimscroll');
}
});
// appDOM - Enhanced Vanilla JavaScript Plugin for DOM Manipulation and UI Controls
(function (window) {
'use strict';
// Create global appDOM object
window.appDOM = (function () {
// Private variables and cache
const htmlRoot = document.documentElement;
const cache = {
actionButtons: null,
audioCache: new Map()
};
// Configuration object with security limits
const config = {
debug: false,
focusDelay: 200,
defaultSoundPath: 'media/sound/',
maxClassNameLength: 50,
maxSelectorLength: 255,
fullscreenConfirmMessage: 'Do you want to enter fullscreen mode?',
selectors: {
actionButtons: '[data-action]'
},
sound: {
preload: false,
volume: 1.0,
fadeIn: false,
fadeInDuration: 500
},
classes: {
classesToKeep: ['hide-page-scrollbar'] // Classes that won't be removed during reset
},
theme: {
styleId: 'theme-style',
defaultTheme: ''
}
};
// Main plugin object
const plugin = {
// Check if string contains substring
containsSubstring: function (string, substring) {
if (!string || !substring) {
logger.warn('Invalid parameters for containsSubstring');
return false;
}
const regex = new RegExp(`\\b${substring}\\b`, 'i');
return regex.test(string);
},
// Reset style function
resetStyle: function () {
logger.group('Reset Style');
const dataAction = document.querySelectorAll(config.selectors.actionButtons);
// Reset all action buttons
dataAction.forEach(element => {
if (element.type === 'checkbox') {
element.checked = false;
element.parentNode.classList.remove("active");
} else {
element.classList.remove("active");
}
});
// Keep specific classes while removing others
const currentClasses = htmlRoot.className.split(' ');
const filteredClasses = currentClasses.filter(className =>
config.classes.classesToKeep.includes(className)
);
htmlRoot.className = filteredClasses.join(' ');
// Reset theme toggle buttons' aria-pressed attribute to match current theme
// This should happen after resetSettings() since that might change the theme
resetSettings();
// After reset, check the current theme and update aria-pressed accordingly
const currentTheme = htmlRoot.getAttribute('data-bs-theme') || 'light';
const isDarkTheme = currentTheme === 'dark';
document.querySelectorAll('[data-action="toggle-theme"]').forEach(button => {
button.setAttribute('aria-pressed', isDarkTheme.toString());
});
// Remove the theme style element if it exists
const themeStyleElement = document.getElementById(config.theme.styleId);
if (themeStyleElement) {
themeStyleElement.remove();
}
// Reset radio buttons for theme selection
document.querySelectorAll('input[data-action="theme-style"]').forEach(radio => {
radio.checked = false;
});
logger.log('Styles reset completed');
logger.groupEnd();
return plugin;
}
};
// Enhanced Security utilities
const security = {
sanitizeClassName: function (className) {
if (typeof className !== 'string') {
logger.error('Invalid class name type');
return '';
}
// Length check to prevent DoS
if (className.length > config.maxClassNameLength) {
logger.error(`Class name exceeds maximum length of ${config.maxClassNameLength} characters`);
return '';
}
// Enhanced pattern to only allow valid CSS class characters
const sanitized = className.replace(/[^a-zA-Z0-9-_]/g, '')
.replace(/^[0-9-_]/, '')
.substring(0, config.maxClassNameLength);
if (sanitized !== className) {
logger.warn(`Class name "${className}" contained invalid characters and was sanitized to "${sanitized}"`);
}
return sanitized;
},
sanitizeSelector: function (selector) {
if (typeof selector !== 'string') {
logger.error('Invalid selector type');
return '';
}
// Length check to prevent DoS
if (selector.length > config.maxSelectorLength) {
logger.error(`Selector exceeds maximum length of ${config.maxSelectorLength} characters`);
return '';
}
// Enhanced CSS injection prevention
if (/<[^>]*>|javascript:|data:|@import|expression|url\(|eval\(|setTimeout|setInterval/i.test(selector)) {
logger.error('Potential malicious selector detected');
return '';
}
// Only allow valid CSS selectors and escape potentially dangerous characters
const sanitized = selector.replace(/[<>"'`=\/\\]/g, '')
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
.trim();
return sanitized;
},
validateDataAttribute: function (attribute) {
if (typeof attribute !== 'string') {
logger.error('Invalid data attribute type');
return '';
}
// Length check
if (attribute.length > config.maxClassNameLength) {
logger.error(`Data attribute exceeds maximum length of ${config.maxClassNameLength} characters`);
return '';
}
// Enhanced pattern for data attributes
return attribute.replace(/[^a-zA-Z0-9-_]/g, '')
.substring(0, config.maxClassNameLength);
},
sanitizeUrl: function (url) {
if (typeof url !== 'string') {
logger.error('Invalid URL type');
return '';
}
// Length check
if (url.length > 2000) { // Standard URL length limit
logger.error('URL exceeds maximum length');
return '';
}
// Simple validation for CSS files
const sanitized = url.replace(/[<>"'`]/g, '');
// Only allow CSS files and restrict to relative paths or same domain
if (!sanitized.endsWith('.css')) {
logger.error('URL must end with .css');
return '';
}
// Check for potential script injection
if (/javascript:|data:|file:|ftp:|@import|expression|eval\(|setTimeout|setInterval/i.test(sanitized)) {
logger.error('Potentially malicious URL detected');
return '';
}
return sanitized;
},
checkFullscreenPermission: function () {
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled)) {
logger.error('Fullscreen not supported in this browser');
return false;
}
if (window.self !== window.top) {
try {
if (!window.parent.document.fullscreenEnabled) {
logger.error('Fullscreen not allowed in iframe');
return false;
}
} catch (e) {
logger.error('Cannot access parent frame for fullscreen permission');
return false;
}
}
return true;
},
getClosestElement: function (element, selector) {
if (!element || !selector) {
logger.error('Invalid parameters for getClosestElement');
return null;
}
try {
return element.closest(selector);
} catch (error) {
logger.error(`Error finding closest element: ${error.message}`);
return null;
}
}
};
// Debug logger
const logger = {
log: function (msg) {
if (config.debug) console.log(`[SmartAdmin] ${msg}`);
},
error: function (msg) {
if (config.debug) console.error(`[SmartAdmin Error] ${msg}`);
},
warn: function (msg) {
if (config.debug) console.warn(`[SmartAdmin Warning] ${msg}`);
},
group: function (name) {
if (config.debug) console.group(`[SmartAdmin Group] ${name}`);
},
groupEnd: function () {
if (config.debug) console.groupEnd();
}
};
// Audio Helper Functions
const audioHelpers = {
sanitizeFilename: function (filename) {
return filename.replace(/[^\w.-]/g, '');
},
fadeInAudio: async function (audio, targetVolume, duration) {
audio.volume = 0;
const steps = 20;
const increment = targetVolume / steps;
const stepDuration = duration / steps;
for (let i = 0; i <= steps; i++) {
await new Promise(resolve => setTimeout(resolve, stepDuration));
audio.volume = Math.min(targetVolume, i * increment);
}
},
createAudioElement: function (path, sound, volume) {
const audio = new Audio();
audio.src = `${path}${sound}`;
audio.volume = volume;
return audio;
},
// Add new helper to stop all sounds except one
stopAllSoundsExcept: function (exceptSound) {
cache.audioCache.forEach((audio, key) => {
if (key !== exceptSound && !audio.paused) {
audio.pause();
audio.currentTime = 0;
// Find and update any elements playing this sound
const playingElements = document.querySelectorAll(`[data-audio-playing="${key}"]`);
playingElements.forEach(el => el.removeAttribute('data-audio-playing'));
}
});
}
};
// Utility functions
const utils = {
debounce: function (func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
validateElement: function (element, name) {
if (!element) {
logger.error(`${name} element not found`);
return false;
}
return true;
},
// Color extraction utilities
extractColors: function () {
logger.group('Color Extraction');
// example usage
//console.log(window.colorMap.primary[500].hex);
//console.log(window.colorMap.primary[500].rgb);
//console.log(window.colorMap.primary[500].rgba(0.5));
//console.log(window.colorMap.bootstrapVars.bodyColor.hex);
//console.log(window.colorMap.bootstrapVars.bodyColor.rgb);
//console.log(window.colorMap.bootstrapVars.bodyColor.rgba(0.5));
// Initialize color map if not exists
if (!window.colorMap) {
window.colorMap = {};
logger.log('Created new colorMap object');
} else {
logger.log('Using existing colorMap object');
}
const categories = ["primary", "danger", "success", "warning", "info"];
const shades = Array.from({ length: 17 }, (_, i) => (i + 1) * 50).filter(shade => shade <= 900);
const bootstrapVars = [
{ name: "bodyBg", style: "background-color: var(--bs-body-bg)" },
{ name: "bodyBgRgb", style: "background-color: rgb(var(--bs-body-bg-rgb))" },
{ name: "bodyColor", style: "color: var(--bs-body-color)" },
{ name: "bodyColorRgb", style: "color: rgb(var(--bs-body-color-rgb))" }
];
// Create temporary elements for color extraction
const ul = document.createElement("ul");
ul.style.display = "none";
// Add category and shade elements
categories.forEach(category => {
shades.forEach(shade => {
const li = document.createElement("li");
li.className = `bg-${category}-${shade}`;
ul.appendChild(li);
});
});
// Add bootstrap variable elements
bootstrapVars.forEach(varInfo => {
const li = document.createElement("li");
li.style.cssText = varInfo.style;
li.dataset.varName = varInfo.name;
ul.appendChild(li);
});
// Create container and append to body
const container = document.createElement("div");
container.id = "color-extraction-container";
container.appendChild(ul);
document.body.appendChild(container);
// Helper function to parse RGB string to object with values and formatted string
const parseRgb = (rgbString) => {
if (!rgbString || rgbString === "rgba(0, 0, 0, 0)" || rgbString === "transparent") {
return { r: 0, g: 0, b: 0, formatted: "rgb(0, 0, 0)", values: "0, 0, 0" };
}
const [r, g, b] = rgbString.match(/\d+/g).map(Number);
return {
r, g, b,
formatted: rgbString,
values: `${r}, ${g}, ${b}`
};
};
// Extract colors for categories and shades
categories.forEach(category => {
// Initialize category if it doesn't exist
if (!window.colorMap[category]) {
window.colorMap[category] = {};
}
shades.forEach(shade => {
const element = ul.querySelector(`.bg-${category}-${shade}`);
const bgColor = window.getComputedStyle(element).backgroundColor;
const rgbData = parseRgb(bgColor);
window.colorMap[category][shade] = {
hex: this.rgbToHex(bgColor),
rgb: rgbData.formatted,
rgba: (opacity = 1) => `rgba(${rgbData.values}, ${opacity})`,
values: rgbData.values
};
});
});
// Extract bootstrap variable colors
if (!window.colorMap.bootstrapVars) {
window.colorMap.bootstrapVars = {};
logger.log('Created bootstrapVars in colorMap');
} else {
logger.log('Updating existing bootstrapVars in colorMap');
}
bootstrapVars.forEach(varInfo => {
const element = ul.querySelector(`[data-var-name="${varInfo.name}"]`);
const property = varInfo.name.includes("bodyColor") ? "color" : "backgroundColor";
const colorValue = window.getComputedStyle(element)[property];
const rgbData = parseRgb(colorValue);
window.colorMap.bootstrapVars[varInfo.name] = {
hex: this.rgbToHex(colorValue),
rgb: rgbData.formatted,
rgba: (opacity = 1) => `rgba(${rgbData.values}, ${opacity})`,
values: rgbData.values
};
});
// Clean up
container.remove();
// Dispatch event when color map is ready
window.dispatchEvent(new Event("colorMapReady"));
logger.log('Color extraction completed');
logger.groupEnd();
},
rgbToHex: function (rgb) {
if (!rgb || rgb === "rgba(0, 0, 0, 0)" || rgb === "transparent") return null;
const [r, g, b] = rgb.match(/\d+/g).map(Number);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).padStart(6, '0')}`;
}
};
// Enhanced fullscreen handler
const fullscreenHandler = {
enter: function () {
try {
if (document.documentElement.requestFullscreen) {
return document.documentElement.requestFullscreen();
} else if (document.documentElement.webkitRequestFullscreen) {
return document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
return document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.msRequestFullscreen) {
return document.documentElement.msRequestFullscreen();
}
throw new Error('No fullscreen API available');
} catch (error) {
logger.error(`Fullscreen error: ${error.message}`);
return Promise.reject(error);
}
},
exit: function () {
try {
if (document.exitFullscreen) {
return document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
return document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
return document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
return document.msExitFullscreen();
}
throw new Error('No fullscreen API available');
} catch (error) {
logger.error(`Fullscreen exit error: ${error.message}`);
return Promise.reject(error);
}
},
isFullscreen: function () {
return !!(document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement);
}
};
// Function to update all chart colors after theme changes
function updateAllPluginColors() {
setTimeout(() => {
if (typeof window.rebuildApexCharts === 'function') {
//rebuild all apex charts
window.rebuildApexCharts();
}
if (typeof window.updateEasyPieCharts === 'function') {
//re-color any easy pie charts
window.updateEasyPieCharts();
}
if (typeof window.updateMiscPluginColors === 'function') {
//re-color any other page plugins
window.updateMiscPluginColors();
}
}, 150);
}
// Utility method to update aria-pressed on theme toggle buttons
plugin.updateThemeButtonsState = function () {
const currentTheme = htmlRoot.getAttribute('data-bs-theme') || 'light';
const isDarkTheme = currentTheme === 'dark';
document.querySelectorAll('[data-action="toggle-theme"]').forEach(button => {
button.setAttribute('aria-pressed', isDarkTheme.toString());
logger.log(`Updated aria-pressed to ${isDarkTheme} on theme toggle button`);
});
return plugin;
};
// Utility method to update theme style states
plugin.updateThemeStyleStates = function () {
logger.group('Updating Theme Style States');
const themeStyle = document.getElementById('theme-style');
if (!themeStyle || !themeStyle.getAttribute('href')) {
logger.log('No theme style element or href found');
logger.groupEnd();
return plugin;
}
const href = themeStyle.getAttribute('href');
const themeStyleName = href.split('/').pop().replace('.css', '').trim();
logger.log(`Current theme style: ${themeStyleName}`);
document.querySelectorAll('[data-theme-style]').forEach(element => {
const elementTheme = element.getAttribute('data-theme-style');
if (!elementTheme) return;
const isMatch = elementTheme === themeStyleName;
element.setAttribute('aria-selected', isMatch ? 'true' : 'false');
htmlRoot.classList.add('theme-active');
if (element.type === 'radio') {
element.checked = isMatch;
logger.log(`Radio button ${elementTheme}: checked=${isMatch}`);
} else {
if (isMatch) {
element.classList.add('active');
} else {
element.classList.remove('active');
}
logger.log(`Element ${elementTheme}: active=${isMatch}`);
}
});
logger.groupEnd();
return plugin;
};
// Expose extractColors in the public API
// usage: window.appDOM.extractColors()
plugin.extractColors = function () {
utils.extractColors();
return plugin;
};
// Theme Switcher Methods
plugin.setThemeStyle = function (themeUrl) {
logger.group('Setting Theme Style');
if (!themeUrl) {
logger.error('Empty theme URL provided');
logger.groupEnd();
return plugin;
}
const sanitizedUrl = security.sanitizeUrl(themeUrl);
if (!sanitizedUrl) {
logger.error('Invalid theme URL');
logger.groupEnd();
return plugin;
}
// Function to handle CSS loading and updates
function updateAfterCssLoad() {
logger.log('Theme CSS fully loaded');
// Update states after setting the style
plugin.updateThemeStyleStates();
// Update All Charts and plugin colors after CSS is loaded
logger.log('Updating plugin colors after CSS load');
plugin.updateAllPluginColors();
// Save to localStorage AFTER the CSS is fully loaded
if (typeof window.saveSettings === 'function') {
logger.log('Saving theme selection to local storage after CSS loaded');
window.saveSettings();
}
// Dispatch event that the theme has changed
const event = new CustomEvent('themeStyleChange', {
detail: { theme: sanitizedUrl }
});
document.dispatchEvent(event);
}
// Get the head element
const head = document.getElementsByTagName('head')[0];
const oldStyleElement = document.getElementById(config.theme.styleId);
// Check if we're trying to load the same CSS that's already loaded
if (oldStyleElement && oldStyleElement.getAttribute('href') === sanitizedUrl) {
logger.log('Theme CSS already loaded with this URL');
logger.groupEnd();
return plugin;
}
// Create a new style element
const newStyleElement = document.createElement('link');
newStyleElement.rel = 'stylesheet';
newStyleElement.media = 'screen';
// Special handling for the first theme change after page load
// Check if the old style element was loaded from localStorage
const loadedFromStorage = oldStyleElement && oldStyleElement.getAttribute('data-loaded-from-storage') === 'true';
// If this is the first theme change and it was loaded from storage,
// we can use the main ID immediately for better transition
const tempId = `${config.theme.styleId}-new`;
if (loadedFromStorage) {
logger.log('Handling initial theme that was loaded from storage');
newStyleElement.id = config.theme.styleId;
newStyleElement.href = sanitizedUrl;
// Set up the load handler before adding to DOM
newStyleElement.onload = function () {
// Remove storage flag as we're now managing it normally
newStyleElement.removeAttribute('data-loaded-from-storage');
// Remove the old stylesheet
if (oldStyleElement) {
oldStyleElement.remove();
logger.log('Initial storage-loaded CSS removed after new one loaded');
}
// Run the update function
updateAfterCssLoad();
};
// Replace the old element directly for initial load
if (oldStyleElement) {
head.replaceChild(newStyleElement, oldStyleElement);
} else {
head.appendChild(newStyleElement);
}
logger.log(`Initial theme replaced with: ${sanitizedUrl}`);
} else {
// Normal case - use temporary ID until loaded
newStyleElement.id = tempId;
newStyleElement.href = sanitizedUrl;
// Set up the load handler before adding to DOM
newStyleElement.onload = function () {
logger.log('New theme CSS loaded');
// Now that new CSS is loaded, rename it to the proper ID
newStyleElement.id = config.theme.styleId;
// Remove the old stylesheet if it exists (after new one is loaded)
if (oldStyleElement) {
oldStyleElement.remove();
logger.log('Old theme CSS removed');
}
// Now run the update function
updateAfterCssLoad();
};
// Add the new stylesheet to the DOM
head.appendChild(newStyleElement);
logger.log(`New theme link added: ${sanitizedUrl}`);
}
// Save to local storage if saveSettings is available
if (typeof window.saveSettings === 'function') {
logger.log('Saving theme selection to local storage');
window.saveSettings();
}
// Provide a fallback in case the onload event doesn't fire
setTimeout(() => {
// Look for either the temp element or a storage-loaded element that hasn't been updated
const temporaryElement = document.getElementById(tempId);
const storageElement = document.getElementById(config.theme.styleId);
if (temporaryElement) {
// Handle case where temp element exists but onload didn't fire
logger.log('CSS load timeout reached for temporary element, forcing update');
temporaryElement.id = config.theme.styleId;
if (oldStyleElement) {
oldStyleElement.remove();
}
updateAfterCssLoad();
} else if (storageElement && storageElement.getAttribute('data-loaded-from-storage') === 'true') {
// Handle case where storage element exists but onload didn't fire
logger.log('CSS load timeout reached for storage-loaded element, forcing update');
storageElement.removeAttribute('data-loaded-from-storage');
updateAfterCssLoad();
}
}, 2000);
logger.groupEnd();
return plugin;
};
// Check active styles
plugin.checkActiveStyles = function (target, data) {
logger.group('Checking Active Styles');
const classes = htmlRoot.className.split(" ") || target;
const dataAction = document.querySelectorAll(config.selectors.actionButtons) || data;
dataAction.forEach(element => {
if (element.type === 'checkbox') {
element.checked = false;
element.parentNode.classList.remove("active");
} else {
element.classList.remove("active");
}
});
classes.forEach(className => {
const sanitizedClass = security.sanitizeClassName(className);
const elements = document.querySelectorAll(`[data-class="${sanitizedClass}"]`);
elements.forEach(element => {
if (element.type === 'checkbox') {
element.checked = true;
element.parentNode.classList.add("active");
} else {
element.classList.add("active");
}
});
});
// Also check and update theme style selectors
const currentThemeLink = document.getElementById(config.theme.styleId);
if (currentThemeLink) {
plugin.updateThemeStyleStates();
}
logger.groupEnd();
return plugin;
};
// Make updateAllPluginColors function publicly accessible
plugin.updateAllPluginColors = function () {
updateAllPluginColors();
return plugin;
};
// Enhanced handlers with all implementations
const handlers = {
toggle: function (element) {
// Get and sanitize attributes
const dataClass = element.getAttribute('data-class');
// Validate dataClass exists before sanitizing
if (!dataClass) {
logger.error('Missing required data-class attribute for toggle action');
return;
}
const sanitizedClass = security.sanitizeClassName(dataClass);
const depClass = element.getAttribute('data-dependency');
const coDepClass = element.getAttribute('data-codependence');
const inputFocus = element.getAttribute('data-focus');
logger.group('Toggle Action');
logger.log(`Toggling class: ${sanitizedClass}`);
htmlRoot.classList.toggle(sanitizedClass);
if (depClass) {
depClass.split(" ").forEach(cls => {
const sanitizedCls = security.sanitizeClassName(cls);
htmlRoot.classList.add(sanitizedCls);
logger.log(`Added dependency class: ${sanitizedCls}`);
});
}
if (coDepClass) {
coDepClass.split(" ").forEach(cls => {
const sanitizedCls = security.sanitizeClassName(cls);
htmlRoot.classList.remove(sanitizedCls);
logger.log(`Removed codependency class: ${sanitizedCls}`);
});
}
if (inputFocus) {
const sanitizedFocus = security.validateDataAttribute(inputFocus);
setTimeout(() => {
const focusElement = document.getElementById(sanitizedFocus);
if (focusElement) focusElement.focus();
logger.log(`Focus set to element: ${sanitizedFocus}`);
}, config.focusDelay);
}
if (typeof window.saveSettings === 'function') {
window.saveSettings();
logger.log('🔑 Settings saved');
}
plugin.checkActiveStyles();
logger.groupEnd();
},
toggleReplace: function (element) {
const target = security.sanitizeSelector(element.getAttribute('data-target'));
const removeClass = security.sanitizeClassName(element.getAttribute('data-removeclass')) || "";
const addClass = security.sanitizeClassName(element.getAttribute('data-addclass'));
const targetElement = document.querySelector(target);
logger.group('Toggle Replace Action');
if (targetElement) {
targetElement.classList.remove(removeClass);
targetElement.classList.add(addClass);
logger.log(`Replaced class "${removeClass}" with "${addClass}" on ${target}`);
} else {
logger.error(`Target element not found: ${target}`);
}
logger.groupEnd();
},
toggleSwap: function (element) {
const targetAttr = element.getAttribute('data-target');
const target = targetAttr ? security.sanitizeSelector(targetAttr) : 'html';
const toggleClass = security.sanitizeClassName(element.getAttribute('data-toggleclass'));
logger.group('Toggle Swap Action');
if (!toggleClass) {
logger.error('Missing required toggleclass attribute for toggle-swap, defaulting to HTML target');
logger.groupEnd();
return;
}
const targetElement = document.querySelector(target);
if (!targetElement) {
logger.error(`Target element not found: ${target}`);
logger.groupEnd();
return;
}
targetElement.classList.toggle(toggleClass);
logger.log(`Toggled class "${toggleClass}" on ${target}`);
if (plugin.containsSubstring(target, 'drawer')) {
htmlRoot.classList.toggle('hide-page-scrollbar');
logger.log('Toggled page scrollbar');
}
logger.groupEnd();
},
panelActions: {
hideTooltips: function (panel) {
// Find all tooltips in the panel and hide them
const tooltipTriggers = panel.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggers.forEach(el => {
const tooltip = bootstrap.Tooltip.getInstance(el);
if (tooltip) {
tooltip.hide();
}
});
},
collapse: function (element) {
logger.group('Panel Collapse Action');
const selectedPanel = security.getClosestElement(element, '.panel');
if (!selectedPanel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
// Hide tooltips before collapsing
this.hideTooltips(selectedPanel);
const panelContainer = selectedPanel.querySelector('.panel-container');
if (!panelContainer) {
logger.error('Panel container not found');
logger.groupEnd();
return;
}
// Always ensure transition is set
panelContainer.style.transition = 'height 0.35s ease';
// Store the current height before any changes
const startHeight = panelContainer.scrollHeight;
// Toggle panel collapsed state
if (selectedPanel.classList.contains('panel-collapsed')) {
// For expanding: First ensure we're at 0 height
panelContainer.style.height = '0';
panelContainer.style.overflow = 'hidden';
// Force a reflow
panelContainer.offsetHeight;
// Remove collapsed class
selectedPanel.classList.remove('panel-collapsed');
// Animate to full height
panelContainer.style.height = startHeight + 'px';
// After animation completes
setTimeout(function () {
panelContainer.style.overflow = 'visible';
panelContainer.style.height = 'auto';
}, 350);
logger.log('Panel uncollapsed: ' + selectedPanel.id);
} else {
// For collapsing: First set exact current height
panelContainer.style.height = startHeight + 'px';
panelContainer.style.overflow = 'hidden';
// Force a reflow
panelContainer.offsetHeight;
// Add collapsed class
selectedPanel.classList.add('panel-collapsed');
// Animate to 0
panelContainer.style.height = '0';
logger.log('Panel collapsed: ' + selectedPanel.id);
}
// Save panel state after toggle
savePanelState();
logger.groupEnd();
},
fullscreen: function (element) {
logger.group('Panel Fullscreen Action');
const selectedPanel = security.getClosestElement(element, '.panel');
if (!selectedPanel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
// Hide tooltips before fullscreen
this.hideTooltips(selectedPanel);
selectedPanel.classList.toggle('panel-fullscreen');
document.documentElement.classList.toggle('panel-fullscreen');
logger.log(`Panel fullscreen toggled: ${selectedPanel.id}`);
logger.groupEnd();
},
close: function (element) {
logger.group('Panel Close Action');
const selectedPanel = security.getClosestElement(element, '.panel');
if (!selectedPanel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
// Hide tooltips before showing close confirmation
this.hideTooltips(selectedPanel);
const panelTitle = selectedPanel.querySelector('.panel-hdr h2')?.textContent.trim() || 'Untitled Panel';
const killPanel = () => {
selectedPanel.style.transition = 'opacity 0.5s';
selectedPanel.style.opacity = '0';
setTimeout(() => {
selectedPanel.remove();
logger.log(`Panel removed: ${selectedPanel.id}`);
}, 500);
};
// Create modal if it doesn't exist
let confirmModal = document.getElementById('panelDeleteModal');
if (!confirmModal) {
const modalHTML = `
<div class="modal fade" id="panelDeleteModal" tabindex="-1" aria-hidden="true" style="--bs-modal-width: 450px;">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark bg-opacity-50 shadow-5 translucent-dark">
<div class="modal-header border-bottom-0">
<h4 class="modal-title text-white d-flex align-items-center">
Delete Panel?
</h4>
<button type="button" class="btn btn-system btn-system-light ms-auto" data-bs-dismiss="modal" aria-label="Close">
<svg class="sa-icon sa-icon-2x">
<use href="img/sprite.svg#x"></use>
</svg>
</button>
</div>
<div class="modal-body"></div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-danger" id="confirmPanelDelete">Yes, delete</button>
</div>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
confirmModal = document.getElementById('panelDeleteModal');
}
// Update modal content with current panel title
const modalBody = confirmModal.querySelector('.modal-body');
//modalBody.textContent = `You are about to delete <strong>${panelTitle}</strong>`;
modalBody.innerHTML = `
<div class="alert alert-danger bg-danger border-danger text-white border-opacity-50 bg-opacity-10 mb-0">
You are about to delete <span class="fw-700">${panelTitle}.</span>
Are you sure you want to delete this panel?
</div>`;
// Initialize Bootstrap modal
const modal = new bootstrap.Modal(confirmModal);
// Set up delete confirmation handler
const confirmDeleteBtn = confirmModal.querySelector('#confirmPanelDelete');
const deleteHandler = () => {
//createUndoButton();
killPanel();
modal.hide();
confirmDeleteBtn.removeEventListener('click', deleteHandler);
};
confirmDeleteBtn.addEventListener('click', deleteHandler);
// Show modal
modal.show();
// Clean up when modal is hidden
confirmModal.addEventListener('hidden.bs.modal', () => {
confirmDeleteBtn.removeEventListener('click', deleteHandler);
}, { once: true });
logger.groupEnd();
},
style: function (element) {
logger.group('Panel Style Action');
// First find the panel container from any location
const panel = security.getClosestElement(element, '.panel');
if (!panel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
// Then find the header within the panel
const selectedPanel = panel.querySelector('.panel-hdr');
if (!selectedPanel) {
logger.error('Panel header not found');
logger.groupEnd();
return;
}
const styleAttr = element.getAttribute('data-panel-style');
if (!styleAttr) {
logger.error('No style classes specified');
logger.groupEnd();
return;
}
// Split and sanitize each class
const newStyles = styleAttr.split(' ')
.map(style => security.sanitizeClassName(style.trim()))
.filter(style => style.startsWith('bg-')); // Only accept bg-* classes
if (newStyles.length === 0) {
logger.error('No valid bg-* classes specified');
logger.groupEnd();
return;
}
// Remove existing bg-* classes
const existingClasses = Array.from(selectedPanel.classList)
.filter(className => className.startsWith('bg-'));
existingClasses.forEach(className => {
selectedPanel.classList.remove(className);
});
// Add new bg classes
newStyles.forEach(style => {
selectedPanel.classList.add(style);
logger.log(`Added panel style: ${style}`);
});
// Save panel state
savePanelState();
logger.groupEnd();
},
toggleClass: function (element) {
logger.group('Panel Toggle Class Action');
// First find the panel container from any location
const selectedPanel = security.getClosestElement(element, '.panel');
if (!selectedPanel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
const toggleClass = security.sanitizeClassName(element.getAttribute('data-panel-toggle'));
if (!toggleClass) {
logger.error('No toggle class specified');
logger.groupEnd();
return;
}
// Toggle class on the panel itself
selectedPanel.classList.toggle(toggleClass);
logger.log(`Panel class toggled: ${toggleClass}`);
// Save panel state
savePanelState();
logger.groupEnd();
},
reset: function (element) {
logger.group('Panel Reset Action');
// First find the panel container from any location
const selectedPanel = security.getClosestElement(element, '.panel');
if (!selectedPanel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
// Get all classes
const panelClasses = Array.from(selectedPanel.classList);
const headerElement = selectedPanel.querySelector('.panel-hdr');
// Remove all classes except the preserved ones from panel
panelClasses.forEach(className => {
if (!['panel', 'panel-collapsed', 'panel-fullscreen'].includes(className)) {
selectedPanel.classList.remove(className);
}
});
// Reset header classes if it exists
if (headerElement) {
const headerClasses = Array.from(headerElement.classList);
headerClasses.forEach(className => {
if (className !== 'panel-hdr') {
headerElement.classList.remove(className);
}
});
}
logger.log('Panel reset to default state');
// Save panel state
savePanelState();
logger.groupEnd();
},
refresh: function (element) {
logger.group('Panel Refresh Action');
const selectedPanel = security.getClosestElement(element, '.panel');
if (!selectedPanel) {
logger.error('Panel element not found');
logger.groupEnd();
return;
}
// Get refresh duration from data attribute or use default
const refreshDuration = parseInt(element.getAttribute('data-refresh-duration')) || 1000;
// Check if panel is already refreshing
if (selectedPanel.classList.contains('panel-refreshing')) {
logger.warn('Panel is already refreshing');
logger.groupEnd();
return;
}
// Add refreshing class
selectedPanel.classList.add('panel-refreshing');
logger.log('Panel refresh started');
// Get callback function name from data attribute
const callbackName = element.getAttribute('data-refresh-callback');
// Execute callback if it exists
if (callbackName && typeof window[callbackName] === 'function') {
try {
window[callbackName](selectedPanel);
logger.log(`Executed callback: ${callbackName}`);
} catch (error) {
logger.error(`Error in refresh callback: ${error.message}`);
}
}
// Remove refreshing class after duration
setTimeout(() => {
selectedPanel.classList.remove('panel-refreshing');
logger.log('Panel refresh completed');
}, refreshDuration);
logger.groupEnd();
}
},
toggleTheme: function () {
logger.group('Toggle Theme Action');
// Get current theme or default to 'light'
const currentTheme = htmlRoot.getAttribute('data-bs-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
// Set the new theme
htmlRoot.setAttribute('data-bs-theme', newTheme);
logger.log(`Theme switched to: ${newTheme}`);
// Use the utility method to update aria-pressed attributes
plugin.updateThemeButtonsState();
// Save settings if available - do this immediately to save the data-bs-theme attribute
if (typeof window.saveSettings === 'function') {
window.saveSettings();
logger.log('Theme attribute settings saved');
}
// Re-extract colors after theme change
utils.extractColors();
logger.log('Colors re-extracted after theme change');
// Update all chart colors
plugin.updateAllPluginColors();
logger.groupEnd();
},
removeClass: function (element) {
const target = security.sanitizeSelector(element.getAttribute('data-target'));
const className = security.sanitizeClassName(element.getAttribute('data-classname'));
const targetElement = document.querySelector(target);
logger.group('Remove Class Action');
if (targetElement?.classList.contains(className)) {
targetElement.classList.remove(className);
logger.log(`Removed class "${className}" from ${target}`);
}
if (target === 'html' && typeof window.saveSettings === 'function') {
window.saveSettings();
plugin.checkActiveStyles();
logger.log('Settings saved');
}
logger.groupEnd();
},
addClass: function (element) {
const target = security.sanitizeSelector(element.getAttribute('data-target'));
const className = security.sanitizeClassName(element.getAttribute('data-classname'));
const targetElement = document.querySelector(target);
logger.group('Add Class Action');
if (targetElement) {
targetElement.classList.add(className);
logger.log(`Added class "${className}" to ${target}`);
} else {
logger.error(`Target element not found: ${target}`);
}
if (target === 'html' && typeof window.saveSettings === 'function') {
window.saveSettings();
plugin.checkActiveStyles();
logger.log('Settings saved');
}
logger.groupEnd();
},
appFullscreen: function () {
logger.group('Fullscreen Action');
if (!security.checkFullscreenPermission()) {
logger.error('Fullscreen permission denied');
logger.groupEnd();
return;
}
if (!fullscreenHandler.isFullscreen()) {
fullscreenHandler.enter()
.then(() => {
logger.log('Entered fullscreen mode');
document.body.classList.add('fullscreen-active');
localStorage.setItem('appFullscreen', 'true');
})
.catch(error => logger.error(`Fullscreen error: ${error.message}`));
} else {
fullscreenHandler.exit()
.then(() => {
logger.log('Exited fullscreen mode');
document.body.classList.remove('fullscreen-active');
localStorage.removeItem('appFullscreen');
})
.catch(error => logger.error(`Fullscreen exit error: ${error.message}`));
}
logger.groupEnd();
},
playSound: async function (element) {
logger.group('Play Sound Action');
try {
const soundFile = element.getAttribute('data-soundfile');
if (!soundFile) {
throw new Error('No sound file specified');
}
const sanitizedSound = audioHelpers.sanitizeFilename(soundFile);
// Check if this sound is currently playing
if (element.getAttribute('data-audio-playing') === sanitizedSound) {
// If it's playing, pause it
handlers.pauseSound(element);
return;
}
// Stop all other playing sounds
audioHelpers.stopAllSoundsExcept(sanitizedSound);
const path = config.defaultSoundPath;
// Try to get from cache first
let audioElement = cache.audioCache.get(sanitizedSound);
if (!audioElement) {
audioElement = audioHelpers.createAudioElement(
path,
sanitizedSound,
config.sound.volume
);
cache.audioCache.set(sanitizedSound, audioElement);
}
// Reset the audio if it was previously played
audioElement.currentTime = 0;
// Apply fade-in if enabled
if (config.sound.fadeIn) {
await audioHelpers.fadeInAudio(
audioElement,
config.sound.volume,
config.sound.fadeInDuration
);
}
await audioElement.play();
// Store the currently playing audio element in a data attribute
element.setAttribute('data-audio-playing', sanitizedSound);
logger.log(`Playing sound: ${path}${sanitizedSound}`);
// Return promise that resolves when audio finishes playing
return new Promise((resolve, reject) => {
audioElement.addEventListener('ended', () => {
element.removeAttribute('data-audio-playing');
resolve();
}, { once: true });
audioElement.addEventListener('error', (e) => reject(e), { once: true });
});
} catch (error) {
logger.error(`Failed to play sound: ${error.message}`);
throw error;
} finally {
logger.groupEnd();
}
},
pauseSound: function (element) {
logger.group('Pause Sound Action');
try {
const playingSound = element.getAttribute('data-audio-playing');
if (!playingSound) {
logger.log('No sound currently playing');
return;
}
const audioElement = cache.audioCache.get(playingSound);
if (audioElement) {
audioElement.pause();
audioElement.currentTime = 0; // Reset to beginning
element.removeAttribute('data-audio-playing');
logger.log(`Paused sound: ${playingSound}`);
}
} catch (error) {
logger.error(`Failed to pause sound: ${error.message}`);
} finally {
logger.groupEnd();
}
},
themeStyle: function (element) {
logger.group('Theme Style Action');
const themeUrl = element.getAttribute('data-theme-style');
if (themeUrl === null) {
logger.error('Missing data-theme-style attribute');
logger.groupEnd();
return;
}
if (themeUrl === '') {
// If empty, remove the theme style element
const themeStyleElement = document.getElementById(config.theme.styleId);
if (themeStyleElement) {
themeStyleElement.remove();
logger.log('Theme style removed');
}
} else {
// Set the theme style
let finalUrl = themeUrl;
// If doesn't end with .css, add the extension
if (!finalUrl.endsWith('.css')) {
finalUrl = `css/themes/${finalUrl}.css`;
}
plugin.setThemeStyle(finalUrl);
}
// Update all theme switcher elements
plugin.updateThemeStyleStates();
// Save settings
if (typeof window.saveSettings === 'function') {
window.saveSettings();
}
logger.log(`Theme style changed to: ${themeUrl}`);
logger.groupEnd();
}
};
// Event delegation handler
// Modify your handleAction function to include theme style action
function handleAction(event) {
const element = event.target.closest(config.selectors.actionButtons);
if (!element) return;
const actionType = security.validateDataAttribute(element.dataset.action);
// Add panel actions to the handler object
const handler = {
'toggle': () => handlers.toggle(element),
'toggle-replace': () => handlers.toggleReplace(element),
'toggle-swap': () => handlers.toggleSwap(element),
'remove-class': () => handlers.removeClass(element),
'add-class': () => handlers.addClass(element),
'app-fullscreen': () => handlers.appFullscreen(),
'playsound': () => handlers.playSound(element),
'pausesound': () => handlers.pauseSound(element),
// Panel actions
'panel-collapse': () => handlers.panelActions.collapse(element),
'panel-fullscreen': () => handlers.panelActions.fullscreen(element),
'panel-close': () => handlers.panelActions.close(element),
'panel-style': () => handlers.panelActions.style(element),
'panel-toggle': () => handlers.panelActions.toggleClass(element),
'panel-reset': () => handlers.panelActions.reset(element),
'panel-refresh': () => handlers.panelActions.refresh(element),
'toggle-theme': () => handlers.toggleTheme(),
'theme-style': () => handlers.themeStyle(element)
}[actionType];
if (handler) {
handler();
logger.log(`Action executed: ${actionType}`);
} else {
logger.warn(`Unknown action type: ${actionType}`);
}
}
// Add sound-specific methods to plugin
plugin.preloadSound = function (soundFile) {
const sanitizedSound = audioHelpers.sanitizeFilename(soundFile);
if (!cache.audioCache.has(sanitizedSound)) {
const audio = audioHelpers.createAudioElement(
config.defaultSoundPath,
sanitizedSound,
config.sound.volume
);
cache.audioCache.set(sanitizedSound, audio);
}
return plugin;
};
// Configuration methods
plugin.config = function (options) {
Object.assign(config, options);
logger.log('Configuration updated');
return plugin;
};
plugin.debug = function (enabled) {
config.debug = enabled;
// Add or remove debug class on body element
const debugClass = 'app-debug-mode';
if (enabled) {
document.body.classList.add(debugClass);
} else {
document.body.classList.remove(debugClass);
}
logger.log(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
return plugin;
};
// Add programmatic methods for actions
plugin.toggleTheme = function () {
handlers.toggleTheme();
return plugin;
};
// Add method to switch theme style programmatically
plugin.switchThemeStyle = function (themeUrl) {
if (!themeUrl) {
logger.error('Theme URL is required');
return plugin;
}
plugin.setThemeStyle(themeUrl);
plugin.updateThemeStyleStates();
return plugin;
};
// Utility method to update aria-pressed on theme toggle buttons
plugin.updateThemeButtonsState = function () {
const currentTheme = htmlRoot.getAttribute('data-bs-theme') || 'light';
const isDarkTheme = currentTheme === 'dark';
document.querySelectorAll('[data-action="toggle-theme"]').forEach(button => {
button.setAttribute('aria-pressed', isDarkTheme.toString());
logger.log(`Updated aria-pressed to ${isDarkTheme} on theme toggle button`);
});
return plugin;
};
plugin.toggleClass = function (target, className) {
if (!target || !className) {
logger.error('Target and className are required for toggleClass');
return plugin;
}
const element = document.createElement('button');
element.setAttribute('data-action', 'toggle');
element.setAttribute('data-class', className);
element.setAttribute('data-target', target);
handlers.toggle(element);
return plugin;
};
plugin.toggleReplace = function (target, removeClass, addClass) {
if (!target || !addClass) {
logger.error('Target and addClass are required for toggleReplace');
return plugin;
}
const element = document.createElement('button');
element.setAttribute('data-action', 'toggle-replace');
element.setAttribute('data-target', target);
element.setAttribute('data-addclass', addClass);
if (removeClass) {
element.setAttribute('data-removeclass', removeClass);
}
handlers.toggleReplace(element);
return plugin;
};
plugin.toggleSwap = function (target, toggleClass) {
if (!target || !toggleClass) {
logger.error('Target and toggleClass are required for toggleSwap');
return plugin;
}
const element = document.createElement('button');
element.setAttribute('data-action', 'toggle-swap');
element.setAttribute('data-target', target);
element.setAttribute('data-toggleclass', toggleClass);
handlers.toggleSwap(element);
return plugin;
};
// Enhanced cleanup method
plugin.destroy = function () {
document.removeEventListener('click', handleAction);
cache.actionButtons = null;
// Clean up audio cache
cache.audioCache.forEach(audio => {
audio.pause();
audio.src = '';
});
cache.audioCache.clear();
logger.log('Plugin destroyed');
};
// Initialize plugin
function init() {
document.addEventListener('click', handleAction);
document.addEventListener('DOMContentLoaded', function () {
cache.actionButtons = document.querySelectorAll(config.selectors.actionButtons);
plugin.checkActiveStyles();
// Extract colors after DOM is loaded
utils.extractColors();
// Also update states immediately for elements already in the DOM
plugin.updateThemeStyleStates();
plugin.updateThemeButtonsState();
});
logger.log('Plugin initialized');
}
// Initialize the plugin
init();
// Return public methods
return plugin;
})();
})(window);
// Display the screen size in the top-right corner of the screen
// document.addEventListener('DOMContentLoaded', function() {
// appDOM.checkActiveStyles().debug(true);
// });
// Sidebar menu scroll to active item
window.addEventListener('load', () => {
setTimeout(() => {
const sideNavMenu = document.getElementById("js-nav-menu");
const activeItem = sideNavMenu?.querySelector('li.active a.active');
const scrollWrapper = document.querySelector('.app-sidebar .slimScrollDiv');
const scrollContainer = scrollWrapper?.firstElementChild;
if (activeItem && scrollContainer) {
const containerRect = scrollContainer.getBoundingClientRect();
const itemRect = activeItem.getBoundingClientRect();
const offset = scrollContainer.scrollTop + (itemRect.bottom - containerRect.top) - 400;
if (offset > 70) {
smoothScrollTo(scrollContainer, offset, 30);
}
}
}, 50);
});
function smoothScrollTo(element, to, duration) {
const start = element.scrollTop;
const change = to - start;
const increment = 20;
let currentTime = 0;
function animateScroll() {
currentTime += increment;
const val = easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if (currentTime < duration) {
setTimeout(animateScroll, increment);
}
}
animateScroll();
}
function easeInOutQuad(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}