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);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc