editoria11y-1.0.0-alpha8/js/editoria11y-drupal.js

js/editoria11y-drupal.js
/* globals Drupal, drupalSettings, Ed11y, console, ed11yLang, ed11yLangDrupal, editoria11yOptions, drupalTranslations */
/**
 * Drupal initializer.
 * Launch as behavior and pull variables from config.
 */

// Prevent multiple inits.
let ed11yOnce;
let ed11yInitialized;
let ed11yWaiting = false;

const ed11yInitializer = function () {
  "use strict";

  if (ed11yInitialized === 'disabled' || ed11yInitialized === 'pending') {
    return;
  }
  ed11yInitialized = 'pending';

  const dS = drupalSettings.editoria11y;
  const urlParams = new URLSearchParams(window.location.search);

  let lang = dS.lang ? dS.lang : 'en';
  // @todo 3.x restore language functionality.
  /*if (lang !== 'en') {
    lang = 'dynamic';
    ed11yLang.dynamic = ed11yLangDrupal;
    options.langSanitizes = true; // Use Drupal string sanitizer.
  }*/

  let options = {

    /* Alpha-only; will be moved into CSF */
    contrastPlugin: true,
    readabilityPlugin: true,
    developerPlugin: true,
    embeddedContentPlugin: true,
    linksAdvancedPlugin: true,
    checks: {
      // Heading checks
      HEADING_SKIPPED_LEVEL: {
        type: 'warning',
      },
      HEADING_EMPTY_WITH_IMAGE: true,
      HEADING_EMPTY: true,
      HEADING_FIRST: true,
      HEADING_LONG: {
        maxLength: 170,
      },
      HEADING_MISSING_ONE: true,

      // Image checks
      MISSING_ALT_LINK: true,
      MISSING_ALT_LINK_HAS_TEXT: true,
      MISSING_ALT: true,
      IMAGE_DECORATIVE_CAROUSEL: {
        sources: '.carousel',
      },
      LINK_IMAGE_NO_ALT_TEXT: true,
      LINK_IMAGE_TEXT: true,
      IMAGE_FIGURE_DECORATIVE: true,
      IMAGE_DECORATIVE: true,
      LINK_ALT_FILE_EXT: true,
      ALT_FILE_EXT: true,
      LINK_PLACEHOLDER_ALT: true,
      ALT_PLACEHOLDER: true,
      LINK_SUS_ALT: true,
      SUS_ALT: true,
      LINK_IMAGE_LONG_ALT: {
        maxLength: 250,
      },
      IMAGE_ALT_TOO_LONG: {
        maxLength: 250,
      },
      LINK_IMAGE_ALT: false,
      // {dismissAll: true,} default.
      LINK_IMAGE_ALT_AND_TEXT: true,
      IMAGE_FIGURE_DUPLICATE_ALT: true,
      IMAGE_PASS: {
        dismissAll: true,
      },
      ALT_UNPRONOUNCEABLE: true,
      LINK_ALT_UNPRONOUNCEABLE: true,
      ALT_MAYBE_BAD: {
        minLength: 15,
      },
      LINK_ALT_MAYBE_BAD: {
        minLength: 15,
      },

      // Link checks
      DUPLICATE_TITLE: {
        dismissAll: true,
      },
      LINK_EMPTY_LABELLEDBY: true,
      LINK_EMPTY_NO_LABEL: true,
      LINK_STOPWORD: {
        type: 'warning',
      },
      LINK_STOPWORD_ARIA: true,
      LINK_SYMBOLS: true,
      LINK_CLICK_HERE: false,
      LINK_DOI: {
        dismissAll: true,
      },
      LINK_URL: {
        maxLength: 40,
      },
      LINK_LABEL: {
        dismissAll: true,
      },
      LINK_EMPTY: true,
      LINK_IDENTICAL_NAME: {
        dismissAll: true,
      },
      LINK_NEW_TAB: {
        dismissAll: true,
      },
      LINK_FILE_EXT: true,

      // Form labels checks
      LABELS_MISSING_IMAGE_INPUT: true,
      LABELS_INPUT_RESET: true,
      LABELS_MISSING_LABEL: true,
      LABELS_ARIA_LABEL_INPUT: true,
      LABELS_NO_FOR_ATTRIBUTE: true,
      LABELS_PLACEHOLDER: false, // @todo change default in config

      // Embedded content checks
      EMBED_AUDIO: {
        sources: '',
      },
      EMBED_VIDEO: {
        sources: '',
      },
      EMBED_DATA_VIZ: {
        sources: '',
      },
      EMBED_UNFOCUSABLE: true,
      EMBED_MISSING_TITLE: true,
      EMBED_GENERAL: true,

      // Quality assurance checks
      QA_BAD_LINK: {
        sources: '',
      },
      QA_STRONG_ITALICS: true,
      QA_IN_PAGE_LINK: true,
      QA_DOCUMENT: {
        sources: '',
        dismissAll: true,
      },
      QA_PDF: {
        dismissAll: true,
      },
      QA_BLOCKQUOTE: true,
      TABLES_MISSING_HEADINGS: true,
      TABLES_SEMANTIC_HEADING: true,
      TABLES_EMPTY_HEADING: true,
      QA_FAKE_HEADING: true,
      QA_FAKE_LIST: true,
      QA_UPPERCASE: true,
      QA_UNDERLINE: true,
      QA_SUBSCRIPT: true,
      QA_NESTED_COMPONENTS: {
        sources: '',
      },
      QA_JUSTIFY: true,
      QA_SMALL_TEXT: true,

      // Meta checks
      META_LANG: true,
      META_SCALABLE: true,
      META_MAX: true,
      META_REFRESH: true,

      // Developer checks
      DUPLICATE_ID: true,
      META_TITLE: true,
      UNCONTAINED_LI: true,
      TABINDEX_ATTR: true,
      HIDDEN_FOCUSABLE: true,
      LABEL_IN_NAME: true,
      BTN_EMPTY: true,
      BTN_EMPTY_LABELLEDBY: true,
      BTN_ROLE_IN_NAME: true,

      // Contrast checks
      CONTRAST_WARNING: {
        dismissAll: true,
      },
      CONTRAST_INPUT: true,
      CONTRAST_ERROR: true,
      CONTRAST_PLACEHOLDER: true,
      CONTRAST_PLACEHOLDER_UNSUPPORTED: true,
      CONTRAST_ERROR_GRAPHIC: true,
      CONTRAST_WARNING_GRAPHIC: false,
      CONTRAST_UNSUPPORTED: {
        dismissAll: true,
      },
    },
    /* End alpha-only check configuration */

    checkRoot: dS.content_root ? dS.content_root : false,
    containerIgnore: !!dS.ignore_elements ?
      `#toolbar-administration *, .tabledrag, ${dS.ignore_elements}` :
      '#toolbar-administration *, .tabledrag',
    panelNoCover: !!dS.panel_no_cover ?
      dS.panel_no_cover :
      '#klaro-cookie-notice, #klaro_toggle_dialog, .same-page-preview-dialog.ui-dialog-position-side, #gin_sidebar, #admin-toolbar',
    panelPinTo: dS.panel_pin === 'left' ? 'left' : 'right',
    ignoreAllIfAbsent: !!dS.ignore_all_if_absent ? dS.ignore_all_if_absent : false,
    // 100 under contextuals, 491 Gin tools, 1000 CKEditor tools, 1260 modals.
    // Ed11y adds 9000 to tips, 99999 to modal tips. Was 491 until May 2025.
    buttonZIndex: 100,
    autoDetectShadowComponents: !!dS.detect_shadow,
    shadowComponents: dS.shadow_components ? dS.shadow_components : false,
    linkIgnore:
      `[aria-hidden][tabindex="-1"], [id$="-local-tasks"] a,
      .contextual-links a,
      .block-local-tasks-block a, .filter-help > a, .contextual-region > nav a 
      ${drupalSettings.path.currentPathIsAdmin ? ', a[target="_blank"]' : ''}`,
    headerIgnore:
      '.filter-guidelines-item *, nav *, [id$="-local-tasks"] *, ' +
      '.block-local-tasks-block *, .tabledrag h4',
    imageIgnore:
      `[aria-hidden], [aria-hidden] img, [role="presentation"], 
      a[href][aria-label] img, button[aria-label] img, 
      a[href][aria-labelledby] img, button[aria-labelledby] img`,
    lang: lang, // Todo 3.x needed? enUS?
    currentPage: dS.page_path,
    allowHide: !!dS.allow_hide,
    allowOK: !!dS.allow_ok,
    syncedDismissals: dS.dismissals,
    showDismissed: urlParams.has('ed1ref'),
    linkStringsNewWindows: !!dS.link_strings_new_windows ?
      new RegExp (dS.link_strings_new_windows, 'gi')
      : !!dS.ignore_link_strings ?
        new RegExp(dS.ignore_link_strings, 'gi')
        : new RegExp ('(' + Drupal.t('download') + ')|(\\s' + Drupal.t('tab') + ')|(' + Drupal.t('window') + ')', 'gi'),
    linkIgnoreStrings: !!dS.ignore_link_strings ? dS.ignore_link_strings.split(',')
      :  [Drupal.t('link is external'), Drupal.t('link sendS email')],
    linkIgnoreSelector: !!dS.link_ignore_selector ? dS.link_ignore_selector : false,
    hiddenHandlers: !!dS.hidden_handlers ? dS.hidden_handlers : '',
    constrainButtons: !!dS.element_hides_overflow ? dS.element_hides_overflow : '',
    theme: !!dS.theme ? dS.theme : 'sleekTheme',
    embeddedContent: !!dS.embedded_content_warning ? dS.embedded_content_warning : false,
    documentLinks: !!dS.download_links ? dS.download_links : `a[href$='.pdf'], a[href*='.pdf?']`,
    customTests: dS.custom_tests,
    cssUrls: !!dS.css_url ? [dS.css_url + '/library/dist/editoria11y.min.css'] : false,
    ignoreTests: dS.ignore_tests ? dS.ignore_tests : false, // @todo merge needs rewrite.
    reportsURL: !!dS.view_reports ? dS.dashboard_url : false,
  };

  // todo postpone: store dismissalKeys for PDFs in page results, and check dismissals table for page level matches on load.

  let editors = (Drupal.editors && (Object.hasOwn(Drupal.editors, 'ckeditor5') || Object.hasOwn(Drupal.editors, 'gutenberg')));
  // As of 2.2.10, ignore front-end editors (rich text comment fields).
  if (editors) {
    options.inlineAlerts = false;
    const editRoutes = /(node|term|user)\/\d+\/edit/;
    // @todo: does this need to be a parameter?
    if (!drupalSettings.path.currentPathIsAdmin &&
      !drupalSettings.path.currentPath.match(editRoutes) ) {
      editors = false;
    }
  }
  if (editors) {
    options.watchForChanges = true;
    if (Object.hasOwn(Drupal.editors, 'gutenberg')) {
      options.buttonZIndex = 1000;
    } else {
      // CKEditor injects a label that messes up the "text + alt" link test.
      options.ignoreAriaOnElements = '[data-drupal-media-preview], [data-drupal-entity-preview]';
    }
  } else {
    options.watchForChanges = dS.watch_for_changes === 'checkRoots' ?
      'checkRoots' :
      dS.watch_for_changes !== 'false';
  }

  let delay = drupalSettings.path.currentPathIsAdmin ? 250 : 0;
  // Way too many race conditions on admin side.
  if (document.URL.indexOf('mode=same_page_preview') > -1 || (
    drupalSettings.path.currentPathIsAdmin &&
    dS.disable_live === true
  )) {
    ed11yOnce = true;
    ed11yInitialized = 'disabled';
    return;
  } else if (drupalSettings.path.currentPathIsAdmin && !editors) {
    // Ed11y will init later if a behavior brings in something editable.
    ed11yInitialized = false;
    return;
  }

  if (document.querySelector('.layout-builder-form')) {
    // Layout builder is not compatible.
    ed11yOnce = true;
    ed11yInitialized = 'disabled';
    return;
  } else if (editors) {
    // Editable content is present, optimize for speed.
    options.autoDetectShadowComponents = false;
    options.ignoreContentOutsideRoots = true; // @todo 3.x is there also config for this?

    if (Object.hasOwn(Drupal.editors, 'gutenberg')) {
      options.ignoreAriaOnElements = 'h1,h2,h3,h4,h5,h6';
      delay = 1000;
      window.setTimeout(function () {
        if (Ed11y.results.length === 0) {
          // Ed11y fails to initialize if Gutenberg is really late.
          Ed11y.checkAll();
        }
      }, 6000);
    }
    options.checkRoot = '.gutenberg__editor .is-root-container, [contenteditable="true"]:not(.gutenberg__editor [contenteditable], [contenteditable="true"] [contenteditable])';
    options.ignoreElements += ', [hidden], [style*="display: none"], [style*="display:none"], [hidden] *, [style*="display: none"] *, [style*="display:none"] *, [data-drupal-message-type]';
    // todo merge
    options.ignoreAllIfAbsent = options.ignoreAllIfAbsent ?
      options.ignoreAllIfAbsent + ', [contenteditable="true"], .gutenberg__editor .is-root-container':
      '[contenteditable="true"], .gutenberg__editor .is-root-container';

    options.initialHeadingLevel = [];
    if (dS.live_h2) {
      options.initialHeadingLevel.push(
        {
          selector: dS.live_h2,
          previousHeading: 1,
        }
      );
    }
    if (dS.live_h3) {
      options.initialHeadingLevel.push(
        {
          selector: dS.live_h3,
          previousHeading: 2,
        }
      );
    }
    if (dS.live_h4) {
      options.initialHeadingLevel.push(
        {
          selector: dS.live_h4,
          previousHeading: 3,
        }
      );
    }
    if (dS.live_h_inherit) {
      options.initialHeadingLevel.push(
        {
          selector: dS.live_h_inherit,
          previousHeading: 'inherit',
        }
      );
    }
    options.initialHeadingLevel.push({
      selector: '*',
      previousHeading: 0, // Ignores first heading for level skip detection.
    });
  }

  options.alertMode = dS.assertiveness ? dS.assertiveness : 'assertive';
  // If assertiveness is "smart" we set it to assertive if the doc was recently changed.
  const now = new Date();
  if (drupalSettings.path.currentPathIsAdmin && (Drupal.editors && (Object.hasOwn(Drupal.editors, 'ckeditor5') || Object.hasOwn(Drupal.editors, 'gutenberg'))) && (options.alertMode === 'smart' || options.alertMode === 'assertive')) {
    options.alertMode = 'active';
  }
  else if (
    urlParams.has('ed1ref') ||
    (options.alertMode === 'smart' &&
      ((now / 1000) - dS.changed < 60)
    )
  ) {
    options.alertMode = 'assertive';
  }


  // todo postpone: ignoreAllIfPresent
  options.preventCheckingIfPresent = !!dS.no_load ?
    dS.no_load + ', .layout-builder-form' :
    '.layout-builder-form';
  if (!!(parent?.drupalSettings?.canvas) && !parent.document.body.querySelector('[class^=_PagePreviewIframe]')) {
    // Only run when Drupal Canvas is running if it is in Preview mode.
    options.preventCheckingIfPresent = 'body';
  }
  // todo postpone: preventCheckingIfAbsent


  const editSelector = (selector, action) => {
    return `[id$="-local-tasks"] a[href*="/${selector}/"][href$="/${action}"],
            .block-local-tasks-block a[href*="/${selector}/"][href$="/${action}"]`;
  };
  const editLink = document.querySelector(editSelector('node', 'edit'));
  const layoutLink = document.querySelector(editSelector('node', 'layout'));
  const userLink = document.querySelector(editSelector('user', 'edit'));
  const termLink = document.querySelector(editSelector('taxonomy/term', 'edit'));
  if (editLink || layoutLink || userLink || termLink) {
    const editIcon = document.createElement('span');
    editIcon.classList.add('ed11y-custom-edit-icon');
    editIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="currentColor" d="M441 59L453 71c9 9 9 25 0 34L424 134 378 88 407 59c9-9 25-9 34 0zM210 256L344 122 390 168 256 302c-3 3-7 5-10 6l-59 17 17-59c1-4 3-8 6-10zM373 25L176 222c-9 9-15 19-18 31l-29 100c-2 8-.1 17 6 24s15 9 24 6l100-27c12-3 23-10 31-18L487 139c28-28 28-74 0-102L475 25C447-3 401-3 373 25zM88 64C39 64 0 103 0 152L0 424c0 49 39 88 88 88l272 0c49 0 88-39 88-88l0-112c0-13-11-24-24-24s-24 11-24 24l0 112c0 22-18 40-40 40L88 464c-22 0-40-18-40-40l0-272c0-22 18-40 40-40l112 0c13 0 24-11 24-24s-11-24-24-24L88 64z"/></svg>';
    const reLink = function(link, text) {
      const linkButton = document.createElement('a');
      linkButton.href = link.href;
      linkButton.textContent = text;
      linkButton.prepend(editIcon.cloneNode(true));
      return linkButton;
    };
    const editLinks = document.createElement('div');
    if (editLink) {
      editLinks.appendChild(reLink(editLink, Drupal.t('Page editor')));
    }
    if (layoutLink) {
      editLinks.appendChild(reLink(layoutLink, Drupal.t('Layout editor')));
    }
    if (userLink) {
      editLinks.appendChild(reLink(userLink, Drupal.t('Edit user')));
    }
    if (termLink) {
      editLinks.appendChild(reLink(termLink, Drupal.t('Edit term')));
    }
    options.editLinks = editLinks;

    // Set listener to hide links on view.
    if (!!dS.hide_edit_links) {
      document.addEventListener('ed11yPop', e => {
        if (e.detail.result.element.closest(drupalSettings.editoria11y.hide_edit_links)) {
          e.detail.tip.shadowRoot.querySelector('.ed11y-custom-edit-links')?.setAttribute('hidden', '');
        }
      });
    }
  }

  if (typeof editoria11yOptionsOverride !== 'undefined' && typeof editoria11yOptions === 'function') {
    options = editoria11yOptions(options); // @todo run like custom tests?
  }

  ed11yWaiting = true;

  window.setTimeout(function() {
    ed11yInitialized = true;

    // Increase zIndex on tips drawn inside Drupal's modal dialog.
    document.addEventListener('ed11yResultsPainted', function () {
      if (Ed11y.State.inlineAlerts) {
        // Inline alerts inherit z-index.
        return;
      }
      Ed11y.Results?.forEach(result => {
        const inDialog = result?.element?.closest('dialog, [role="dialog"]');
        if (inDialog) {
          result?.toggle?.style.setProperty('--ed11y-buttonZIndex', '99999');
        }
      });
    });

    Ed11y.Lang.addI18n(Sa11yLangEnUS.strings); // @todo language match.
    const ed11y = new Ed11y.Ed11y(options);

    ed11yWaiting = false;

    // When Drupal dialog opens, constrain checks inside dialog.
    let rootsCache;
    let dialogRoots = '';
    document.addEventListener('dialog:aftercreate', function () {
      // @todo merge convert from fixedRoots while running.
      if (!rootsCache) {
        rootsCache = Ed11y.Options.checkRoot;
        const rootsParse = Ed11y.Options.checkRoot.split(',');
        rootsParse.forEach((root, i) => {
          rootsParse[i] = `#drupal-modal ${root}`;
        });
        dialogRoots = rootsParse.join(', ');
      }
      Ed11y.State.checkRoot = dialogRoots;
      Ed11y.State.forceFullCheck = true;
      Ed11y.incrementalCheck(); // todo merge
      // Todo: if Editoria11y disables, drop its zIndex behind the modal?
    });
    document.addEventListener('dialog:afterclose', function () {
      // todo check if there are ANY dialogs still open.
      if (rootsCache) {
        Ed11y.State.checkRoot = Ed11y.Options.checkRoot;
        Ed11y.forceFullCheck = true;
        Ed11y.incrementalCheck();
      }
    });
    window.setTimeout(function() {
      if (Ed11y.State.disabled) {
        reportSyncDone();
        // Tell crawler to move on.
      }
      // Append ?ed1string to URLs to check translations
      if (!urlParams.has('ed1strings')) {
        return;
      }
      if (typeof(drupalTranslations) === 'undefined') {
        console.warn('Editoria11y: No translations present to debug.');
        return;
      }
      const target = document.querySelector('main');
      const wrap = document.createElement('div');
      target.prepend(wrap);
      const missingTranslations = document.createElement('strong');
      missingTranslations.textContent = Drupal.t("Translation needed: ");
      missingTranslations.style.setProperty('border', '1px solid');
      missingTranslations.style.setProperty('filter', 'invert(1)');
      for (const [key, value] of Object.entries(Ed11y.Lang)) { // @todo merge
        if (!ed11yLangDrupal[key]) {
          console.warn(key);
        }
        let checkTranslation = true;
        if (!(drupalTranslations && drupalTranslations.strings && drupalTranslations.problems)) {
          checkTranslation = false;
        }
        let item = document.createElement('div');
        if (value.title && typeof value.tip()) {
          item.textContent = value.tip('example');
          if (checkTranslation && !drupalTranslations.strings[""][value.tip('')]) {
            item.prepend(missingTranslations.cloneNode(true));
          }
          let title = document.createElement('strong');
          title.style.setProperty('display', 'block');
          title.textContent = value.title;
          if (checkTranslation && !drupalTranslations.strings[""][value.title]) {
            title.prepend(missingTranslations.cloneNode(true));
          }
          item.prepend(title);
        } else {
          item.innerHTML = value;
          if (checkTranslation && !(drupalTranslations.strings[""][value] || drupalTranslations.strings.problems[value])) {
            item.prepend(missingTranslations.cloneNode(true));
          }
        }
        const itemKey = document.createElement('em');
        itemKey.textContent = key  + ': ';
        item.prepend(itemKey);
        wrap.append(item);
        const br = document.createElement('br');
        wrap.append(br);
      }

    },100);
  }, delay);

  /**
   * Initiate sync
   *
   * */

  const reportSyncDone = function() {
    if (parent && parent.ed11ySynced) {
      parent.ed11ySynced--;
    }
  };

  let csrfToken = false;
  function getCsrfToken(action, data)
  {
    {
      fetch(`${dS.session_url}`, {
        method: "GET"
      })
        .then(res => res.text())
        .then(token => {
          csrfToken = token;
          postData(action, data).catch(err => console.error(err));
        })
        .catch(err => console.error(err));
    }
  }

  let postData = async function (action, data) {
    if (!csrfToken) {
      getCsrfToken(action, data);
    } else {
      let apiRoot = dS.api_url.replace('results/report','');
      let url = `${apiRoot}${action}`;
      fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrfToken,
        },
        body: JSON.stringify(data),
      }).then(() => reportSyncDone())
        .catch((error) => console.error('Error:', error));
    }
  };

  // Purge changed aliases & deleted pages.
  const ed1Ref = urlParams.has('ed1ref') ? decodeURIComponent(urlParams.get('ed1ref')) : false;
  if (ed1Ref && ed1Ref !== dS.page_path) {
    let data = {
      page_path: ed1Ref,
    };
    window.setTimeout(function() {
      postData('purge/page', data);
    },100,data);
  }

  let results = {};
  let oks = {};
  let hides = {};
  let total = 0;
  let extractResults = function () {
    results = {};
    oks = [];
    hides = [];
    total = 0;
    Ed11y.Results.forEach(result => {
      const testKey = result.test;
      const testName = Ed11y.Lang.langStrings[`${testKey}_TEST_NAME`];
      if (!testName) {
        // @todo 3.x confirm all test names are now in place.
        console.warn('3.x debug test without name: ', result);
      }
      else if (result.dismissalStatus !== "ok") { // @todo merge test
        // log all items not marked as OK
        if (results[testKey]) {
          results[testKey] = {
            count: parseInt(results[testKey].count) + 1,
            result_name: testName,
          };
          total++;
        } else {
          results[testKey] = {
            count: 1,
            result_name: testName,
          };
          total++;
        }
      }
      else {
        // todo 2.3: use dismissalKey instead with a separate row for each dismissalKey
        oks.push({
          resultKey: result.test,
          dismissalKey: result.dismiss,
          resultName: testName,
        });
      }
    });
  };

  let sendResults = function () {
    window.setTimeout(function () {
      total = 0;
      extractResults();
      let data = {
        page_title: dS.page_title,
        page_path: dS.page_path,
        entity_id: dS.entity_id,
        page_count: total,
        language: dS.lang,
        entity_type: dS.entity_type, // node or false
        route_name: dS.route_name, // e.g., entity.node.canonical or view.frontpage.page_1
        results: results,
        hides: hides,
        oks: oks,
      };
      postData('results/report', data);
      // Short timeout to let execution queue clear.
    }, 100);
  };

  let firstRun = true;
  if (dS.dismissals && dS.sync !== 'dismissals' && dS.sync !== 'disable') {
    document.addEventListener('ed11yResults', function () {
      if (firstRun) {
        if ((Ed11y.Results.length > 0 || dS.pid)) {
          sendResults();
        } else {
          reportSyncDone();
        }
        firstRun = false;
      }
    });
  }

  let dismissalsCache = {};
  let dismissalsData = {
    dismissals: [],
  };

  const debounce = (callback, wait) => {
    let timeoutId = null;
    return (...args) => {
      window.clearTimeout(timeoutId);
      timeoutId = window.setTimeout(() => {
        callback.apply(null, args);
      }, wait);
    };
  };

  const sendDismissals = debounce(()=> {
      // Get dynamic title from edit pages.
      // todo: Canvas title selector?
      const editableTitleField = document.querySelector('#edit-title-wrapper input, #edit-name-wrapper input, #edit-name input');
      dismissalsData.page_title = drupalSettings.path.currentPathIsAdmin &&
      editableTitleField && editableTitleField.value ?
        editableTitleField.value :
        dS.page_title;
      dismissalsData.page_path = dS.page_path;
      dismissalsData.entity_id = dS.entity_id;
      dismissalsData.language = dS.lang;
      dismissalsData.entity_type = dS.entity_type; // node or false
      dismissalsData.route_name = dS.route_name; // e.g., entity.node.canonical or view.frontpage.page_1
      postData('dismiss', dismissalsData).then(() => {
        dismissalsData.dismissals = [];
        dismissalsCache = {};
      });
  }, 250);

  const resendResults = debounce(()=> {
    sendResults();
  }, 250);

  const prepareDismissal = function (detail) {
    if (!!detail) {
      if (detail.dismissAction === 'reset') {
        dismissalsCache.dismissals = [];
        dismissalsData = {
          dismissals: [
            {
              dismissal_status: 'reset', // ok, ignore or reset
              result_key: detail.dismissTest, // which test is sending a result
              element_id: detail.dismissKey, // some recognizable attribute of the item marked
            },
          ],
        };
        if (dS.sync !== 'dismissals') {
          window.setTimeout(function() {
            resendResults();
          },500);
        }
      } else if (detail.dismissTest in dismissalsCache && dismissalsCache[detail.dismissTest].includes(detail.dismissKey)) {
        return false;
      } else {
        // Send if we have not already sent the same key.
        if (!(detail.dismissTest in dismissalsCache)) {
          dismissalsCache[detail.dismissTest] = [detail.dismissKey];
        } else {
          dismissalsCache[detail.dismissTest].push(detail.dismissKey);
        }
        dismissalsData.dismissals.push(
          {
            // @todo merge rewrite
            result_name: Ed11y.Lang.langStrings[detail.dismissTest + '_TEST_NAME'], // which test is sending a result
            result_key: detail.dismissTest, // which test is sending a result
            element_id: detail.dismissKey, // some recognizable attribute of the item marked
            dismissal_status: detail.dismissAction, // ok, ignore or reset
          }
        );
        if (detail.dismissAction === 'ok' && dS.sync !== 'dismissals') {
          window.setTimeout(function() {
            resendResults();
          },500);
        }
      }
      window.setTimeout(function() {sendDismissals();}, 0);
    }
  };
  if (dS.dismissals && dS.sync !== 'disable') {
    document.addEventListener('ed11yDismissalUpdate', function (e) {
      prepareDismissal(e.detail);
    }, false);
  }

  ed11yOnce = true;
};

Drupal.behaviors.editoria11y = {
  attach: function (context) {
    "use strict";

    if (ed11yInitialized === true && ed11yOnce) {
      // Recheck page about a second after every behavior.
      // Todo: global mutation watch instead or in addition?
      window.setTimeout(function () {
        Ed11y.forceFullCheck = true;
        if (drupalSettings.editor || typeof(DrupalGutenberg) === 'object') {
          Ed11y.State.inlineAlerts = false;
        }
        if (Ed11y.bodyStyle) { // @todo merge rewrite
          // todo: shouldn't forceFull make this not necessary?
          Ed11y.incrementalCheck();
        } else {
          Ed11y.checkAll();
        }
      }, 1000);
    } else if (ed11yOnce &&
      (!ed11yInitialized ||
        ed11yInitialized !== 'pending'
      ) &&
      !drupalSettings.editoria11y.disable_live &&
      Drupal.editors &&
      (Object.hasOwn(Drupal.editors, 'ckeditor5') ||
        Object.hasOwn(Drupal.editors, 'gutenberg'))) {
      window.setTimeout(function () {
        if (ed11yInitialized !== true) {
          ed11yInitializer();
        }
      }, 1000);

    }

    if (context === document && !ed11yOnce && CSS.supports('selector(:is(body))')) {
      ed11yOnce = true;
      // Timeout necessary to prevent Paragraphs needing 2 clicks to open.
      window.setTimeout(()=> {
        ed11yInitializer();
      }, 100);
    }
  }
};

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

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