Add accessibility toolbar with 6 toggleable options
Adds a floating accessibility toolbar to the template with individually enable/disable options: text resize, color inversion, high contrast, highlight links, readable font, and pause animations. Each option has an admin toggle in the Theme tab and persists visitor preferences in localStorage. Also fixes count() on null in mod_login override. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,7 @@ $headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'U
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (count($twofactormethods) > 1) : ?>
|
<?php if (!empty($twofactormethods) && count($twofactormethods) > 1) : ?>
|
||||||
<div class="mod-login__field mb-3">
|
<div class="mod-login__field mb-3">
|
||||||
<label for="modlgn-secretkey-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
|
<label for="modlgn-secretkey-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ $params_theme_control_type = (string) $this->params->get('theme_control_type', '
|
|||||||
$params_theme_fab_enabled = $this->params->get('theme_fab_enabled', 1);
|
$params_theme_fab_enabled = $this->params->get('theme_fab_enabled', 1);
|
||||||
$params_theme_fab_pos = $this->params->get('theme_fab_pos', 'br');
|
$params_theme_fab_pos = $this->params->get('theme_fab_pos', 'br');
|
||||||
|
|
||||||
|
// Accessibility params
|
||||||
|
$params_a11y_toolbar = $this->params->get('a11y_toolbar_enabled', 1);
|
||||||
|
$params_a11y_resize = $this->params->get('a11y_text_resize', 1);
|
||||||
|
$params_a11y_invert = $this->params->get('a11y_color_inversion', 1);
|
||||||
|
$params_a11y_contrast = $this->params->get('a11y_high_contrast', 1);
|
||||||
|
$params_a11y_links = $this->params->get('a11y_highlight_links', 1);
|
||||||
|
$params_a11y_font = $this->params->get('a11y_readable_font', 1);
|
||||||
|
$params_a11y_animations = $this->params->get('a11y_pause_animations', 1);
|
||||||
|
$params_a11y_pos = (string) $this->params->get('a11y_toolbar_pos', 'tl');
|
||||||
|
|
||||||
// Detecting Active Variables
|
// Detecting Active Variables
|
||||||
$option = $input->getCmd('option', '');
|
$option = $input->getCmd('option', '');
|
||||||
$view = $input->getCmd('view', '');
|
$view = $input->getCmd('view', '');
|
||||||
@@ -277,8 +287,16 @@ $wa->useScript('user.js'); // js/user.js
|
|||||||
<?php if (trim($params_custom_head_end)) : ?><?php echo $params_custom_head_end; ?><?php endif; ?>
|
<?php if (trim($params_custom_head_end)) : ?><?php echo $params_custom_head_end; ?><?php endif; ?>
|
||||||
</head>
|
</head>
|
||||||
<body data-bs-spy="scroll" data-bs-target="#toc"
|
<body data-bs-spy="scroll" data-bs-target="#toc"
|
||||||
data-theme-fab-enabled="<?php echo $params_theme_fab_enabled ? '1' : '0'; ?>"
|
data-theme-fab-enabled="<?php echo $params_theme_fab_enabled ? '1' : '0'; ?>"
|
||||||
data-theme-fab-pos="<?php echo htmlspecialchars($params_theme_fab_pos, ENT_QUOTES, 'UTF-8'); ?>"
|
data-theme-fab-pos="<?php echo htmlspecialchars($params_theme_fab_pos, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
data-a11y-toolbar="<?php echo $params_a11y_toolbar ? '1' : '0'; ?>"
|
||||||
|
data-a11y-resize="<?php echo $params_a11y_resize ? '1' : '0'; ?>"
|
||||||
|
data-a11y-invert="<?php echo $params_a11y_invert ? '1' : '0'; ?>"
|
||||||
|
data-a11y-contrast="<?php echo $params_a11y_contrast ? '1' : '0'; ?>"
|
||||||
|
data-a11y-links="<?php echo $params_a11y_links ? '1' : '0'; ?>"
|
||||||
|
data-a11y-font="<?php echo $params_a11y_font ? '1' : '0'; ?>"
|
||||||
|
data-a11y-animations="<?php echo $params_a11y_animations ? '1' : '0'; ?>"
|
||||||
|
data-a11y-pos="<?php echo htmlspecialchars($params_a11y_pos, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
class="site <?php
|
class="site <?php
|
||||||
echo $option . ' ' . $wrapper
|
echo $option . ' ' . $wrapper
|
||||||
. ' view-' . $view
|
. ' view-' . $view
|
||||||
|
|||||||
@@ -108,6 +108,29 @@ TPL_MOKO_THEME_FAB_ENABLED_DESC="Display a persistent, accessible theme toggle."
|
|||||||
TPL_MOKO_THEME_FAB_POS="Floating switch position"
|
TPL_MOKO_THEME_FAB_POS="Floating switch position"
|
||||||
TPL_MOKO_THEME_FAB_POS_DESC="Screen corner for the toggle."
|
TPL_MOKO_THEME_FAB_POS_DESC="Screen corner for the toggle."
|
||||||
|
|
||||||
|
; ===== Accessibility toolbar =====
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_ENABLED="Accessibility toolbar"
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_ENABLED_DESC="Show a floating accessibility toolbar with text resize and colour inversion controls."
|
||||||
|
TPL_MOKO_A11Y_TEXT_RESIZE="Text resize"
|
||||||
|
TPL_MOKO_A11Y_TEXT_RESIZE_DESC="Allow visitors to increase or decrease text size."
|
||||||
|
TPL_MOKO_A11Y_COLOR_INVERSION="Colour inversion"
|
||||||
|
TPL_MOKO_A11Y_COLOR_INVERSION_DESC="Allow visitors to invert page colours for improved readability."
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_POS="Toolbar position"
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_POS_DESC="Screen corner for the accessibility toolbar."
|
||||||
|
TPL_MOKO_A11Y_BTN_LABEL="Accessibility options"
|
||||||
|
TPL_MOKO_A11Y_TEXT_DECREASE="Decrease text size"
|
||||||
|
TPL_MOKO_A11Y_TEXT_RESET="Reset text size"
|
||||||
|
TPL_MOKO_A11Y_TEXT_INCREASE="Increase text size"
|
||||||
|
TPL_MOKO_A11Y_INVERT_COLORS="Invert colours"
|
||||||
|
TPL_MOKO_A11Y_HIGH_CONTRAST="High contrast"
|
||||||
|
TPL_MOKO_A11Y_HIGH_CONTRAST_DESC="Allow visitors to boost page contrast for improved readability."
|
||||||
|
TPL_MOKO_A11Y_HIGHLIGHT_LINKS="Highlight links"
|
||||||
|
TPL_MOKO_A11Y_HIGHLIGHT_LINKS_DESC="Allow visitors to outline all links so they stand out from surrounding text."
|
||||||
|
TPL_MOKO_A11Y_READABLE_FONT="Readable font"
|
||||||
|
TPL_MOKO_A11Y_READABLE_FONT_DESC="Allow visitors to switch to a clean system font optimised for readability."
|
||||||
|
TPL_MOKO_A11Y_PAUSE_ANIMATIONS="Pause animations"
|
||||||
|
TPL_MOKO_A11Y_PAUSE_ANIMATIONS_DESC="Allow visitors to stop all CSS animations and transitions."
|
||||||
|
|
||||||
; ===== CSS Variables tab =====
|
; ===== CSS Variables tab =====
|
||||||
TPL_MOKOCASSIOPEIA_CSS_VARS_FIELDSET_LABEL="CSS Variables"
|
TPL_MOKOCASSIOPEIA_CSS_VARS_FIELDSET_LABEL="CSS Variables"
|
||||||
TPL_MOKOCASSIOPEIA_CSS_VARS_INTRO="<p>All colours, spacing and layout values are driven by CSS custom properties. To override any variable without editing the template, add your overrides to <code>media/templates/site/mokocassiopeia/css/user.css</code>, or create a custom palette file (see the Theme tab). Variables are scoped to <code>:root[data-bs-theme="light"]</code> or <code>:root[data-bs-theme="dark"]</code> so light and dark values are independent.</p>"
|
TPL_MOKOCASSIOPEIA_CSS_VARS_INTRO="<p>All colours, spacing and layout values are driven by CSS custom properties. To override any variable without editing the template, add your overrides to <code>media/templates/site/mokocassiopeia/css/user.css</code>, or create a custom palette file (see the Theme tab). Variables are scoped to <code>:root[data-bs-theme="light"]</code> or <code>:root[data-bs-theme="dark"]</code> so light and dark values are independent.</p>"
|
||||||
|
|||||||
@@ -108,6 +108,29 @@ TPL_MOKO_THEME_FAB_ENABLED_DESC="Display a persistent, accessible theme toggle."
|
|||||||
TPL_MOKO_THEME_FAB_POS="Floating switch position"
|
TPL_MOKO_THEME_FAB_POS="Floating switch position"
|
||||||
TPL_MOKO_THEME_FAB_POS_DESC="Screen corner for the toggle."
|
TPL_MOKO_THEME_FAB_POS_DESC="Screen corner for the toggle."
|
||||||
|
|
||||||
|
; ===== Accessibility toolbar =====
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_ENABLED="Accessibility toolbar"
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_ENABLED_DESC="Show a floating accessibility toolbar with text resize and color inversion controls."
|
||||||
|
TPL_MOKO_A11Y_TEXT_RESIZE="Text resize"
|
||||||
|
TPL_MOKO_A11Y_TEXT_RESIZE_DESC="Allow visitors to increase or decrease text size."
|
||||||
|
TPL_MOKO_A11Y_COLOR_INVERSION="Color inversion"
|
||||||
|
TPL_MOKO_A11Y_COLOR_INVERSION_DESC="Allow visitors to invert page colors for improved readability."
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_POS="Toolbar position"
|
||||||
|
TPL_MOKO_A11Y_TOOLBAR_POS_DESC="Screen corner for the accessibility toolbar."
|
||||||
|
TPL_MOKO_A11Y_BTN_LABEL="Accessibility options"
|
||||||
|
TPL_MOKO_A11Y_TEXT_DECREASE="Decrease text size"
|
||||||
|
TPL_MOKO_A11Y_TEXT_RESET="Reset text size"
|
||||||
|
TPL_MOKO_A11Y_TEXT_INCREASE="Increase text size"
|
||||||
|
TPL_MOKO_A11Y_INVERT_COLORS="Invert colors"
|
||||||
|
TPL_MOKO_A11Y_HIGH_CONTRAST="High contrast"
|
||||||
|
TPL_MOKO_A11Y_HIGH_CONTRAST_DESC="Allow visitors to boost page contrast for improved readability."
|
||||||
|
TPL_MOKO_A11Y_HIGHLIGHT_LINKS="Highlight links"
|
||||||
|
TPL_MOKO_A11Y_HIGHLIGHT_LINKS_DESC="Allow visitors to outline all links so they stand out from surrounding text."
|
||||||
|
TPL_MOKO_A11Y_READABLE_FONT="Readable font"
|
||||||
|
TPL_MOKO_A11Y_READABLE_FONT_DESC="Allow visitors to switch to a clean system font optimized for readability."
|
||||||
|
TPL_MOKO_A11Y_PAUSE_ANIMATIONS="Pause animations"
|
||||||
|
TPL_MOKO_A11Y_PAUSE_ANIMATIONS_DESC="Allow visitors to stop all CSS animations and transitions."
|
||||||
|
|
||||||
; ===== CSS Variables tab =====
|
; ===== CSS Variables tab =====
|
||||||
TPL_MOKOCASSIOPEIA_CSS_VARS_FIELDSET_LABEL="CSS Variables"
|
TPL_MOKOCASSIOPEIA_CSS_VARS_FIELDSET_LABEL="CSS Variables"
|
||||||
TPL_MOKOCASSIOPEIA_CSS_VARS_INTRO="<p>All colors, spacing and layout values are driven by CSS custom properties. To override any variable without editing the template, add your overrides to <code>media/templates/site/mokocassiopeia/css/user.css</code>, or create a custom palette file (see the Theme tab). Variables are scoped to <code>:root[data-bs-theme="light"]</code> or <code>:root[data-bs-theme="dark"]</code> so light and dark values are independent.</p>"
|
TPL_MOKOCASSIOPEIA_CSS_VARS_INTRO="<p>All colors, spacing and layout values are driven by CSS custom properties. To override any variable without editing the template, add your overrides to <code>media/templates/site/mokocassiopeia/css/user.css</code>, or create a custom palette file (see the Theme tab). Variables are scoped to <code>:root[data-bs-theme="light"]</code> or <code>:root[data-bs-theme="dark"]</code> so light and dark values are independent.</p>"
|
||||||
|
|||||||
@@ -17180,6 +17180,190 @@ button#mokoThemeSwitch {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
ACCESSIBILITY TOOLBAR
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
/* Color inversion */
|
||||||
|
html.a11y-inverted {
|
||||||
|
filter: invert(1) hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.a11y-inverted img,
|
||||||
|
html.a11y-inverted video,
|
||||||
|
html.a11y-inverted picture,
|
||||||
|
html.a11y-inverted svg,
|
||||||
|
html.a11y-inverted [style*="background-image"],
|
||||||
|
html.a11y-inverted .brand-logo img {
|
||||||
|
filter: invert(1) hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High contrast */
|
||||||
|
html.a11y-high-contrast {
|
||||||
|
filter: contrast(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.a11y-high-contrast img,
|
||||||
|
html.a11y-high-contrast video,
|
||||||
|
html.a11y-high-contrast picture {
|
||||||
|
filter: contrast(0.714);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight links */
|
||||||
|
html.a11y-highlight-links a {
|
||||||
|
outline: 2px solid currentColor !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Readable font — system sans-serif stack for maximum clarity */
|
||||||
|
html.a11y-readable-font,
|
||||||
|
html.a11y-readable-font * {
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||||
|
letter-spacing: 0.02em !important;
|
||||||
|
word-spacing: 0.05em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pause animations */
|
||||||
|
html.a11y-pause-animations *,
|
||||||
|
html.a11y-pause-animations *::before,
|
||||||
|
html.a11y-pause-animations *::after {
|
||||||
|
animation-duration: 0s !important;
|
||||||
|
animation-delay: 0s !important;
|
||||||
|
transition-duration: 0s !important;
|
||||||
|
transition-delay: 0s !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar container */
|
||||||
|
#mokoA11yToolbar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1201;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: .25rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mokoA11yToolbar.a11y-pos-tl { top: 1rem; left: 1rem; }
|
||||||
|
#mokoA11yToolbar.a11y-pos-tr { top: 1rem; right: 1rem; align-items: flex-end; }
|
||||||
|
#mokoA11yToolbar.a11y-pos-bl { bottom: 1rem; left: 1rem; }
|
||||||
|
#mokoA11yToolbar.a11y-pos-br { bottom: 1rem; right: 1rem; align-items: flex-end; }
|
||||||
|
|
||||||
|
/* Toggle button */
|
||||||
|
.a11y-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--link-color, #3565e5);
|
||||||
|
background: var(--bs-body-bg, #fff);
|
||||||
|
color: var(--link-color, #3565e5);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .2s, color .2s, box-shadow .2s;
|
||||||
|
box-shadow: var(--box-shadow, 0 .25rem .5rem rgba(0,0,0,.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-toggle:hover,
|
||||||
|
.a11y-toggle:focus-visible {
|
||||||
|
background: var(--link-color, #3565e5);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-toggle.active {
|
||||||
|
background: var(--link-color, #3565e5);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel */
|
||||||
|
.a11y-panel {
|
||||||
|
background: var(--bs-body-bg, #fff);
|
||||||
|
border: 1px solid var(--border-color, #dee2e6);
|
||||||
|
border-radius: var(--border-radius, .375rem);
|
||||||
|
padding: .75rem;
|
||||||
|
min-width: 200px;
|
||||||
|
box-shadow: var(--box-shadow-lg, 0 1rem 3rem rgba(0,0,0,.175));
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-group {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-group-label {
|
||||||
|
display: block;
|
||||||
|
font-size: .75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
margin-bottom: .35rem;
|
||||||
|
color: var(--body-font-color, #444);
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-btn-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid var(--border-color, #dee2e6);
|
||||||
|
border-radius: var(--border-radius, .375rem);
|
||||||
|
background: var(--bs-body-bg, #fff);
|
||||||
|
color: var(--body-font-color, #444);
|
||||||
|
font-size: .875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-btn:hover,
|
||||||
|
.a11y-btn:focus-visible {
|
||||||
|
border-color: var(--link-color, #3565e5);
|
||||||
|
background: var(--link-color, #3565e5);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-size-display {
|
||||||
|
font-size: .8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 3ch;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--body-font-color, #444);
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-btn-wide {
|
||||||
|
width: 100%;
|
||||||
|
gap: .5rem;
|
||||||
|
padding: .35rem .75rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11y-btn-wide.active {
|
||||||
|
background: var(--link-color, #3565e5);
|
||||||
|
border-color: var(--link-color, #3565e5);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.a11y-toggle,
|
||||||
|
.a11y-btn {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html.component body{
|
html.component body{
|
||||||
padding-top: 3.125rem;
|
padding-top: 3.125rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,235 @@
|
|||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// ACCESSIBILITY TOOLBAR
|
||||||
|
// ========================================================================
|
||||||
|
var a11yStorageKey = "moko-a11y";
|
||||||
|
var fontSizeSteps = [85, 90, 100, 110, 120, 130];
|
||||||
|
var defaultStep = 2; // index of 100
|
||||||
|
|
||||||
|
function getA11yPrefs() {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(a11yStorageKey);
|
||||||
|
if (raw) return JSON.parse(raw);
|
||||||
|
} catch (e) {}
|
||||||
|
return { fontStep: defaultStep, inverted: false, contrast: false, links: false, font: false, paused: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveA11yPrefs(prefs) {
|
||||||
|
try { localStorage.setItem(a11yStorageKey, JSON.stringify(prefs)); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFontSize(step) {
|
||||||
|
root.style.fontSize = fontSizeSteps[step] + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInversion(on) {
|
||||||
|
if (on) {
|
||||||
|
root.classList.add("a11y-inverted");
|
||||||
|
} else {
|
||||||
|
root.classList.remove("a11y-inverted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyContrast(on) {
|
||||||
|
root.classList.toggle("a11y-high-contrast", on);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLinks(on) {
|
||||||
|
root.classList.toggle("a11y-highlight-links", on);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFont(on) {
|
||||||
|
root.classList.toggle("a11y-readable-font", on);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPaused(on) {
|
||||||
|
root.classList.toggle("a11y-pause-animations", on);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a Font Awesome icon element (safe DOM, no innerHTML). */
|
||||||
|
function faIcon(classes) {
|
||||||
|
var span = doc.createElement("span");
|
||||||
|
span.className = classes;
|
||||||
|
span.setAttribute("aria-hidden", "true");
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildA11yToolbar() {
|
||||||
|
if (doc.getElementById("mokoA11yToolbar")) return;
|
||||||
|
|
||||||
|
var body = doc.body;
|
||||||
|
var showResize = body.getAttribute("data-a11y-resize") === "1";
|
||||||
|
var showInvert = body.getAttribute("data-a11y-invert") === "1";
|
||||||
|
var showContrast = body.getAttribute("data-a11y-contrast") === "1";
|
||||||
|
var showLinks = body.getAttribute("data-a11y-links") === "1";
|
||||||
|
var showFont = body.getAttribute("data-a11y-font") === "1";
|
||||||
|
var showAnimations = body.getAttribute("data-a11y-animations") === "1";
|
||||||
|
var pos = (body.getAttribute("data-a11y-pos") || "tl").toLowerCase();
|
||||||
|
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = "tl";
|
||||||
|
|
||||||
|
var prefs = getA11yPrefs();
|
||||||
|
|
||||||
|
// Container
|
||||||
|
var toolbar = doc.createElement("div");
|
||||||
|
toolbar.id = "mokoA11yToolbar";
|
||||||
|
toolbar.className = "a11y-pos-" + pos;
|
||||||
|
toolbar.setAttribute("role", "toolbar");
|
||||||
|
toolbar.setAttribute("aria-label", "Accessibility options");
|
||||||
|
|
||||||
|
// Toggle button (accessibility icon)
|
||||||
|
var toggle = doc.createElement("button");
|
||||||
|
toggle.type = "button";
|
||||||
|
toggle.id = "mokoA11yToggle";
|
||||||
|
toggle.className = "a11y-toggle";
|
||||||
|
toggle.setAttribute("aria-label", "Accessibility options");
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
toggle.appendChild(faIcon("fa-solid fa-universal-access"));
|
||||||
|
|
||||||
|
// Panel
|
||||||
|
var panel = doc.createElement("div");
|
||||||
|
panel.id = "mokoA11yPanel";
|
||||||
|
panel.className = "a11y-panel";
|
||||||
|
panel.hidden = true;
|
||||||
|
|
||||||
|
// --- Text resize controls ---
|
||||||
|
if (showResize) {
|
||||||
|
var resizeGroup = doc.createElement("div");
|
||||||
|
resizeGroup.className = "a11y-group";
|
||||||
|
resizeGroup.setAttribute("role", "group");
|
||||||
|
resizeGroup.setAttribute("aria-label", "Text size");
|
||||||
|
|
||||||
|
var sizeLabel = doc.createElement("span");
|
||||||
|
sizeLabel.className = "a11y-group-label";
|
||||||
|
sizeLabel.textContent = "Text size";
|
||||||
|
resizeGroup.appendChild(sizeLabel);
|
||||||
|
|
||||||
|
var btnRow = doc.createElement("div");
|
||||||
|
btnRow.className = "a11y-btn-row";
|
||||||
|
|
||||||
|
var btnMinus = doc.createElement("button");
|
||||||
|
btnMinus.type = "button";
|
||||||
|
btnMinus.className = "a11y-btn";
|
||||||
|
btnMinus.setAttribute("aria-label", "Decrease text size");
|
||||||
|
btnMinus.appendChild(faIcon("fa-solid fa-minus"));
|
||||||
|
|
||||||
|
var sizeDisplay = doc.createElement("span");
|
||||||
|
sizeDisplay.className = "a11y-size-display";
|
||||||
|
sizeDisplay.setAttribute("aria-live", "polite");
|
||||||
|
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
|
||||||
|
|
||||||
|
var btnPlus = doc.createElement("button");
|
||||||
|
btnPlus.type = "button";
|
||||||
|
btnPlus.className = "a11y-btn";
|
||||||
|
btnPlus.setAttribute("aria-label", "Increase text size");
|
||||||
|
btnPlus.appendChild(faIcon("fa-solid fa-plus"));
|
||||||
|
|
||||||
|
var btnReset = doc.createElement("button");
|
||||||
|
btnReset.type = "button";
|
||||||
|
btnReset.className = "a11y-btn a11y-btn-reset";
|
||||||
|
btnReset.setAttribute("aria-label", "Reset text size");
|
||||||
|
btnReset.appendChild(faIcon("fa-solid fa-rotate-left"));
|
||||||
|
|
||||||
|
btnMinus.addEventListener("click", function () {
|
||||||
|
if (prefs.fontStep > 0) {
|
||||||
|
prefs.fontStep--;
|
||||||
|
applyFontSize(prefs.fontStep);
|
||||||
|
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
|
||||||
|
saveA11yPrefs(prefs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnPlus.addEventListener("click", function () {
|
||||||
|
if (prefs.fontStep < fontSizeSteps.length - 1) {
|
||||||
|
prefs.fontStep++;
|
||||||
|
applyFontSize(prefs.fontStep);
|
||||||
|
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
|
||||||
|
saveA11yPrefs(prefs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnReset.addEventListener("click", function () {
|
||||||
|
prefs.fontStep = defaultStep;
|
||||||
|
applyFontSize(prefs.fontStep);
|
||||||
|
sizeDisplay.textContent = fontSizeSteps[prefs.fontStep] + "%";
|
||||||
|
saveA11yPrefs(prefs);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnRow.appendChild(btnMinus);
|
||||||
|
btnRow.appendChild(sizeDisplay);
|
||||||
|
btnRow.appendChild(btnPlus);
|
||||||
|
btnRow.appendChild(btnReset);
|
||||||
|
resizeGroup.appendChild(btnRow);
|
||||||
|
panel.appendChild(resizeGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper: build a switch-style toggle button ---
|
||||||
|
function addSwitchOption(show, prefKey, icon, label, applyFn) {
|
||||||
|
if (!show) return;
|
||||||
|
var group = doc.createElement("div");
|
||||||
|
group.className = "a11y-group";
|
||||||
|
|
||||||
|
var btn = doc.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "a11y-btn a11y-btn-wide";
|
||||||
|
btn.setAttribute("role", "switch");
|
||||||
|
btn.setAttribute("aria-checked", prefs[prefKey] ? "true" : "false");
|
||||||
|
btn.setAttribute("aria-label", label);
|
||||||
|
btn.appendChild(faIcon(icon));
|
||||||
|
btn.appendChild(doc.createTextNode(" " + label));
|
||||||
|
|
||||||
|
if (prefs[prefKey]) btn.classList.add("active");
|
||||||
|
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
prefs[prefKey] = !prefs[prefKey];
|
||||||
|
applyFn(prefs[prefKey]);
|
||||||
|
btn.setAttribute("aria-checked", prefs[prefKey] ? "true" : "false");
|
||||||
|
btn.classList.toggle("active", prefs[prefKey]);
|
||||||
|
saveA11yPrefs(prefs);
|
||||||
|
});
|
||||||
|
|
||||||
|
group.appendChild(btn);
|
||||||
|
panel.appendChild(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toggle options ---
|
||||||
|
addSwitchOption(showInvert, "inverted", "fa-solid fa-circle-half-stroke", "Invert colors", applyInversion);
|
||||||
|
addSwitchOption(showContrast, "contrast", "fa-solid fa-adjust", "High contrast", applyContrast);
|
||||||
|
addSwitchOption(showLinks, "links", "fa-solid fa-link", "Highlight links", applyLinks);
|
||||||
|
addSwitchOption(showFont, "font", "fa-solid fa-font", "Readable font", applyFont);
|
||||||
|
addSwitchOption(showAnimations, "paused", "fa-solid fa-pause", "Pause animations", applyPaused);
|
||||||
|
|
||||||
|
// Toggle panel open/close
|
||||||
|
toggle.addEventListener("click", function () {
|
||||||
|
var isOpen = !panel.hidden;
|
||||||
|
panel.hidden = isOpen;
|
||||||
|
toggle.setAttribute("aria-expanded", isOpen ? "false" : "true");
|
||||||
|
toggle.classList.toggle("active", !isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
doc.addEventListener("click", function (e) {
|
||||||
|
if (!toolbar.contains(e.target) && !panel.hidden) {
|
||||||
|
panel.hidden = true;
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
toggle.classList.remove("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply saved preferences on load
|
||||||
|
if (prefs.fontStep !== defaultStep) applyFontSize(prefs.fontStep);
|
||||||
|
if (prefs.inverted) applyInversion(true);
|
||||||
|
if (prefs.contrast) applyContrast(true);
|
||||||
|
if (prefs.links) applyLinks(true);
|
||||||
|
if (prefs.font) applyFont(true);
|
||||||
|
if (prefs.paused) applyPaused(true);
|
||||||
|
|
||||||
|
toolbar.appendChild(toggle);
|
||||||
|
toolbar.appendChild(panel);
|
||||||
|
body.appendChild(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// TEMPLATE UTILITIES
|
// TEMPLATE UTILITIES
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -271,6 +500,11 @@
|
|||||||
buildThemeToggle();
|
buildThemeToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build accessibility toolbar if enabled
|
||||||
|
if (doc.body.getAttribute("data-a11y-toolbar") === "1") {
|
||||||
|
buildA11yToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
// Sticky header behavior
|
// Sticky header behavior
|
||||||
handleScroll();
|
handleScroll();
|
||||||
win.addEventListener("scroll", handleScroll);
|
win.addEventListener("scroll", handleScroll);
|
||||||
|
|||||||
@@ -241,6 +241,65 @@
|
|||||||
<option value="1">JYES</option>
|
<option value="1">JYES</option>
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
|
<!-- Accessibility -->
|
||||||
|
<field name="theme_sep_a11y" type="spacer" label="Accessibility" hr="false" class="text fw-bold" />
|
||||||
|
<field name="a11y_toolbar_enabled" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_TOOLBAR_ENABLED" description="TPL_MOKO_A11Y_TOOLBAR_ENABLED_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_text_resize" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_TEXT_RESIZE" description="TPL_MOKO_A11Y_TEXT_RESIZE_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_color_inversion" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_COLOR_INVERSION" description="TPL_MOKO_A11Y_COLOR_INVERSION_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_high_contrast" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_HIGH_CONTRAST" description="TPL_MOKO_A11Y_HIGH_CONTRAST_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_highlight_links" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_HIGHLIGHT_LINKS" description="TPL_MOKO_A11Y_HIGHLIGHT_LINKS_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_readable_font" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_READABLE_FONT" description="TPL_MOKO_A11Y_READABLE_FONT_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_pause_animations" type="radio" default="1"
|
||||||
|
label="TPL_MOKO_A11Y_PAUSE_ANIMATIONS" description="TPL_MOKO_A11Y_PAUSE_ANIMATIONS_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="a11y_toolbar_pos" type="list" default="tl"
|
||||||
|
label="TPL_MOKO_A11Y_TOOLBAR_POS" description="TPL_MOKO_A11Y_TOOLBAR_POS_DESC"
|
||||||
|
showon="a11y_toolbar_enabled:1">
|
||||||
|
<option value="br">Bottom-right</option>
|
||||||
|
<option value="bl">Bottom-left</option>
|
||||||
|
<option value="tr">Top-right</option>
|
||||||
|
<option value="tl">Top-left</option>
|
||||||
|
</field>
|
||||||
|
|
||||||
<!-- Toggle UI -->
|
<!-- Toggle UI -->
|
||||||
<field name="theme_sep_toggle" type="spacer" label="Theme Toggle UI" hr="false" class="text fw-bold" />
|
<field name="theme_sep_toggle" type="spacer" label="Theme Toggle UI" hr="false" class="text fw-bold" />
|
||||||
<field name="theme_fab_enabled" type="radio" default="1"
|
<field name="theme_fab_enabled" type="radio" default="1"
|
||||||
|
|||||||
Reference in New Issue
Block a user