Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 (node.hasChildNodes() && Y.Lang.trim(node._node.childNodes[0].nodeValue) !== '') {
130
                foreground = Y.Color.fromArray(
131
                    this._getComputedBackgroundColor(node, node.getComputedStyle('color')),
132
                    Y.Color.TYPES.RGBA
133
                );
134
                background = Y.Color.fromArray(this._getComputedBackgroundColor(node), Y.Color.TYPES.RGBA);
135
 
136
                lum1 = this._getLuminanceFromCssColor(foreground);
137
                lum2 = this._getLuminanceFromCssColor(background);
138
 
139
                // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html".
140
                if (lum1 > lum2) {
141
                    ratio = (lum1 + 0.05) / (lum2 + 0.05);
142
                } else {
143
                    ratio = (lum2 + 0.05) / (lum1 + 0.05);
144
                }
145
                if (ratio <= 4.5) {
146
                    Y.log('Contrast ratio is too low: ' + ratio +
147
                          ' Colour 1: ' + foreground +
148
                          ' Colour 2: ' + background +
149
                          ' Luminance 1: ' + lum1 +
150
                          ' Luminance 2: ' + lum2);
151
 
152
                    // We only want the highest node with dodgy contrast reported.
153
                    var i = 0;
154
                    var found = false;
155
                    for (i = 0; i < problemNodes.length; i++) {
156
                        if (node.ancestors('*').indexOf(problemNodes[i]) !== -1) {
157
                            // Do not add node - it already has a parent in the list.
158
                            found = true;
159
                            break;
160
                        } else if (problemNodes[i].ancestors('*').indexOf(node) !== -1) {
161
                            // Replace the existing node with this one because it is higher up the DOM.
162
                            problemNodes[i] = node;
163
                            found = true;
164
                            break;
165
                        }
166
                    }
167
                    if (!found) {
168
                        problemNodes.push(node);
169
                    }
170
                }
171
            }
172
        }, this);
173
        this._addWarnings(list, M.util.get_string('needsmorecontrast', COMPONENT), problemNodes, false);
174
 
175
        // Check for lots of text with no headings.
176
        if (this.editor.get('text').length > 1000 && !this.editor.one('h3, h4, h5')) {
177
            this._addWarnings(list, M.util.get_string('needsmoreheadings', COMPONENT), [this.editor], false);
178
        }
179
 
180
        // Check for tables with no captions.
181
        problemNodes = [];
182
        this.editor.all('table').each(function(table) {
183
            var caption = table.one('caption');
184
            if (caption === null || caption.get('text').trim() === '') {
185
                problemNodes.push(table);
186
            }
187
        }, this);
188
        this._addWarnings(list, M.util.get_string('tablesmissingcaption', COMPONENT), problemNodes, false);
189
 
190
        // Check for tables with merged cells.
191
        problemNodes = [];
192
        this.editor.all('table').each(function(table) {
193
            var caption = table.one('[colspan],[rowspan]');
194
            if (caption !== null) {
195
                problemNodes.push(table);
196
            }
197
        }, this);
198
        this._addWarnings(list, M.util.get_string('tableswithmergedcells', COMPONENT), problemNodes, false);
199
 
200
        // Check for tables with no row/col headers
201
        problemNodes = [];
202
        this.editor.all('table').each(function(table) {
203
            if (table.one('tr').one('td')) {
204
                // First row has a non-header cell, so all rows must have at least one header.
205
                table.all('tr').some(function(row) {
206
                    var header = row.one('th');
207
                    if (!header || (header.get('text').trim() === '')) {
208
                        problemNodes.push(table);
209
                        return true;
210
                    }
211
                    return false;
212
                }, this);
213
            } else {
214
                // First row must have at least one header then.
215
                var hasHeader = false;
216
                table.one('tr').all('th').some(function(header) {
217
                    hasHeader = true;
218
                    if (header.get('text').trim() === '') {
219
                        problemNodes.push(table);
220
                        return true;
221
                    }
222
                    return false;
223
                });
224
                if (!hasHeader) {
225
                    problemNodes.push(table);
226
                }
227
            }
228
        }, this);
229
        this._addWarnings(list, M.util.get_string('tablesmissingheaders', COMPONENT), problemNodes, false);
230
 
231
        if (!list.hasChildNodes()) {
232
            list.append('<p>' + M.util.get_string('nowarnings', COMPONENT) + '</p>');
233
        }
234
 
235
        // Return the list of current warnings.
236
        return list;
237
    },
238
 
239
    /**
240
     * Generate the HTML that lists the found warnings.
241
     *
242
     * @method _addWarnings
243
     * @param {Node} list Node to append the html to.
244
     * @param {String} description Description of this failure.
245
     * @param {array} nodes An array of failing nodes.
246
     * @param {boolean} imagewarnings true if the warnings are related to images, false if text.
247
     */
248
    _addWarnings: function(list, description, nodes, imagewarnings) {
249
        var warning, fails, i, src, textfield, li, link, text;
250
 
251
        if (nodes.length > 0) {
252
            warning = Y.Node.create('<p>' + description + '</p>');
253
            fails = Y.Node.create('<ol class="accessibilitywarnings"></ol>');
254
            i = 0;
255
            for (i = 0; i < nodes.length; i++) {
256
                li = Y.Node.create('<li></li>');
257
                if (imagewarnings) {
258
                    src = nodes[i].getAttribute('src');
259
                    link = Y.Node.create('<a href="#"><img src="' + src + '" /> ' + src + '</a>');
260
                } else {
261
                    textfield = ('innerText' in nodes[i]) ? 'innerText' : 'textContent';
262
                    text = nodes[i].get(textfield).trim();
263
                    if (text === '') {
264
                        text = M.util.get_string('emptytext', COMPONENT);
265
                    }
266
                    if (nodes[i] === this.editor) {
267
                        text = M.util.get_string('entiredocument', COMPONENT);
268
                    }
269
                    link = Y.Node.create('<a href="#">' + text + '</a>');
270
                }
271
                link.setData('sourceNode', nodes[i]);
272
                li.append(link);
273
                fails.append(li);
274
            }
275
 
276
            warning.append(fails);
277
            list.append(warning);
278
        }
279
    },
280
 
281
    /**
282
     * Convert a CSS color to a luminance value.
283
     *
284
     * @method _getLuminanceFromCssColor
285
     * @param {String} colortext The Hex value for the colour
286
     * @return {Number} The luminance value.
287
     * @private
288
     */
289
    _getLuminanceFromCssColor: function(colortext) {
290
        var color;
291
 
292
        if (colortext === 'transparent') {
293
            colortext = '#ffffff';
294
        }
295
        color = Y.Color.toArray(Y.Color.toRGB(colortext));
296
 
297
        // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html".
298
        var part1 = function(a) {
299
            a = parseInt(a, 10) / 255.0;
300
            if (a <= 0.03928) {
301
                a = a / 12.92;
302
            } else {
303
                a = Math.pow(((a + 0.055) / 1.055), 2.4);
304
            }
305
            return a;
306
        };
307
 
308
        var r1 = part1(color[0]),
309
            g1 = part1(color[1]),
310
            b1 = part1(color[2]);
311
 
312
        return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
313
    },
314
 
315
    /**
316
     * Get the computed RGB converted to full alpha value, considering the node hierarchy.
317
     *
318
     * @method _getComputedBackgroundColor
319
     * @param {Node} node
320
     * @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
321
     * @return {Array} Colour in Array form (RGBA)
322
     * @private
323
     */
324
    _getComputedBackgroundColor: function(node, color) {
325
        color = color || node.getComputedStyle('backgroundColor');
326
 
327
        if (color.toLowerCase() === 'transparent') {
328
            // Y.Color doesn't handle 'transparent' properly.
329
            color = 'rgba(1, 1, 1, 0)';
330
        }
331
 
332
        // Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
333
        var colorParts = Y.Color.toArray(color);
334
        var alpha = colorParts[3];
335
 
336
        if (alpha === 1) {
337
            // If the alpha of the background is already 1, then the parent background colour does not change anything.
338
            return colorParts;
339
        }
340
 
341
        // Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
342
        var parentColor = this._getComputedBackgroundColor(node.get('parentNode'));
343
        return [
344
            // RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
345
            (1 - alpha) * parentColor[0] + alpha * colorParts[0],
346
            (1 - alpha) * parentColor[1] + alpha * colorParts[1],
347
            (1 - alpha) * parentColor[2] + alpha * colorParts[2],
348
            // We always return a colour with full alpha.
349
            1
350
        ];
351
    }
352
});