[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 // This file is part of Moodle - http://moodle.org/ 2 // 3 // Moodle is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // (at your option) any later version. 7 // 8 // Moodle is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU General Public License for more details. 12 // 13 // You should have received a copy of the GNU General Public License 14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 15 16 /* 17 * @package atto_accessibilitychecker 18 * @copyright 2014 Damyon Wiese <damyon@moodle.com> 19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 20 */ 21 22 /** 23 * @module moodle-atto_accessibilitychecker-button 24 */ 25 26 /** 27 * Accessibility Checking tool for the Atto editor. 28 * 29 * @namespace M.atto_accessibilitychecker 30 * @class Button 31 * @extends M.editor_atto.EditorPlugin 32 */ 33 34 var COMPONENT = 'atto_accessibilitychecker'; 35 36 Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { 37 38 initializer: function() { 39 this.addButton({ 40 icon: 'e/accessibility_checker', 41 callback: this._displayDialogue 42 }); 43 }, 44 45 /** 46 * Display the Accessibility Checker tool. 47 * 48 * @method _displayDialogue 49 * @private 50 */ 51 _displayDialogue: function() { 52 var dialogue = this.getDialogue({ 53 headerContent: M.util.get_string('pluginname', COMPONENT), 54 width: '500px', 55 focusAfterHide: true 56 }); 57 58 // Set the dialogue content, and then show the dialogue. 59 dialogue.set('bodyContent', this._getDialogueContent()) 60 .show(); 61 }, 62 63 /** 64 * Return the dialogue content for the tool. 65 * 66 * @method _getDialogueContent 67 * @private 68 * @return {Node} The content to place in the dialogue. 69 */ 70 _getDialogueContent: function() { 71 var content = Y.Node.create('<div style="word-wrap: break-word;"></div>'); 72 content.append(this._getWarnings()); 73 74 // Add ability to select problem areas in the editor. 75 content.delegate('click', function(e) { 76 e.preventDefault(); 77 78 var host = this.get('host'), 79 node = e.currentTarget.getData('sourceNode'), 80 dialogue = this.getDialogue(); 81 82 if (node) { 83 // Focus on the editor as we hide the dialogue. 84 dialogue.set('focusAfterHide', this.editor).hide(); 85 86 // Then set the selection. 87 host.setSelection(host.getSelectionFromNode(node)); 88 } else { 89 // Hide the dialogue. 90 dialogue.hide(); 91 } 92 }, 'a', this); 93 94 return content; 95 }, 96 97 /** 98 * Find all problems with the content editable region. 99 * 100 * @method _getWarnings 101 * @return {Node} A complete list of all warnings and problems. 102 * @private 103 */ 104 _getWarnings: function() { 105 var problemNodes, 106 list = Y.Node.create('<div></div>'); 107 108 // Images with no alt text or dodgy alt text. 109 problemNodes = []; 110 this.editor.all('img').each(function(img) { 111 var alt = img.getAttribute('alt'); 112 if (typeof alt === 'undefined' || alt === '') { 113 if (img.getAttribute('role') !== 'presentation') { 114 problemNodes.push(img); 115 } 116 } 117 }, this); 118 this._addWarnings(list, M.util.get_string('imagesmissingalt', COMPONENT), problemNodes, true); 119 120 problemNodes = []; 121 this.editor.all('*').each(function(node) { 122 var foreground, 123 background, 124 ratio, 125 lum1, 126 lum2; 127 128 // Check for non-empty text. 129 if (Y.Lang.trim(node.get('text')) !== '') { 130 foreground = node.getComputedStyle('color'); 131 background = node.getComputedStyle('backgroundColor'); 132 133 lum1 = this._getLuminanceFromCssColor(foreground); 134 lum2 = this._getLuminanceFromCssColor(background); 135 136 // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html". 137 if (lum1 > lum2) { 138 ratio = (lum1 + 0.05) / (lum2 + 0.05); 139 } else { 140 ratio = (lum2 + 0.05) / (lum1 + 0.05); 141 } 142 if (ratio <= 4.5) { 143 Y.log('Contrast ratio is too low: ' + ratio + 144 ' Colour 1: ' + foreground + 145 ' Colour 2: ' + background + 146 ' Luminance 1: ' + lum1 + 147 ' Luminance 2: ' + lum2); 148 149 // We only want the highest node with dodgy contrast reported. 150 var i = 0; 151 var found = false; 152 for (i = 0; i < problemNodes.length; i++) { 153 if (node.ancestors('*').indexOf(problemNodes[i]) !== -1) { 154 // Do not add node - it already has a parent in the list. 155 found = true; 156 break; 157 } else if (problemNodes[i].ancestors('*').indexOf(node) !== -1) { 158 // Replace the existing node with this one because it is higher up the DOM. 159 problemNodes[i] = node; 160 found = true; 161 break; 162 } 163 } 164 if (!found) { 165 problemNodes.push(node); 166 } 167 } 168 } 169 }, this); 170 this._addWarnings(list, M.util.get_string('needsmorecontrast', COMPONENT), problemNodes, false); 171 172 // Check for lots of text with no headings. 173 if (this.editor.get('text').length > 1000 && !this.editor.one('h3, h4, h5')) { 174 this._addWarnings(list, M.util.get_string('needsmoreheadings', COMPONENT), [this.editor], false); 175 } 176 177 // Check for tables with no captions. 178 problemNodes = []; 179 this.editor.all('table').each(function(table) { 180 var caption = table.one('caption'); 181 if (caption === null || caption.get('text').trim() === '') { 182 problemNodes.push(table); 183 } 184 }, this); 185 this._addWarnings(list, M.util.get_string('tablesmissingcaption', COMPONENT), problemNodes, false); 186 187 // Check for tables with merged cells. 188 problemNodes = []; 189 this.editor.all('table').each(function(table) { 190 var caption = table.one('[colspan],[rowspan]'); 191 if (caption !== null) { 192 problemNodes.push(table); 193 } 194 }, this); 195 this._addWarnings(list, M.util.get_string('tableswithmergedcells', COMPONENT), problemNodes, false); 196 197 // Check for tables with no row/col headers 198 problemNodes = []; 199 this.editor.all('table').each(function(table) { 200 if (table.one('tr').one('td')) { 201 // First row has a non-header cell, so all rows must have at least one header. 202 table.all('tr').some(function(row) { 203 var header = row.one('th'); 204 if (!header || (header.get('text').trim() === '')) { 205 problemNodes.push(table); 206 return true; 207 } 208 return false; 209 }, this); 210 } else { 211 // First row must have at least one header then. 212 var hasHeader = false; 213 table.one('tr').all('th').some(function(header) { 214 hasHeader = true; 215 if (header.get('text').trim() === '') { 216 problemNodes.push(table); 217 return true; 218 } 219 return false; 220 }); 221 if (!hasHeader) { 222 problemNodes.push(table); 223 } 224 } 225 }, this); 226 this._addWarnings(list, M.util.get_string('tablesmissingheaders', COMPONENT), problemNodes, false); 227 228 if (!list.hasChildNodes()) { 229 list.append('<p>' + M.util.get_string('nowarnings', COMPONENT) + '</p>'); 230 } 231 232 // Return the list of current warnings. 233 return list; 234 }, 235 236 /** 237 * Generate the HTML that lists the found warnings. 238 * 239 * @method _addWarnings 240 * @param {Node} A Node to append the html to. 241 * @param {String} description Description of this failure. 242 * @param {array} nodes An array of failing nodes. 243 * @param {boolean} imagewarnings true if the warnings are related to images, false if text. 244 */ 245 _addWarnings: function(list, description, nodes, imagewarnings) { 246 var warning, fails, i, src, textfield, li, link, text; 247 248 if (nodes.length > 0) { 249 warning = Y.Node.create('<p>' + description + '</p>'); 250 fails = Y.Node.create('<ol class="accessibilitywarnings"></ol>'); 251 i = 0; 252 for (i = 0; i < nodes.length; i++) { 253 li = Y.Node.create('<li></li>'); 254 if (imagewarnings) { 255 src = nodes[i].getAttribute('src'); 256 link = Y.Node.create('<a href="#"><img src="' + src + '" /> ' + src + '</a>'); 257 } else { 258 textfield = ('innerText' in nodes[i]) ? 'innerText' : 'textContent'; 259 text = nodes[i].get(textfield).trim(); 260 if (text === '') { 261 text = M.util.get_string('emptytext', COMPONENT); 262 } 263 if (nodes[i] === this.editor) { 264 text = M.util.get_string('entiredocument', COMPONENT); 265 } 266 link = Y.Node.create('<a href="#">' + text + '</a>'); 267 } 268 link.setData('sourceNode', nodes[i]); 269 li.append(link); 270 fails.append(li); 271 } 272 273 warning.append(fails); 274 list.append(warning); 275 } 276 }, 277 278 /** 279 * Convert a CSS color to a luminance value. 280 * 281 * @method _getLuminanceFromCssColor 282 * @param {String} colortext The Hex value for the colour 283 * @return {Number} The luminance value. 284 * @private 285 */ 286 _getLuminanceFromCssColor: function(colortext) { 287 var color; 288 289 if (colortext === 'transparent') { 290 colortext = '#ffffff'; 291 } 292 color = Y.Color.toArray(Y.Color.toRGB(colortext)); 293 294 // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html". 295 var part1 = function(a) { 296 a = parseInt(a, 10) / 255.0; 297 if (a <= 0.03928) { 298 a = a / 12.92; 299 } else { 300 a = Math.pow(((a + 0.055) / 1.055), 2.4); 301 } 302 return a; 303 }; 304 305 var r1 = part1(color[0]), 306 g1 = part1(color[1]), 307 b1 = part1(color[2]); 308 309 return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1; 310 } 311 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |