ai_accessibility-1.0.2/js/ai_accessibility.ck.js
js/ai_accessibility.ck.js
(function (Drupal, drupalSettings) {
if (typeof CKEDITOR === 'undefined' && typeof ClassicEditor === 'undefined' && typeof window.CKEditor5 === 'undefined') {
// Not in editor context we recognize.
return;
}
// Simple integration: when document is ready, add a button to run validation.
document.addEventListener('DOMContentLoaded', () => {
const runBtn = document.createElement('button');
runBtn.type = 'button';
runBtn.textContent = 'AI A11y: Validate content';
runBtn.style.position = 'fixed';
runBtn.style.right = '20px';
runBtn.style.bottom = '20px';
runBtn.style.zIndex = 9999;
runBtn.className = 'ai-a11y-validate-button';
document.body.appendChild(runBtn);
const panel = document.createElement('div');
panel.style.position = 'fixed';
panel.style.right = '20px';
panel.style.bottom = '70px';
panel.style.width = '360px';
panel.style.maxHeight = '60vh';
panel.style.overflow = 'auto';
panel.style.background = '#fff';
panel.style.border = '1px solid #ccc';
panel.style.padding = '10px';
panel.style.zIndex = 9999;
panel.style.display = 'none';
document.body.appendChild(panel);
// Expose a bridge so CKEditor plugin can open the same panel.
window.aiA11y = window.aiA11y || {};
window.aiA11y.openPanel = async function (openEditorInstance) {
await runHandler(openEditorInstance);
};
runBtn.addEventListener('click', async () => runHandler());
// Main handler reused by standalone button and CKEditor plugin.
async function runHandler(passedEditor) {
panel.innerHTML = '<strong>Validating (axe)...</strong>';
panel.style.display = 'block';
// Try to find CKEditor 5 instance on the page, prefer passedEditor.
let ckEditorInstance = passedEditor || null;
try {
if (!ckEditorInstance && window.CKEditor5) {
const editables = document.querySelectorAll('[contenteditable="true"], textarea');
for (const el of editables) {
if (el.ckeditorInstance) { ckEditorInstance = el.ckeditorInstance; break; }
const possible = el.closest && el.closest('.ck-editor');
if (possible && possible.ckeditorInstance) { ckEditorInstance = possible.ckeditorInstance; break; }
}
}
}
catch (e) { console.warn('Error detecting CKEditor5 instance', e); }
// Helper to get/set content using CKEditor API if available.
const setContent = async (newHtml) => {
if (ckEditorInstance && typeof ckEditorInstance.setData === 'function') {
await ckEditorInstance.setData(newHtml);
}
else {
const editorEl = document.querySelector('textarea[name="body[0][value]"]') || document.querySelector('textarea[name="body"]');
if (editorEl) editorEl.value = newHtml;
}
};
const getContent = async () => {
if (ckEditorInstance && typeof ckEditorInstance.getData === 'function') {
return await ckEditorInstance.getData();
}
const editorEl = document.querySelector('textarea[name="body[0][value]"]') || document.querySelector('textarea[name="body"]');
return editorEl ? editorEl.value : document.body.innerHTML;
};
let content = '';
try {
content = await getContent();
}
catch (e) {
console.warn('Error getting editor content', e);
content = document.body.innerHTML;
}
// If axe is loaded, try to run it against the editor DOM (prefer real editable DOM when CKEditor available).
if (window.axe && typeof window.axe.run === 'function') {
try {
let target = document;
if (ckEditorInstance && ckEditorInstance.editing && ckEditorInstance.editing.view && typeof ckEditorInstance.editing.view.getDomRoot === 'function') {
const domRoot = ckEditorInstance.editing.view.getDomRoot();
if (domRoot) target = domRoot;
}
else if (ckEditorInstance && ckEditorInstance.ui && typeof ckEditorInstance.ui.getEditableElement === 'function') {
const el = ckEditorInstance.ui.getEditableElement();
if (el) target = el;
}
// Avoid passing an empty `runOnly.values` array which causes
// axe to throw: "runOnly.values must be a non-empty array".
// Use default axe options unless a non-empty set of rules is configured.
const results = await window.axe.run(target);
panel.innerHTML = '';
const violations = results.violations || [];
if (violations.length === 0) {
panel.innerHTML = '<div>No issues found.</div>';
return;
}
violations.forEach(v => {
const item = document.createElement('div');
item.style.borderBottom = '1px solid #eee';
item.style.padding = '8px 0';
item.innerHTML = `<strong>${v.help}</strong><div>${v.description}</div>`;
const viewBtn = document.createElement('button');
viewBtn.type = 'button';
viewBtn.textContent = 'See recommendation';
viewBtn.style.marginRight = '8px';
viewBtn.addEventListener('click', () => alert(v.help));
item.appendChild(viewBtn);
const aiBtn = document.createElement('button');
aiBtn.type = 'button';
aiBtn.textContent = 'Fix with AI';
aiBtn.addEventListener('click', async () => {
aiBtn.textContent = 'Calling AI...';
const selector = (v.nodes && v.nodes[0] && v.nodes[0].target && v.nodes[0].target[0]) ? v.nodes[0].target[0] : v.id;
const r = await fetch('/ai-accessibility/ia', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ issue_id: v.id || selector, html: content }),
});
const s = await r.json();
if (s.suggestions && s.suggestions[0]) {
const sug = s.suggestions[0];
if (sug.action === 'set_alt') {
// apply alt to first img inside target
const img = target.querySelector('img');
if (img) {
img.setAttribute('alt', sug.text);
const html = target.innerHTML || document.body.innerHTML;
await setContent(html);
panel.innerHTML = '<div>Fix applied.</div>';
}
}
else {
panel.innerHTML = `<div>${sug.text}</div>`;
}
}
aiBtn.textContent = 'Fix with AI';
});
item.appendChild(aiBtn);
panel.appendChild(item);
});
}
catch (e) {
panel.innerHTML = '<div>Error running axe</div>';
console.error(e);
}
}
else {
// Fallback to server-side validate endpoint.
panel.innerHTML = '<strong>Validating (server)...</strong>';
try {
const resp = await fetch('/ai-accessibility/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: content }),
});
const data = await resp.json();
panel.innerHTML = '';
if (!data.violations || data.violations.length === 0) {
panel.innerHTML = '<div>No issues found.</div>';
return;
}
data.violations.forEach(v => {
const item = document.createElement('div');
item.style.borderBottom = '1px solid #eee';
item.style.padding = '8px 0';
item.innerHTML = `<strong>${v.description}</strong><div>${v.help}</div>`;
const viewBtn = document.createElement('button');
viewBtn.type = 'button';
viewBtn.textContent = 'See recommendation';
viewBtn.style.marginRight = '8px';
viewBtn.addEventListener('click', () => alert(v.help));
item.appendChild(viewBtn);
const aiBtn = document.createElement('button');
aiBtn.type = 'button';
aiBtn.textContent = 'Fix with AI';
aiBtn.addEventListener('click', async () => {
aiBtn.textContent = 'Calling AI...';
const r = await fetch('/ai-accessibility/ia', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ issue_id: v.id, html: content }),
});
const s = await r.json();
if (s.suggestions && s.suggestions[0]) {
const sug = s.suggestions[0];
if (sug.action === 'set_alt') {
const parser = new DOMParser();
const d = parser.parseFromString(content, 'text/html');
const img = d.querySelector('img');
if (img) {
img.setAttribute('alt', sug.text);
const newHtml = d.body.innerHTML;
await setContent(newHtml);
panel.innerHTML = '<div>Fix applied.</div>';
}
}
else {
panel.innerHTML = `<div>${sug.text}</div>`;
}
}
aiBtn.textContent = 'Fix with AI';
});
item.appendChild(aiBtn);
panel.appendChild(item);
});
}
catch (e) {
panel.innerHTML = '<div>Validation error</div>';
console.error(e);
}
}
}
});
})(Drupal, drupalSettings);
