Files
wp-bootstrap/src/js/dark-mode.js
magdev 89afa00678
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m8s
Create Release Package / Build Release (push) Successful in 1m53s
security: OWASP audit and hardening (v1.0.8)
- Archive XSS: wrap get_the_archive_title/description with wp_kses_post()
  in ContextBuilder to sanitize Editor-editable term content rendered via |raw
- Comment fields: esc_html() on comment_author, esc_url() on comment_author_url
  at data source; template updated to output pre-escaped URL via |raw
- dark-mode.js: whitelist localStorage value against ['dark','light'] to
  prevent attribute injection from third-party script tampering
- TwigService: add is_safe=>html to esc_html/esc_attr/esc_url Twig functions
  to prevent double-encoding if autoescape is ever enabled
- Add .markdownlint.json (disable MD024 duplicate headings, MD013 line length)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 13:23:33 +01:00

97 lines
3.6 KiB
JavaScript

/**
* WP Bootstrap Dark Mode Toggle
*
* Handles dark mode switching using Bootstrap 5.3's data-bs-theme attribute.
* Respects prefers-color-scheme media query and persists choice in localStorage.
*
* @package WPBootstrap
* @since 0.1.0
*/
(function () {
'use strict';
var STORAGE_KEY = 'wp-bootstrap-theme';
var ATTR = 'data-bs-theme';
/**
* Get the user's stored preference, or fall back to system preference.
*
* @return {string} 'dark' or 'light'
*/
function getPreferredTheme() {
var stored = localStorage.getItem(STORAGE_KEY);
// Whitelist: only honour known-good values to prevent attribute injection
// from a tampered localStorage (e.g. XSS-written value by another script).
if (stored === 'dark' || stored === 'light') {
return stored;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* Apply the theme to the document and update all toggle buttons.
*
* @param {string} theme - 'dark' or 'light'
*/
function setTheme(theme) {
document.documentElement.setAttribute(ATTR, theme);
document.querySelectorAll('[data-bs-theme-toggle]').forEach(function (toggle) {
var isDark = theme === 'dark';
toggle.setAttribute('aria-label', isDark
? (toggle.dataset.labelLight || 'Switch to light mode')
: (toggle.dataset.labelDark || 'Switch to dark mode'));
toggle.setAttribute('aria-pressed', String(isDark));
var sunIcon = toggle.querySelector('.wp-bootstrap-sun-icon');
var moonIcon = toggle.querySelector('.wp-bootstrap-moon-icon');
if (sunIcon) sunIcon.style.display = isDark ? 'inline-block' : 'none';
if (moonIcon) moonIcon.style.display = isDark ? 'none' : 'inline-block';
});
}
// Apply theme immediately to prevent flash.
setTheme(getPreferredTheme());
// When DOM is ready, re-apply for toggle buttons and attach event listeners.
document.addEventListener('DOMContentLoaded', function () {
setTheme(getPreferredTheme());
document.querySelectorAll('[data-bs-theme-toggle]').forEach(function (toggle) {
toggle.addEventListener('click', function () {
var currentTheme = document.documentElement.getAttribute(ATTR);
var newTheme = currentTheme === 'dark' ? 'light' : 'dark';
localStorage.setItem(STORAGE_KEY, newTheme);
setTheme(newTheme);
announceTheme(newTheme);
});
});
});
// Listen for system preference changes when no stored preference exists.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if (!localStorage.getItem(STORAGE_KEY)) {
setTheme(e.matches ? 'dark' : 'light');
}
});
/**
* Announce theme change to screen readers via a live region.
*
* @param {string} theme - 'dark' or 'light'
*/
function announceTheme(theme) {
var msg = theme === 'dark' ? 'Dark mode enabled' : 'Light mode enabled';
var el = document.getElementById('wp-bootstrap-theme-status');
if (!el) {
el = document.createElement('div');
el.id = 'wp-bootstrap-theme-status';
el.setAttribute('role', 'status');
el.setAttribute('aria-live', 'polite');
el.className = 'visually-hidden';
document.body.appendChild(el);
}
el.textContent = msg;
}
})();