A11y

Test for assessibility problems

目前為 2018-07-14 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name A11y
  3. // @namespace assessibility.colivre.org
  4. // @version 0.3
  5. // @description Test for assessibility problems
  6. // @author Aurélio A. Heckert
  7. // @match *://*/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Test if element has acessibility problems
  15. // If there is a problem, call `notify(el, <ERROR|WARN>, '<description string>')`
  16. function testA11yEl(el) {
  17. var id = el.id
  18.  
  19. if (el.tagName == 'IMG' && emptyVal(el.alt) && emptyVal(el.title))
  20. notify(el, ERROR, 'Image without alt text.', 'https://www.w3.org/WAI/tutorials/images/');
  21.  
  22. if (el.tagName == 'A' && noText(el))
  23. notify(el, ERROR, 'Link without text.');
  24.  
  25. if (el.tagName == 'BUTTON' && noText(el))
  26. notify(el, ERROR, 'Button without text.');
  27.  
  28. if (el.tagName.match(/^H[0-9]$/) && noText(el))
  29. notify(el, ERROR, `Heading ${el.tagName} without text.`);
  30.  
  31. if (el.tagName.match(/^H[0-9]$/) && el.nextSibling &&
  32. el.nextSibling.tagName && // The next tag are united to this.
  33. el.nextSibling.tagName.match(/^H[0-9]$/))
  34. notify(el, WARN, `Skipping heading ${el.tagName}.`, 'https://www.w3.org/WAI/tutorials/page-structure/headings#heading-ranks');
  35.  
  36. if (el.tagName.match(/^H[0-9]$/) && el.nextSibling && el.nextElementSibling &&
  37. el.nextSibling.constructor == Text && el.nextSibling.textContent.match(/^\s*$/) && // has no text between, only space.
  38. el.nextElementSibling.tagName.match(/^H[0-9]$/))
  39. notify(el, WARN, `Skipping heading ${el.tagName}.`, 'https://www.w3.org/WAI/tutorials/page-structure/headings#heading-ranks');
  40.  
  41. if (el.tagName.match(/INPUT|TEXTAREA|SELECT/) &&
  42. !(el.type=='submit' && !emptyVal(el.value)) &&
  43. !(el.type=='hidden') &&
  44. !(
  45. findAncestor(el, (a)=>a.tagName == 'LABEL') /* it is children of a <label> */ ||
  46. (id && document.querySelector(`label[for=${id}]`)) /* a <label> points to it */
  47. )
  48. )
  49. notify(el, ERROR, `Formfield ${el.tagName} without label.`, 'http://webaim.org/standards/508/checklist#standardn');
  50.  
  51. }
  52.  
  53. function noText(el) {
  54. var imgs = []
  55. imgs.push.apply(imgs, el.querySelectorAll('img'));
  56. return emptyVal(el) && emptyVal(el.title) && emptyVal(imgs.map((img)=> img.alt).join(''))
  57. }
  58.  
  59. function findAncestor(el, matchFunc) {
  60. while (el = el.parentNode) {
  61. if (matchFunc(el)) return el;
  62. }
  63. return null;
  64. }
  65.  
  66. var elements = [];
  67. var a11yBox, a11yList, pointer;
  68. const ERROR = 'ERROR';
  69. const WARN = 'WARN';
  70. const BOXID = 'a11y-userscript-box';
  71.  
  72. const $ = function(query) { return document.querySelector(query) };
  73. const $$ = function(query) { return document.querySelectorAll(query) };
  74.  
  75. function emptyVal(val) {
  76. if (typeof(val) == 'string') val = val.replace(/\s/g, '');
  77. return typeof(val) == 'undefined' || val == null || val.length == 0
  78. }
  79.  
  80. function mk(tag, attrs) {
  81. tag = document.createElement(tag);
  82. for (var attName in attrs) {
  83. var attVal = attrs[attName];
  84. if (attName == 'children') attVal.forEach(([t,a])=> {
  85. mk(t, Object.assign(a, {parent: tag}));
  86. });
  87. else if (attName == 'parent') attVal.appendChild(tag);
  88. else if (attName == 'text') tag.innerText = attVal;
  89. else if (attName == 'html') tag.innerHTML = attVal;
  90. else if (attName.match(/^on/)) tag[attName] = attVal;
  91. else tag.setAttribute(attName, attVal);
  92. }
  93. return tag;
  94. }
  95.  
  96. function buildA11yBox() {
  97. a11yBox = mk('div', {
  98. parent: document.body,
  99. id: BOXID,
  100. class: BOXID+'-toggle-min',
  101. children: [
  102. ['i',{ id: BOXID+'-toggle-min-bt', onclick: toggleMin }]
  103. ]
  104. });
  105. a11yList = mk('ul', {parent: a11yBox});
  106. mk('style', {
  107. parent: document.documentElement.firstElementChild,
  108. html: `
  109. #${BOXID} {position: fixed; bottom: 0; right: 0; background: rgba(255,255,255,0.7); z-index: 99999; opacity: 1;
  110. box-shadow: 0 0 30px rgba(0,0,0,0.6); padding: 15px; border-radius: 15px 0 0 0; color: #000}
  111. #${BOXID}-toggle-min-bt::before {content: "\\00d7"; position: absolute; top: 2px; left: 2px;
  112. display: block; text-align: center; border: 1px solid #CCC; border-radius: 30px;
  113. font-size: 24px; line-height: 24px; width: 24px; background: #EEE; cursor: pointer}
  114. #${BOXID} ul {margin: 0; padding: 0; border: 1px solid rgba(0,0,0,0.2);
  115. border-top:none; max-height: 90vh; overflow: auto}
  116. #${BOXID} li {margin: 0; padding: 6px 10px; border-top: 1px solid rgba(0,0,0,0.1); list-style: none; cursor: pointer}
  117. .${BOXID}-toggle-min {opacity: 40%}
  118. .${BOXID}-toggle-min ul {display: none}
  119. .${BOXID}-ERROR {background: rgba(255,200,200,0.8)}
  120. .${BOXID}-WARN {background: rgba(255,250,200,0.8)}
  121. #${BOXID} li span { font-size: 70%; padding-right: 1em }
  122. #${BOXID} li p { display: inline; margin: 0 }
  123. #${BOXID} li a { display: inline-block; margin: 0 0 0 3px; padding: 1px 2px; text-decoration: none;
  124. border: 1px solid rgba(0,0,0,0.2); border-radius: 3px; color: #000 }
  125. #${BOXID}-pointer { position: absolute; left: -1000px; z-index: 99990;
  126. border: 2px dashed #FFF; box-shadow: 0 0 110px 25px rgba(0,0,0,0.6) }
  127. #${BOXID}-pointer i { position: absolute; color: red; text-shadow: 1px 1px 1px #000, 0 0 8px #000;
  128. font-size: 90px; line-height: 90px; text-align: center }
  129. #${BOXID}-pointer-N { top: -90px; width: 100% }
  130. #${BOXID}-pointer-S { bottom: -90px; width: 100% }
  131. #${BOXID}-pointer-W { left: -90px }
  132. #${BOXID}-pointer-E { right: -90px }
  133. `
  134. });
  135. pointer = mk('div', {
  136. parent: document.body,
  137. id: BOXID+'-pointer',
  138. children: [
  139. ['i', {id: BOXID+'-pointer-N', text: '↓'}],
  140. ['i', {id: BOXID+'-pointer-S', text: '↑'}],
  141. ['i', {id: BOXID+'-pointer-W', text: '→'}],
  142. ['i', {id: BOXID+'-pointer-E', text: '←'}]
  143. ]
  144. });
  145. }
  146.  
  147. function toggleMin() {
  148. a11yBox.className = (a11yBox.className.length > 0)? '' : BOXID+'-toggle-min';
  149. }
  150.  
  151. function notificationClick(notification, el) {
  152. var rect = el.getBoundingClientRect();
  153. var x = parseInt(rect.left + window.pageXOffset - 2);
  154. var y = parseInt(rect.top + window.pageYOffset - 2);
  155. var w = parseInt(rect.width + 4);
  156. var h = parseInt(rect.height + 4);
  157. pointer.style.left = x + 'px';
  158. pointer.style.top = y + 'px';
  159. pointer.style.width = w + 'px';
  160. pointer.style.height = h + 'px';
  161. $('#'+BOXID+'-pointer-W').style.lineHeight = h + 'px';
  162. $('#'+BOXID+'-pointer-E').style.lineHeight = h + 'px';
  163. window.scrollTo(parseInt(x-screen.width/4), parseInt(y-screen.height/4));
  164. console.log(el);
  165. }
  166.  
  167. function notify(el, type, message, helpURL) {
  168. if (!a11yBox) buildA11yBox();
  169. if (elements.filter((reg)=> reg.el==el && reg.message==message).length > 0) return;
  170. elements.push({ el, message });
  171. var li = mk('li', {
  172. parent: a11yList,
  173. class: BOXID+'-'+type,
  174. onclick: ()=> notificationClick(this, el),
  175. children: [
  176. ['span', {text: type}],
  177. ['p', {text: message}],
  178. ]
  179. });
  180. if (helpURL) mk('a', {
  181. href: helpURL,
  182. target: '_blank',
  183. text: 'help',
  184. parent: li
  185. });
  186. }
  187.  
  188. function walk(el) {
  189. if (el.id == BOXID) return;
  190. testA11yEl(el);
  191. for (var e,i=0; e=el.children[i]; i++) ((e)=> setTimeout(()=> walk(e), 1))(e);
  192. }
  193.  
  194. function testPage() {
  195. console.log('testing page...');
  196. walk(document.body);
  197. }
  198.  
  199. testPage();
  200. setInterval(testPage, 15000);
  201. })();