[ 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 146 // We only want the highest node with dodgy contrast reported. 147 var i = 0; 148 var found = false; 149 for (i = 0; i < problemNodes.length; i++) { 150 if (node.ancestors('*').indexOf(problemNodes[i]) !== -1) { 151 // Do not add node - it already has a parent in the list. 152 found = true; 153 break; 154 } else if (problemNodes[i].ancestors('*').indexOf(node) !== -1) { 155 // Replace the existing node with this one because it is higher up the DOM. 156 problemNodes[i] = node; 157 found = true; 158 break; 159 } 160 } 161 if (!found) { 162 problemNodes.push(node); 163 } 164 } 165 } 166 }, this); 167 this._addWarnings(list, M.util.get_string('needsmorecontrast', COMPONENT), problemNodes, false); 168 169 // Check for lots of text with no headings. 170 if (this.editor.get('text').length > 1000 && !this.editor.one('h3, h4, h5')) { 171 this._addWarnings(list, M.util.get_string('needsmoreheadings', COMPONENT), [this.editor], false); 172 } 173 174 // Check for tables with no captions. 175 problemNodes = []; 176 this.editor.all('table').each(function(table) { 177 var caption = table.one('caption'); 178 if (caption === null || caption.get('text').trim() === '') { 179 problemNodes.push(table); 180 } 181 }, this); 182 this._addWarnings(list, M.util.get_string('tablesmissingcaption', COMPONENT), problemNodes, false); 183 184 // Check for tables with merged cells. 185 problemNodes = []; 186 this.editor.all('table').each(function(table) { 187 var caption = table.one('[colspan],[rowspan]'); 188 if (caption !== null) { 189 problemNodes.push(table); 190 } 191 }, this); 192 this._addWarnings(list, M.util.get_string('tableswithmergedcells', COMPONENT), problemNodes, false); 193 194 // Check for tables with no row/col headers 195 problemNodes = []; 196 this.editor.all('table').each(function(table) { 197 if (table.one('tr').one('td')) { 198 // First row has a non-header cell, so all rows must have at least one header. 199 table.all('tr').some(function(row) { 200 var header = row.one('th'); 201 if (!header || (header.get('text').trim() === '')) { 202 problemNodes.push(table); 203 return true; 204 } 205 return false; 206 }, this); 207 } else { 208 // First row must have at least one header then. 209 var hasHeader = false; 210 table.one('tr').all('th').some(function(header) { 211 hasHeader = true; 212 if (header.get('text').trim() === '') { 213 problemNodes.push(table); 214 return true; 215 } 216 return false; 217 }); 218 if (!hasHeader) { 219 problemNodes.push(table); 220 } 221 } 222 }, this); 223 this._addWarnings(list, M.util.get_string('tablesmissingheaders', COMPONENT), problemNodes, false); 224 225 if (!list.hasChildNodes()) { 226 list.append('<p>' + M.util.get_string('nowarnings', COMPONENT) + '</p>'); 227 } 228 229 // Return the list of current warnings. 230 return list; 231 }, 232 233 /** 234 * Generate the HTML that lists the found warnings. 235 * 236 * @method _addWarnings 237 * @param {Node} A Node to append the html to. 238 * @param {String} description Description of this failure. 239 * @param {array} nodes An array of failing nodes. 240 * @param {boolean} imagewarnings true if the warnings are related to images, false if text. 241 */ 242 _addWarnings: function(list, description, nodes, imagewarnings) { 243 var warning, fails, i, src, textfield, li, link, text; 244 245 if (nodes.length > 0) { 246 warning = Y.Node.create('<p>' + description + '</p>'); 247 fails = Y.Node.create('<ol class="accessibilitywarnings"></ol>'); 248 i = 0; 249 for (i = 0; i < nodes.length; i++) { 250 li = Y.Node.create('<li></li>'); 251 if (imagewarnings) { 252 src = nodes[i].getAttribute('src'); 253 link = Y.Node.create('<a href="#"><img src="' + src + '" /> ' + src + '</a>'); 254 } else { 255 textfield = ('innerText' in nodes[i]) ? 'innerText' : 'textContent'; 256 text = nodes[i].get(textfield).trim(); 257 if (text === '') { 258 text = M.util.get_string('emptytext', COMPONENT); 259 } 260 if (nodes[i] === this.editor) { 261 text = M.util.get_string('entiredocument', COMPONENT); 262 } 263 link = Y.Node.create('<a href="#">' + text + '</a>'); 264 } 265 link.setData('sourceNode', nodes[i]); 266 li.append(link); 267 fails.append(li); 268 } 269 270 warning.append(fails); 271 list.append(warning); 272 } 273 }, 274 275 /** 276 * Convert a CSS color to a luminance value. 277 * 278 * @method _getLuminanceFromCssColor 279 * @param {String} colortext The Hex value for the colour 280 * @return {Number} The luminance value. 281 * @private 282 */ 283 _getLuminanceFromCssColor: function(colortext) { 284 var color; 285 286 if (colortext === 'transparent') { 287 colortext = '#ffffff'; 288 } 289 color = Y.Color.toArray(Y.Color.toRGB(colortext)); 290 291 // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html". 292 var part1 = function(a) { 293 a = parseInt(a, 10) / 255.0; 294 if (a <= 0.03928) { 295 a = a / 12.92; 296 } else { 297 a = Math.pow(((a + 0.055) / 1.055), 2.4); 298 } 299 return a; 300 }; 301 302 var r1 = part1(color[0]), 303 g1 = part1(color[1]), 304 b1 = part1(color[2]); 305 306 return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1; 307 } 308 }); 309 310 311 }, '@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 |