>/* once-wcag-accesibilidad-js */
/* ============================================================
once-form-accesibilidad.js
Correcciones WCAG 2.1 Nivel AA para el formulario ONCE
Inyectar en: JotForm → Settings → Custom Code → JavaScript
============================================================ */
(function () {
'use strict';
/* ── Helpers ──────────────────────────────────────────── */
/* Añade un ID a aria-describedby sin borrar los existentes */
function appendAriaDescribedBy(el, id) {
if (!el || !id) return;
var current = (el.getAttribute('aria-describedby') || '').split(' ').filter(Boolean);
if (current.indexOf(id) === -1) {
current.push(id);
el.setAttribute('aria-describedby', current.join(' '));
}
}
/* Copia atributos seleccionados de un elemento a otro */
function copyAttributes(src, tgt, attrs) {
attrs.forEach(function (attr) {
if (src.hasAttribute(attr)) {
tgt.setAttribute(attr, src.getAttribute(attr));
}
});
}
/* ── Fix 1: Enlace "Saltar al contenido" (WCAG 2.4.1) ── */
function injectSkipLink() {
if (document.querySelector('.wcag-skip-link')) return;
var skip = document.createElement('a');
skip.href = '#main-content';
skip.className = 'wcag-skip-link';
skip.textContent = 'Saltar al contenido principal';
document.body.insertBefore(skip, document.body.firstChild);
/* Asignar el id de ancla al contenedor del formulario */
var formAll = document.querySelector('.form-all, form[id]');
if (formAll && !document.getElementById('main-content')) {
formAll.id = 'main-content';
}
}
/* ── Fix 2: Jerarquía de encabezados (WCAG 1.3.1) ──── */
function fixHeadings() {
var h1s = Array.from(document.querySelectorAll('h1'));
var h2s = Array.from(document.querySelectorAll('h2'));
/* Si un H2 aparece ANTES del primer H1 en el DOM, convertirlo en
*/
if (h1s.length > 0) {
var firstH1 = h1s[0];
h2s.forEach(function (h2) {
/* compareDocumentPosition: bit 2 = el nodo comparado está ANTES */
if (firstH1.compareDocumentPosition(h2) & Node.DOCUMENT_POSITION_PRECEDING) {
var p = document.createElement('p');
p.className = 'form-intro-text';
p.innerHTML = h2.innerHTML;
copyAttributes(h2, p, ['id', 'style', 'aria-label']);
h2.parentNode.replaceChild(p, h2);
}
});
}
/* Si hay más de un H1, convertir los adicionales en H2 */
var h1sActualizados = Array.from(document.querySelectorAll('h1'));
if (h1sActualizados.length > 1) {
h1sActualizados.slice(1).forEach(function (h1) {
var h2 = document.createElement('h2');
h2.innerHTML = h1.innerHTML;
copyAttributes(h1, h2, ['id', 'class', 'style', 'aria-label']);
h1.parentNode.replaceChild(h2, h1);
});
}
}
/* ── Fix 3a: Nota explicativa de campos obligatorios (WCAG 1.3.1) ── */
function injectRequiredNotice() {
if (document.querySelector('.wcag-required-notice')) return;
var formAll = document.querySelector('.form-all, form[id]');
if (!formAll) return;
var p = document.createElement('p');
p.className = 'wcag-required-notice';
/* aria-hidden en el asterisco visual para que el lector no lo lea */
p.innerHTML = 'Los campos marcados con * son obligatorios.';
formAll.insertBefore(p, formAll.firstChild);
}
/* ── Fix 3b: aria-hidden en asteriscos + aria-required en inputs (WCAG 1.3.1) */
function fixRequiredFields() {
/* Ocultar asteriscos a lectores de pantalla */
document.querySelectorAll('.form-required').forEach(function (span) {
span.setAttribute('aria-hidden', 'true');
});
/* Marcar los inputs/select/textarea del mismo campo como obligatorios */
document.querySelectorAll('.form-required').forEach(function (span) {
var line = span.closest('li[id^="id_"], .form-line, .jf-form-group, fieldset');
if (!line) return;
var field = line.querySelector(
'input:not([type="hidden"]):not([type="submit"]), select, textarea'
);
if (field && !field.hasAttribute('aria-required')) {
field.setAttribute('aria-required', 'true');
}
});
}
/* ── Fix 4: Atributos autocomplete (WCAG 1.3.5) ──── */
function fixAutocomplete() {
var mapa = [
{ id: 'input_10', value: 'given-name' }, /* Nombre */
{ id: 'input_12', value: 'family-name' }, /* Apellido */
{ id: 'q39_numeroDe', value: 'off' }, /* DNI — desactivar por seguridad */
{ id: 'input_37_area', value: 'tel-national' }, /* Prefijo telefónico */
];
mapa.forEach(function (entry) {
var el = document.getElementById(entry.id);
if (el) el.setAttribute('autocomplete', entry.value);
});
/* Campo número de teléfono: buscar el input hermano del sublabel_37_area */
var sublabelArea = document.getElementById('sublabel_37_area');
if (sublabelArea) {
var contenedor = sublabelArea.closest('li[id^="id_"], .form-line, fieldset');
if (contenedor) {
var tel = contenedor.querySelector('input:not(#input_37_area):not([type="hidden"])');
if (tel && !tel.getAttribute('autocomplete')) {
tel.setAttribute('autocomplete', 'tel');
}
}
}
}
/* ── Fix 5: Vincular sublabels a sus inputs (WCAG 1.3.1) ── */
function fixSublabels() {
var pares = [
{ input: 'input_37_area', sublabel: 'sublabel_37_area' }, /* Prefijo tel. */
{ input: 'day_41', sublabel: 'sublabel_41_day' }, /* Día */
{ input: 'month_41', sublabel: 'sublabel_41_month' }, /* Mes */
{ input: 'year_41', sublabel: 'sublabel_41_year' }, /* Año */
];
pares.forEach(function (par) {
var input = document.getElementById(par.input);
var sublabel = document.getElementById(par.sublabel);
if (!input || !sublabel) return;
/* Garantizar que el sublabel tiene ID */
if (!sublabel.id) sublabel.id = par.sublabel;
appendAriaDescribedBy(input, par.sublabel);
});
}
/* ── Fix 6: SVGs decorativos sin accesibilidad (WCAG 1.1.1) ── */
function fixSVGs() {
document.querySelectorAll('svg').forEach(function (svg) {
/* Saltamos los que ya están correctamente marcados */
if (svg.getAttribute('aria-hidden') === 'true') return;
if (svg.getAttribute('role') === 'img') return;
if (svg.getAttribute('aria-label') || svg.getAttribute('aria-labelledby')) return;
if (svg.querySelector('title')) return;
/* Sin título ni etiqueta → es decorativo */
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
});
}
/* ── Fix 7: Vincular hints de textareas (WCAG 1.3.1) ── */
function fixTextareaHints() {
document.querySelectorAll('textarea').forEach(function (textarea) {
if (!textarea.id) return;
var contenedor = textarea.closest('li[id^="id_"], .form-line, fieldset');
if (!contenedor) return;
var hint = contenedor.querySelector('.form-hint, .form-description, [class*="hint"]');
if (!hint) return;
if (!hint.id) hint.id = 'hint_' + textarea.id;
appendAriaDescribedBy(textarea, hint.id);
});
}
/* ── Fix 8: Iframes sin title (WCAG 4.1.2) ─────────────────── */
function fixIframes() {
document.querySelectorAll('iframe').forEach(function (iframe) {
if (iframe.getAttribute('title') || iframe.getAttribute('aria-label')) return;
var src = iframe.src || iframe.getAttribute('src') || '';
var title = src.includes('terms') || src.includes('Terms') || src.includes('termsConditions')
? 'Términos y Condiciones'
: 'Contenido embebido';
iframe.setAttribute('title', title);
});
}
/* ── Fix 9: Barra de progreso multipágina (buena práctica AA) ── */
function injectProgressBar() {
if (document.getElementById('wcag-progress-bar')) return;
/* Contar páginas por separadores de JotForm */
var pagebreaks = document.querySelectorAll('.form-pagebreak, [class*="pagebreak"]');
var totalPages = pagebreaks.length > 0 ? pagebreaks.length + 1 : 1;
if (totalPages <= 1) return; /* Sin barra si es formulario de una sola página */
var bar = document.createElement('div');
bar.id = 'wcag-progress-bar';
bar.className = 'wcag-progress-bar';
bar.setAttribute('role', 'progressbar');
bar.setAttribute('aria-valuenow', '1');
bar.setAttribute('aria-valuemin', '1');
bar.setAttribute('aria-valuemax', String(totalPages));
bar.setAttribute('aria-label', 'Página 1 de ' + totalPages);
bar.textContent = 'Paso 1 de ' + totalPages;
var formAll = document.querySelector('.form-all, form[id]');
if (formAll) {
formAll.insertBefore(bar, formAll.firstChild);
}
}
/* ── Fix 10: Gestión de foco en campos condicionales (WCAG 2.4.3) ── */
function observeConditionalFields() {
var formAll = document.querySelector('.form-all, form[id]');
if (!formAll) return;
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
/* Caso A: nodo nuevo añadido al DOM (campo revelado como nuevo elemento) */
mutation.addedNodes.forEach(function (node) {
if (node.nodeType !== 1) return;
if (!node.matches('li[id^="id_"], .form-line, fieldset')) return;
var primero = node.querySelector(
'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled])'
);
if (primero) setTimeout(function () { primero.focus(); }, 80);
});
/* Caso B: estilo cambiado de display:none a visible */
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
var target = mutation.target;
var antes = mutation.oldValue || '';
var ocultaAntes = antes.indexOf('display: none') !== -1 || antes.indexOf('display:none') !== -1;
var visibleAhora = target.style.display !== 'none' && target.style.display !== '';
if (ocultaAntes && visibleAhora) {
var primero = target.querySelector(
'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled])'
);
if (primero) setTimeout(function () { primero.focus(); }, 80);
}
}
});
});
observer.observe(formAll, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
attributeFilter: ['style']
});
}
/* ── Fix 8 live: Actualizar barra de progreso al cambiar de página ── */
function observePageChanges() {
var formAll = document.querySelector('.form-all, form[id]');
if (!formAll) return;
var paginaActual = 1;
/* Guard para evitar bucle por las propias mutaciones que hacemos */
var enEjecucion = false;
var observer = new MutationObserver(function (mutations) {
if (enEjecucion) return;
/* Detectar indicador de número de página que JotForm inyecta */
var indicador = document.querySelector(
'[class*="form-page-number"], .form-pagebreak-title, [class*="jf-page-number"]'
);
if (!indicador) return;
var match = indicador.textContent.match(/(d+)/);
if (!match) return;
var nueva = parseInt(match[1], 10);
if (nueva === paginaActual) return;
paginaActual = nueva;
enEjecucion = true;
var bar = document.getElementById('wcag-progress-bar');
if (bar) {
var total = bar.getAttribute('aria-valuemax') || '2';
bar.setAttribute('aria-valuenow', String(nueva));
bar.setAttribute('aria-label', 'Página ' + nueva + ' de ' + total);
bar.textContent = 'Paso ' + nueva + ' de ' + total;
}
setTimeout(function () { enEjecucion = false; }, 300);
});
observer.observe(formAll, { childList: true, subtree: true });
}
/* ── Orquestador principal ─────────────────────────────── */
function runAllFixes() {
injectSkipLink(); /* Fix 1 — skip link (primero: estructura de página) */
fixHeadings(); /* Fix 2 — jerarquía H1/H2 */
injectRequiredNotice(); /* Fix 3a — nota "campos obligatorios" */
fixRequiredFields(); /* Fix 3b — aria-hidden asteriscos + aria-required */
fixAutocomplete(); /* Fix 4 — atributos autocomplete */
fixSublabels(); /* Fix 5 — aria-describedby en campos compuestos */
fixSVGs(); /* Fix 6 — SVGs decorativos */
fixTextareaHints(); /* Fix 7 — hints de textareas */
fixIframes(); /* Fix 8 — iframes sin title */
injectProgressBar(); /* Fix 9 — barra de progreso (si multipágina) */
observeConditionalFields(); /* Fix 10 — foco en campos condicionales */
observePageChanges(); /* Fix 9 live — actualizar barra al cambiar página */
}
/* Ejecutar después de que JotForm inicialice su propio JS (~300 ms) */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setTimeout(runAllFixes, 300);
});
} else {
setTimeout(runAllFixes, 300);
}
})();