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