medialize/ally.js

View on GitHub
build/data-tables/templates/document.hbs

Summary

Maintainability
Test Coverage
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>{{ title }}</title>

  <style id="prism-styles">{{> prismCss }}</style>
  <style>
    [title] {
      cursor: help;
    }
    body {
      margin: 0;
      padding: 5px;
    }
    table {
      border-collapse: collapse;
      width: 100%;
    }
    table caption,
    table th,
    table td {
      padding: 5px;
      border: 1px solid #aaaaaa;
      background: white;
    }
    table caption {
      text-align: left;
      background: #aaaaaa;
      color: white;
    }
    tbody th {
      text-align: left;
      font-size: 14px;
    }
    tbody td {
      text-align: center;
    }
    td > span.compare {
      display: block;
    }
    td > span.compare.mismatch {
      margin: 0 -5px -5px;
      border: 5px solid red;
    }

    thead th[data-column="ident"] {
      text-align: left;
    }

    table:not(#sticky-table-header) + table thead {
      /*
        visually hide consecutive headers
        see https://w3c.github.io/wai-tutorials/forms/labels/#note-on-hiding-elements
      */
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      border: 0;
      overflow: hidden;
      clip: rect(0 0 0 0);
    }

    table td[data-focusable="true"],
    table td > span.compare[data-focusable="true"] {
      /* focusable */
      background: #FFDC00;
    }
    table td[data-tabbable="true"],
    table td > span.compare[data-tabbable="true"] {
      /* tabbable */
      background: #01FF70;
    }
    table td[data-focusable="false"][data-tabbable="true"],
    table td > span.compare[data-focusable="false"][data-tabbable="true"] {
      /* only tabbable */
      background: #19F1FF;
    }
    table td[data-focusable="false"][data-tabbable="false"],
    table td > span.compare[data-focusable="false"][data-tabbable="false"] {
      /* inert */
      background: #DDDDDD;
    }
    table td[data-redirecting-to],
    table td > span.compare[data-redirecting-to] {
      /* redirecting */
      background: #FFA9F9 !important;
    }
    table td[data-focusable="false"][data-tabbable="false"][data-focus-host] {
      /* hosting focused element */
      background: #73CCC2;
    }

    table td.irrelevant-to-context {
      opacity: 0.5;
    }

    /* inline foot-note boxes */
    tbody td {
      position: relative;
      padding: 12px 5px 5px;
      margin: 1px;
    }
    tbody th {
      position: relative;
      padding: 5px 5px 12px 5px;
      margin: 1px;
      background: none;
    }
    tbody th .notes {
      bottom: 1px;
      right: 1px;
    }
    tbody td .notes {
      top: 1px;
      left: 1px;
    }
    tbody .notes {
      position: absolute;
      line-height: 0;
      font-size: 0;
    }
    tbody .notes .note {
      display: inline-block;
      padding: 2px;
      font-size: 10px;
      line-height: 8px;
      color: #666;
      background: #EEE;
      text-decoration: none;
    }
    tbody .notes > .note + .note {
      margin-left: 1px;
    }
    tbody .notes > .note.tabindex-value {
      background: rgba(0, 0, 0, 0.1);
    }
    tbody .notes > .note.warning {
      color: white;
      background: red;
    }
    tbody .notes > .note.target {
      color: white;
      background: black;
    }
    tbody .notes > .note.compare-target {
      color: white;
      background: black;
    }
    #footnotes-list li:target {
      background: #EEE;
    }
    #floating-footnote {
      position: absolute;
      max-width: 500px;
      padding: 10px;
      border: 1px solid black;
      background: white;
    }
    #floating-footnote h3 {
      margin-top: 0;
    }

    #sticky-table-header {
      position: fixed;
      top: 0;
      left: 0;
      margin: 0 5px;
      z-index: 1000;
    }
    #sticky-table-header caption {
      position: relative;
    }
    #sticky-table-header caption ul {
      position: absolute;
      display: none;
      top: 14px;
      left: 0px;
      font-size: 14px;
      background: white;
      border: 2px solid #AAA;
    }
    #sticky-table-header caption ul li {
      display: block;
      margin: 0;
    }
    #sticky-table-header caption:hover ul {
      display: block;
    }

    code[class*="language-"] {
      white-space: pre-line;
    }

    code .token.tag {
      white-space: pre;
    }

  </style>
</head>
<body>

  <h1>{{ title }}</h1>

  {{{ introduction }}}

  {{#if groups}}
    <nav id="table-of-contents">
      <h1>Table Of Contents</h1>
      <ul>
      {{#each groups}}
        <li><a href="#{{ id }}">{{ label }}</a></li>
      {{/each}}

        <li><a href="#footnotes">Footnotes</a></li>
      </ul>
    </nav>
  {{/if}}

  {{{ table }}}

  <h2 id="footnotes">Footnotes</h2>
  <ul id="footnotes-list">
    <li id="footnote-M">The DOM Element Interface is missing the focus method.</li>
    <li id="footnote-E">The <code>focus</code> event is not emitted upon the element becoming the active element (<code>document.activeElement</code>).</li>
    <li id="footnote-S">The <code>:focus</code> CSS pseudo class is not set on the element when it is the active element (<code>document.activeElement</code>).</li>
    <li id="footnote-C">The element becomes the active element (<code>document.activeElement</code>) when an element of <code>contentDocument</code> or the <code>shadowRoot</code> has focus.</li>

    {{#each notes}}
      <li id="footnote-{{ @key }}">
        <h3>Note {{ @key }}</h3>
        {{{ this }}}
      </li>
    {{/each}}
  </ul>

  <script>

    var headerTable;
    var headerTableCaption;
    var headerHeight = 0;

    // make sure all tables have the same width
    function justifyTableWidth() {
      var tables = [].slice.call(document.querySelectorAll('table'), 0);
      var largestIdentWidth = 0;
      var largestDataCellWidth = 0;

      // make the table consume the space it requires
      tables.map(function(table) {
        table.style.position = 'absolute';
        table.style.width = 'auto';
      });
      // find the widest <th> and <td> to align everything to
      tables.map(function(table) {
        largestIdentWidth = Math.max(largestIdentWidth, table.querySelector('tbody th').offsetWidth);
        largestDataCellWidth = Math.max.apply(Math, [].map.call(table.querySelectorAll('tbody tr:first-child td'), function(cell) {
          return cell.offsetWidth;
        }).concat([largestDataCellWidth]));
      });

      var dataCells = (tables[0].querySelectorAll('col').length - 1) * largestDataCellWidth;

      // align width of <table>, <th> and <td>
      tables.map(function(table) {
        table.style.position = '';
        table.style.width = (dataCells + largestIdentWidth) + 'px';
        [].forEach.call(table.querySelectorAll('col'), function(col) {
          if (col.getAttribute('data-column') === 'ident') {
            return;
          }

          col.style.width = largestDataCellWidth + 'px';
        });
      });
    }

    // create sticky header table from first actual table
    function createHeaderTable() {
      var firstTable = document.querySelector('table');
      headerTable = firstTable.cloneNode(true);
      [].forEach.call(headerTable.querySelectorAll('tbody, tfoot'), function(element) {
        element.parentNode.removeChild(element);
      });
      [].forEach.call(headerTable.querySelectorAll('th, td'), function(element) {
        element.setAttribute('id', '');
      });

      var caption = headerTable.querySelector('caption');
      headerTableCaption = document.createElement('span');
      caption.innerHTML = '';
      caption.appendChild(headerTableCaption);

      headerTable.id = 'sticky-table-header';
      headerTable.setAttribute('aria-hidden', 'true');
      headerTable.style.width = firstTable.offsetWidth + 'px';
      window.addEventListener('resize', function() {
        headerTable.style.width = firstTable.offsetWidth + 'px';
      }, true);

      firstTable.parentNode.insertBefore(headerTable, firstTable);
      headerHeight = headerTable.offsetHeight;
    }

    function addTableOfContentsToStickyHeader() {
      var toc = document.querySelector('#table-of-contents ul');
      var list = toc.cloneNode(true);
      headerTableCaption.parentNode.appendChild(list);
    }

    // listen to scroll position to show/hide sticky header and update its caption
    function bindHeaderTableScrolling() {
      var headerCaption = headerTableCaption.textContent;
      headerTable.style.display = 'none';

      var timer;
      var handleHeaderTable = function() {
        if (headerTable.style.display !== 'none') {
          headerTable.style.display = 'none';
        }

        var elementAtPoint = document.elementFromPoint(50, 0);
        if (!elementAtPoint || !elementAtPoint.closest('table')) {
          return;
        }

        // make sticky header follow horizontal scroll
        var horizontalScrollOffset = (document.scrollingElement || document.body).scrollLeft
          || document.documentElement.scrollLeft
          || window.scrollX;

        headerTable.style.left = (0 - horizontalScrollOffset) + 'px';

        // identify the visible part of the table to update the sticky caption
        var tableAtPoint = document.elementFromPoint(50, headerHeight + 20);
        var table = tableAtPoint && tableAtPoint.closest('table');
        var caption = table && table.querySelector('caption');

        if (caption) {
          if (caption.textContent !== headerCaption) {
            headerCaption = caption.textContent;
            headerTableCaption.textContent = headerCaption;
          }

          headerTable.style.display = '';
        }
      };

      var handleScrollEvent = function() {
        timer && clearTimeout(timer);
        timer = setTimeout(handleHeaderTable, 50);
      };

      window.addEventListener('scroll', handleScrollEvent, true);
      handleScrollEvent();
    }

    // add hight of header to fragment URI targets
    function handleHashchangeEvent() {
      var id = location.hash.slice(1);
      var target = id && document.getElementById(id);
      if (!target) {
        return;
      }

      target.scrollIntoView({
        block: 'start',
        behavior: 'instant',
      });

      window.scrollBy(0, 0 - headerHeight);
    }
    function compensateHeaderTable() {
      if (!window.scrollBy) {
        return;
      }

      // https://developer.mozilla.org/en-US/docs/Web/Events/hashchange
      window.addEventListener('hashchange', handleHashchangeEvent, true);
    }

    function bindFootnoteTooltip() {
      var container = document.createElement('div');
      container.id = 'floating-footnote';
      container.hidden = true;
      container.setAttribute('tabindex', '-1');
      document.body.appendChild(container);

      // position the tooltip below the anchor
      document.body.addEventListener('focus', function(event) {
        if (!event.target.classList.contains('note')) {
          return;
        }

        var anchor = event.target;
        var id = anchor.getAttribute('href').slice(1);
        var target = document.getElementById(id);
        var position = anchor.getBoundingClientRect();
        var scrollTop = (document.scrollingElement || document.body).scrollTop
          || document.documentElement.scrollTop
          || window.scrollY;

        container.style.top = (scrollTop + position.top + position.height) + 'px';
        if (position.left + 500 > window.innerWidth) {
          container.style.left = '';
          container.style.right = '5px';
        } else {
          container.style.left = position.left + 'px';
          container.style.right = '';
        }

        container.hidden = false;
        container.innerHTML = target.innerHTML;
      }, true);

      // allow selecting text / clicking things in the tooltip,
      // but hide it when anything else is focused
      var clickedOnContainer = false;
      container.addEventListener('mousedown', function() {
        clickedOnContainer = true;
        setTimeout(function() {
          clickedOnContainer = false;
        }, 100);
      }, true);
      document.body.addEventListener('blur', function(event) {
        if (clickedOnContainer) {
          return;
        }

        container.hidden = true;
      }, true);
      container.addEventListener('blur', function(event) {
        container.hidden = true;
      }, true);

      // hide the container on ESC
      document.body.addEventListener('keydown', function(event) {
        if (event.keyCode !== 27 || container.hidden) {
          return;
        }

        container.blur();
        container.hidden = true;
      }, true);

      // abort jumping to the footnote, as we moved the relevant footnote into view on focus
      document.body.addEventListener('click', function(event) {
        if (!event.target.classList.contains('note') || container.hidden) {
          return;
        }

        event.preventDefault();
      }, true);
    }

    (function() {
      // https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint
      // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
      if (!document.elementFromPoint || !document.body.closest) {
        return;
      }

      justifyTableWidth();

      createHeaderTable();
      addTableOfContentsToStickyHeader();
      bindHeaderTableScrolling();

      compensateHeaderTable();
      document.body.addEventListener('load', function() {
        handleHashchangeEvent();
      });

      if (document.body.hidden !== undefined) {
        bindFootnoteTooltip();
      }

    })();

  </script>

  {{> tracking }}

</body>
</html>