Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"checker.min.js","sources":["../src/checker.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/*\n * @package    tiny_accessibilitychecker\n * @copyright  2022, Stevani Andolo  <stevani@hotmail.com.au>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {getString, getStrings} from 'core/str';\nimport {component} from './common';\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport ColorBase from './colorbase';\nimport {getPlaceholderSelectors} from 'editor_tiny/options';\n\n/**\n * @typedef ProblemDetail\n * @type {object}\n * @param {string} description The description of the problem\n * @param {ProblemNode[]} problemNodes The list of affected nodes\n */\n\n/**\n * @typedef ProblemNode\n * @type {object}\n * @param {string} nodeName The node name for the affected node\n * @param {string} nodeIndex The indexd of the node\n * @param {string} text A description of the issue\n * @param {string} src The source of the image\n */\n\nexport default class {\n\n    constructor(editor) {\n        this.editor = editor;\n        this.colorBase = new ColorBase();\n        this.modal = null;\n        this.placeholderSelectors = null;\n        const placeholders = getPlaceholderSelectors(this.editor);\n        if (placeholders.length) {\n            this.placeholderSelectors = placeholders.join(', ');\n        }\n    }\n\n    destroy() {\n        delete this.editor;\n        delete this.colorBase;\n\n        this.modal.destroy();\n        delete this.modal;\n    }\n\n    async displayDialogue() {\n        this.modal = await Modal.create({\n            large: true,\n            title: getString('pluginname', component),\n            body: this.getDialogueContent(),\n            show: true,\n        });\n\n        // Destroy the class when hiding the modal.\n        this.modal.getRoot().on(ModalEvents.hidden, () => this.destroy());\n\n        this.modal.getRoot()[0].addEventListener('click', (event) => {\n            const faultLink = event.target.closest('[data-action=\"highlightfault\"]');\n            if (!faultLink) {\n                return;\n            }\n\n            event.preventDefault();\n\n            const nodeName = faultLink.dataset.nodeName;\n            let selectedNode = null;\n            if (nodeName) {\n                if (nodeName.includes(',') || nodeName === 'body') {\n                    selectedNode = this.editor.dom.select('body')[0];\n                } else {\n                    const nodeIndex = faultLink.dataset.nodeIndex ?? 0;\n                    selectedNode = this.editor.dom.select(nodeName)[nodeIndex];\n                }\n            }\n\n            if (selectedNode && selectedNode.nodeName.toUpperCase() !== 'BODY') {\n                this.selectAndScroll(selectedNode);\n            }\n\n            this.modal.hide();\n        });\n    }\n\n    async getAllWarningStrings() {\n        const keys = [\n            'emptytext',\n            'entiredocument',\n            'imagesmissingalt',\n            'needsmorecontrast',\n            'needsmoreheadings',\n            'tablesmissingcaption',\n            'tablesmissingheaders',\n            'tableswithmergedcells',\n        ];\n\n        const stringValues = await getStrings(keys.map((key) => ({key, component})));\n        return new Map(keys.map((key, index) => ([key, stringValues[index]])));\n    }\n\n    /**\n     * Return the dialogue content.\n     *\n     * @return {Promise<Array>} A template promise containing the rendered dialogue content.\n     */\n     async getDialogueContent() {\n        const langStrings = await this.getAllWarningStrings();\n\n        // Translate langstrings into real strings.\n        const warnings = this.getWarnings().map((warning) => {\n            if (warning.description) {\n                if (warning.description.type === 'langstring') {\n                    warning.description = langStrings.get(warning.description.value);\n                } else {\n                    warning.description = warning.description.value;\n                }\n            }\n\n            warning.nodeData = warning.nodeData.map((problemNode) => {\n                if (problemNode.text) {\n                    if (problemNode.text.type === 'langstring') {\n                        problemNode.text = langStrings.get(problemNode.text.value);\n                    } else {\n                        problemNode.text = problemNode.text.value;\n                    }\n                }\n\n                return problemNode;\n            });\n\n            return warning;\n        });\n\n        return Templates.render('tiny_accessibilitychecker/warning_content', {\n            warnings\n        });\n    }\n\n    /**\n     * Set the selection and scroll to the selected element.\n     *\n     * @param {node} node\n     */\n    selectAndScroll(node) {\n        this.editor.selection.select(node).scrollIntoView({\n            behavior: 'smooth',\n            block: 'nearest'\n        });\n    }\n\n    /**\n     * Find all problems with the content editable region.\n     *\n     * @return {ProblemDetail[]} A complete list of all warnings and problems.\n     */\n    getWarnings() {\n        const warnings = [];\n\n        // Check Images with no alt text or dodgy alt text.\n        warnings.push(this.createWarnings('imagesmissingalt', this.checkImage(), true));\n        warnings.push(this.createWarnings('needsmorecontrast', this.checkOtherElements(), false));\n\n        // Check for no headings.\n        if (this.editor.getContent({format: 'text'}).length > 1000 && this.editor.dom.select('h3,h4,h5').length < 1) {\n            warnings.push(this.createWarnings('needsmoreheadings', [this.editor], false));\n        }\n\n        // Check for tables with no captions.\n        warnings.push(this.createWarnings('tablesmissingcaption', this.checkTableCaption(), false));\n\n        // Check for tables with merged cells.\n        warnings.push(this.createWarnings('tableswithmergedcells', this.checkTableMergedCells(), false));\n\n        // Check for tables with no row/col headers.\n        warnings.push(this.createWarnings('tablesmissingheaders', this.checkTableHeaders(), false));\n\n        return warnings.filter((warning) => warning.nodeData.length > 0);\n    }\n\n    /**\n     * Generate the data that describes the issues found.\n     *\n     * @param {String} description Description of this failure.\n     * @param {HTMLElement[]} nodes An array of failing nodes.\n     * @param {boolean} isImageType Whether the warnings are related to image type checks\n     * @return {ProblemDetail[]} A set of problem details\n     */\n    createWarnings(description, nodes, isImageType) {\n        const getTextValue = (node) => {\n            if (node === this.editor) {\n                return {\n                    type: 'langstring',\n                    value: 'entiredocument',\n                };\n            }\n\n            const emptyStringValue = {\n                type: 'langstring',\n                value: 'emptytext',\n            };\n            if ('innerText' in node) {\n                const value = node.innerText.trim();\n                return value.length ? {type: 'raw', value} : emptyStringValue;\n            } else if ('textContent' in node) {\n                const value = node.textContent.trim();\n                return value.length ? {type: 'raw', value} : emptyStringValue;\n            }\n\n            return {type: 'raw', value: node.nodeName};\n        };\n\n        const getEventualNode = (node) => {\n            if (node !== this.editor) {\n                return node;\n            }\n            const childNodes = node.dom.select('body')[0].childNodes;\n            if (childNodes.length) {\n                return document.body;\n            } else {\n                return childNodes;\n            }\n        };\n\n        const warning = {\n            description: {\n                type: 'langstring',\n                value: description,\n            },\n            nodeData: [],\n        };\n\n        warning.nodeData = [...nodes].filter((node) => {\n            // If the failed node is a placeholder element. We should remove it from the list.\n            if (node !== this.editor && this.placeholderSelectors) {\n                return node.matches(this.placeholderSelectors) === false;\n            }\n\n            return node;\n        }).map((node) => {\n            const describedNode = getEventualNode(node);\n\n            // Find the index of the node within the type of node.\n            // This is used to select the correct node when the user selects it.\n            const nodeIndex = this.editor.dom.select(describedNode.nodeName).indexOf(describedNode);\n            const warning = {\n                src: null,\n                text: null,\n                nodeName: describedNode.nodeName,\n                nodeIndex,\n            };\n\n            if (isImageType) {\n                warning.src = node.getAttribute('src');\n            } else {\n                warning.text = getTextValue(node);\n            }\n\n            return warning;\n        });\n\n        return warning;\n    }\n\n    /**\n     * Check accessiblity issue only for img type.\n     *\n     * @return {Node} A complete list of all warnings and problems.\n     */\n    checkImage() {\n        const problemNodes = [];\n        this.editor.dom.select('img').forEach((img) => {\n            const alt = img.getAttribute('alt');\n            if (!alt && img.getAttribute('role') !== 'presentation') {\n                problemNodes.push(img);\n            }\n        });\n        return problemNodes;\n    }\n\n    /**\n     * Look for any table without a caption.\n     *\n     * @return {Node} A complete list of all warnings and problems.\n     */\n    checkTableCaption() {\n        const problemNodes = [];\n        this.editor.dom.select('table').forEach((table) => {\n            const caption = table.querySelector('caption');\n            if (!caption?.textContent.trim()) {\n                problemNodes.push(table);\n            }\n        });\n\n        return problemNodes;\n    }\n\n    /**\n     * Check accessiblity issue for not img and table only.\n     *\n     * @return {Node} A complete list of all warnings and problems.\n     * @private\n     */\n    checkOtherElements() {\n        const problemNodes = [];\n\n        const getRatio = (lum1, lum2) => {\n            // Algorithm from \"http://www.w3.org/TR/WCAG20-GENERAL/G18.html\".\n            if (lum1 > lum2) {\n                return (lum1 + 0.05) / (lum2 + 0.05);\n            } else {\n                return (lum2 + 0.05) / (lum1 + 0.05);\n            }\n        };\n\n        this.editor.dom.select('body *')\n            .filter((node) => node.hasChildNodes() && node.childNodes[0].nodeValue !== null)\n            .forEach((node) => {\n                const foreground = this.colorBase.fromArray(\n                    this.getComputedBackgroundColor(\n                        node,\n                        window.getComputedStyle(node, null).getPropertyValue('color')\n                    ),\n                    this.colorBase.TYPES.RGBA\n                );\n                const background = this.colorBase.fromArray(\n                    this.getComputedBackgroundColor(\n                        node\n                    ),\n                    this.colorBase.TYPES.RGBA\n                );\n\n                const lum1 = this.getLuminanceFromCssColor(foreground);\n                const lum2 = this.getLuminanceFromCssColor(background);\n                const ratio = getRatio(lum1, lum2);\n\n                if (ratio <= 4.5) {\n                    window.console.log(`\n                        Contrast ratio is too low: ${ratio}\n                        Colour 1: ${foreground}\n                        Colour 2: ${background}\n                        Luminance 1: ${lum1}\n                        Luminance 2: ${lum2}\n                    `);\n\n                    // We only want the highest node with dodgy contrast reported.\n                    if (!problemNodes.find((existingProblemNode) => existingProblemNode.contains(node))) {\n                        problemNodes.push(node);\n                    }\n                }\n            });\n        return problemNodes;\n    }\n\n    /**\n     * Check accessiblity issue only for table with merged cells.\n     *\n     * @return {Node} A complete list of all warnings and problems.\n     * @private\n     */\n    checkTableMergedCells() {\n        const problemNodes = [];\n        this.editor.dom.select('table').forEach((table) => {\n            const rowcolspan = table.querySelectorAll('[colspan], [rowspan]');\n            if (rowcolspan.length) {\n                problemNodes.push(table);\n            }\n        });\n        return problemNodes;\n    }\n\n    /**\n     * Check accessiblity issue only for table with no headers.\n     *\n     * @return {Node} A complete list of all warnings and problems.\n     * @private\n     */\n    checkTableHeaders() {\n        const problemNodes = [];\n\n        this.editor.dom.select('table').forEach((table) => {\n            if (table.querySelector('tr').querySelector('td')) {\n                // The first row has a non-header cell, so all rows must have at least one header.\n                const missingHeader = [...table.querySelectorAll('tr')].some((row) => {\n                    const header = row.querySelector('th');\n                    if (!header) {\n                        return true;\n                    }\n\n                    if (!header.textContent.trim()) {\n                        return true;\n                    }\n\n                    return false;\n                });\n                if (missingHeader) {\n                    // At least one row is missing the header, or it is empty.\n                    problemNodes.push(table);\n                }\n            } else {\n                // Every header must have some content.\n                if ([...table.querySelectorAll('tr th')].some((header) => !header.textContent.trim())) {\n                    problemNodes.push(table);\n                }\n            }\n        });\n        return problemNodes;\n    }\n\n    /**\n     * Convert a CSS color to a luminance value.\n     *\n     * @param {String} colortext The Hex value for the colour\n     * @return {Number} The luminance value.\n     * @private\n     */\n    getLuminanceFromCssColor(colortext) {\n        if (colortext === 'transparent') {\n            colortext = '#ffffff';\n        }\n        const color = this.colorBase.toArray(this.colorBase.toRGB(colortext));\n\n        // Algorithm from \"http://www.w3.org/TR/WCAG20-GENERAL/G18.html\".\n        const part1 = (a) => {\n            a = parseInt(a, 10) / 255.0;\n            if (a <= 0.03928) {\n                a = a / 12.92;\n            } else {\n                a = Math.pow(((a + 0.055) / 1.055), 2.4);\n            }\n            return a;\n        };\n\n        const r1 = part1(color[0]);\n        const g1 = part1(color[1]);\n        const b1 = part1(color[2]);\n\n        return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;\n    }\n\n    /**\n     * Get the computed RGB converted to full alpha value, considering the node hierarchy.\n     *\n     * @param {Node} node\n     * @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.\n     * @return {Array} Colour in Array form (RGBA)\n     * @private\n     */\n    getComputedBackgroundColor(node, color) {\n        if (!node.parentNode) {\n            // This is the document node and has no colour.\n            // We cannot use window.getComputedStyle on the document.\n            // If we got here, then the document has no background colour. Fall back to white.\n            return this.colorBase.toArray('rgba(255, 255, 255, 1)');\n        }\n        color = color ? color : window.getComputedStyle(node, null).getPropertyValue('background-color');\n\n        if (color.toLowerCase() === 'rgba(0, 0, 0, 0)' || color.toLowerCase() === 'transparent') {\n            color = 'rgba(1, 1, 1, 0)';\n        }\n\n        // Convert the colour to its constituent parts in RGBA format, then fetch the alpha.\n        const colorParts = this.colorBase.toArray(color);\n        const alpha = colorParts[3];\n\n        if (alpha === 1) {\n            // If the alpha of the background is already 1, then the parent background colour does not change anything.\n            return colorParts;\n        }\n\n        // Fetch the computed background colour of the parent and use it to calculate the RGB of this item.\n        const parentColor = this.getComputedBackgroundColor(node.parentNode);\n        return [\n            // RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).\n            (1 - alpha) * parentColor[0] + alpha * colorParts[0],\n            (1 - alpha) * parentColor[1] + alpha * colorParts[1],\n            (1 - alpha) * parentColor[2] + alpha * colorParts[2],\n            // We always return a colour with full alpha.\n            1\n        ];\n    }\n}\n"],"names":["constructor","editor","colorBase","ColorBase","modal","placeholderSelectors","placeholders","this","length","join","destroy","Modal","create","large","title","component","body","getDialogueContent","show","getRoot","on","ModalEvents","hidden","addEventListener","event","faultLink","target","closest","preventDefault","nodeName","dataset","selectedNode","includes","dom","select","nodeIndex","toUpperCase","selectAndScroll","hide","keys","stringValues","map","key","Map","index","langStrings","getAllWarningStrings","warnings","getWarnings","warning","description","type","get","value","nodeData","problemNode","text","Templates","render","node","selection","scrollIntoView","behavior","block","push","createWarnings","checkImage","checkOtherElements","getContent","format","checkTableCaption","checkTableMergedCells","checkTableHeaders","filter","nodes","isImageType","getTextValue","emptyStringValue","innerText","trim","textContent","getEventualNode","childNodes","document","matches","describedNode","indexOf","src","getAttribute","problemNodes","forEach","img","table","caption","querySelector","hasChildNodes","nodeValue","foreground","fromArray","getComputedBackgroundColor","window","getComputedStyle","getPropertyValue","TYPES","RGBA","background","lum1","getLuminanceFromCssColor","lum2","ratio","getRatio","console","log","find","existingProblemNode","contains","querySelectorAll","some","row","header","colortext","color","toArray","toRGB","part1","a","parseInt","Math","pow","parentNode","toLowerCase","colorParts","alpha","parentColor"],"mappings":"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"}