From ff22a6e2217009eb5ce46ed0db38c5d5a7ab0eb4 Mon Sep 17 00:00:00 2001 From: Meirza Date: Fri, 28 Apr 2023 19:51:34 +0700 Subject: [PATCH 1/2] MDL-76673 tiny: Check color contrast only on elements that have text If the content in Tiny is wrapped by an element with only a background color, without text color, the accessibility checker will use the parent text-color as the foreground color, which is owned by form-control, it becomes a problem if the wrapped element background using #34274f and the parent text-color is #495057 then the low contrast as a result. To avoid such a situation, the patch will only calculate color contrast if only the element has text, so it will give results per element if the accessibility checker is unsatisfied. The patch also changes the selected element from 'body > *' to 'body *', so it will return all elements (parents and children) in the content instead of parents only. And minor modifications on the warning_content.mustache to fix the numbering format. --- .../accessibilitychecker/amd/build/checker.min.js | 2 +- .../amd/build/checker.min.js.map | 2 +- .../plugins/accessibilitychecker/amd/src/checker.js | 5 +++-- .../templates/warning_content.mustache | 12 +++++------- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js index ba758239f7f..59a1afeb5b1 100644 --- a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js @@ -3,6 +3,6 @@ define("tiny_accessibilitychecker/checker",["exports","core/templates","core/str * @package tiny_accessibilitychecker * @copyright 2022, Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents),_colorbase=_interopRequireDefault(_colorbase);return _exports.default=class{constructor(editor){this.editor=editor,this.colorBase=new _colorbase.default,this.modal=null,this.placeholderSelectors=null;const placeholders=(0,_options.getPlaceholderSelectors)(this.editor);placeholders.length&&(this.placeholderSelectors=placeholders.join(", "))}destroy(){delete this.editor,delete this.colorBase,this.modal.destroy(),delete this.modal}async displayDialogue(){this.modal=await Modal.create({type:Modal.types.DEFAULT,large:!0,title:(0,_str.get_string)("pluginname",_common.component),body:this.getDialogueContent()}),this.modal.getRoot().on(ModalEvents.hidden,(()=>this.destroy())),this.modal.getRoot()[0].addEventListener("click",(event=>{const faultLink=event.target.closest('[data-action="highlightfault"]');if(!faultLink)return;event.preventDefault();const nodeName=faultLink.dataset.nodeName;let selectedNode=null;if(nodeName)if(nodeName.includes(",")||"body"===nodeName)selectedNode=this.editor.dom.select("body")[0];else{var _faultLink$dataset$no;const nodeIndex=null!==(_faultLink$dataset$no=faultLink.dataset.nodeIndex)&&void 0!==_faultLink$dataset$no?_faultLink$dataset$no:0;selectedNode=this.editor.dom.select(nodeName)[nodeIndex]}selectedNode&&"BODY"!==selectedNode.nodeName.toUpperCase()&&this.selectAndScroll(selectedNode),this.modal.hide()})),this.modal.show()}async getAllWarningStrings(){const keys=["emptytext","entiredocument","imagesmissingalt","needsmorecontrast","needsmoreheadings","tablesmissingcaption","tablesmissingheaders","tableswithmergedcells"],stringValues=await(0,_str.get_strings)(keys.map((key=>({key:key,component:_common.component}))));return new Map(keys.map(((key,index)=>[key,stringValues[index]])))}async getDialogueContent(){const langStrings=await this.getAllWarningStrings(),warnings=this.getWarnings().map((warning=>(warning.description&&("langstring"===warning.description.type?warning.description=langStrings.get(warning.description.value):warning.description=warning.description.value),warning.nodeData=warning.nodeData.map((problemNode=>(problemNode.text&&("langstring"===problemNode.text.type?problemNode.text=langStrings.get(problemNode.text.value):problemNode.text=problemNode.text.value),problemNode))),warning)));return _templates.default.render("tiny_accessibilitychecker/warning_content",{warnings:warnings})}selectAndScroll(node){this.editor.selection.select(node).scrollIntoView({behavior:"smooth",block:"nearest"})}getWarnings(){const warnings=[];return warnings.push(this.createWarnings("imagesmissingalt",this.checkImage(),!0)),warnings.push(this.createWarnings("needsmorecontrast",this.checkOtherElements(),!1)),this.editor.getContent({format:"text"}).length>1e3&&this.editor.dom.select("h3,h4,h5").length<1&&warnings.push(this.createWarnings("needsmoreheadings",[this.editor],!1)),warnings.push(this.createWarnings("tablesmissingcaption",this.checkTableCaption(),!1)),warnings.push(this.createWarnings("tableswithmergedcells",this.checkTableMergedCells(),!1)),warnings.push(this.createWarnings("tablesmissingheaders",this.checkTableHeaders(),!1)),warnings.filter((warning=>warning.nodeData.length>0))}createWarnings(description,nodes,isImageType){const getTextValue=node=>{if(node===this.editor)return{type:"langstring",value:"entiredocument"};const emptyStringValue={type:"langstring",value:"emptytext"};if("innerText"in node){const value=node.innerText.trim();return value.length?{type:"raw",value:value}:emptyStringValue}if("textContent"in node){const value=node.textContent.trim();return value.length?{type:"raw",value:value}:emptyStringValue}return{type:"raw",value:node.nodeName}},getEventualNode=node=>{if(node!==this.editor)return node;const childNodes=node.dom.select("body")[0].childNodes;return childNodes.length?document.body:childNodes},warning={description:{type:"langstring",value:description},nodeData:[]};return warning.nodeData=[...nodes].filter((node=>node!==this.editor&&this.placeholderSelectors?!1===node.matches(this.placeholderSelectors):node)).map((node=>{const describedNode=getEventualNode(node),nodeIndex=this.editor.dom.select(describedNode.nodeName).indexOf(describedNode),warning={src:null,text:null,nodeName:describedNode.nodeName,nodeIndex:nodeIndex};return isImageType?warning.src=node.getAttribute("src"):warning.text=getTextValue(node),warning})),warning}checkImage(){const problemNodes=[];return this.editor.dom.select("img").forEach((img=>{img.getAttribute("alt")||"presentation"===img.getAttribute("role")||problemNodes.push(img)})),problemNodes}checkTableCaption(){const problemNodes=[];return this.editor.dom.select("table").forEach((table=>{const caption=table.querySelector("caption");null!=caption&&caption.textContent.trim()||problemNodes.push(table)})),problemNodes}checkOtherElements(){const problemNodes=[];return this.editor.dom.select("body > *").filter((node=>{var _node$textContent;return null===(_node$textContent=node.textContent)||void 0===_node$textContent?void 0:_node$textContent.trim().length})).forEach((node=>{const foreground=this.colorBase.fromArray(this.getComputedBackgroundColor(node,window.getComputedStyle(node,null).getPropertyValue("color")),this.colorBase.TYPES.RGBA),background=this.colorBase.fromArray(this.getComputedBackgroundColor(node),this.colorBase.TYPES.RGBA),lum1=this.getLuminanceFromCssColor(foreground),lum2=this.getLuminanceFromCssColor(background),ratio=((lum1,lum2)=>lum1>lum2?(lum1+.05)/(lum2+.05):(lum2+.05)/(lum1+.05))(lum1,lum2);ratio<=4.5&&(window.console.log("\n Contrast ratio is too low: ".concat(ratio,"\n Colour 1: ").concat(foreground,"\n Colour 2: ").concat(background,"\n Luminance 1: ").concat(lum1,"\n Luminance 2: ").concat(lum2,"\n ")),problemNodes.find((existingProblemNode=>existingProblemNode.contains(node)))||problemNodes.push(node))})),problemNodes}checkTableMergedCells(){const problemNodes=[];return this.editor.dom.select("table").forEach((table=>{table.querySelectorAll("[colspan], [rowspan]").length&&problemNodes.push(table)})),problemNodes}checkTableHeaders(){const problemNodes=[];return this.editor.dom.select("table").forEach((table=>{if(table.querySelector("tr").querySelector("td")){[...table.querySelectorAll("tr")].some((row=>{const header=row.querySelector("th");return!header||!header.textContent.trim()}))&&problemNodes.push(table)}else[...table.querySelectorAll("tr th")].some((header=>!header.textContent.trim()))&&problemNodes.push(table)})),problemNodes}getLuminanceFromCssColor(colortext){"transparent"===colortext&&(colortext="#ffffff");const color=this.colorBase.toArray(this.colorBase.toRGB(colortext)),part1=a=>((a=parseInt(a,10)/255)<=.03928?a/=12.92:a=Math.pow((a+.055)/1.055,2.4),a);return.2126*part1(color[0])+.7152*part1(color[1])+.0722*part1(color[2])}getComputedBackgroundColor(node,color){if(!node.parentNode)return this.colorBase.toArray("rgba(255, 255, 255, 1)");"rgba(0, 0, 0, 0)"!==(color=color||window.getComputedStyle(node,null).getPropertyValue("background-color")).toLowerCase()&&"transparent"!==color.toLowerCase()||(color="rgba(1, 1, 1, 0)");const colorParts=this.colorBase.toArray(color),alpha=colorParts[3];if(1===alpha)return colorParts;const parentColor=this.getComputedBackgroundColor(node.parentNode);return[(1-alpha)*parentColor[0]+alpha*colorParts[0],(1-alpha)*parentColor[1]+alpha*colorParts[1],(1-alpha)*parentColor[2]+alpha*colorParts[2],1]}},_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents),_colorbase=_interopRequireDefault(_colorbase);return _exports.default=class{constructor(editor){this.editor=editor,this.colorBase=new _colorbase.default,this.modal=null,this.placeholderSelectors=null;const placeholders=(0,_options.getPlaceholderSelectors)(this.editor);placeholders.length&&(this.placeholderSelectors=placeholders.join(", "))}destroy(){delete this.editor,delete this.colorBase,this.modal.destroy(),delete this.modal}async displayDialogue(){this.modal=await Modal.create({type:Modal.types.DEFAULT,large:!0,title:(0,_str.get_string)("pluginname",_common.component),body:this.getDialogueContent()}),this.modal.getRoot().on(ModalEvents.hidden,(()=>this.destroy())),this.modal.getRoot()[0].addEventListener("click",(event=>{const faultLink=event.target.closest('[data-action="highlightfault"]');if(!faultLink)return;event.preventDefault();const nodeName=faultLink.dataset.nodeName;let selectedNode=null;if(nodeName)if(nodeName.includes(",")||"body"===nodeName)selectedNode=this.editor.dom.select("body")[0];else{var _faultLink$dataset$no;const nodeIndex=null!==(_faultLink$dataset$no=faultLink.dataset.nodeIndex)&&void 0!==_faultLink$dataset$no?_faultLink$dataset$no:0;selectedNode=this.editor.dom.select(nodeName)[nodeIndex]}selectedNode&&"BODY"!==selectedNode.nodeName.toUpperCase()&&this.selectAndScroll(selectedNode),this.modal.hide()})),this.modal.show()}async getAllWarningStrings(){const keys=["emptytext","entiredocument","imagesmissingalt","needsmorecontrast","needsmoreheadings","tablesmissingcaption","tablesmissingheaders","tableswithmergedcells"],stringValues=await(0,_str.get_strings)(keys.map((key=>({key:key,component:_common.component}))));return new Map(keys.map(((key,index)=>[key,stringValues[index]])))}async getDialogueContent(){const langStrings=await this.getAllWarningStrings(),warnings=this.getWarnings().map((warning=>(warning.description&&("langstring"===warning.description.type?warning.description=langStrings.get(warning.description.value):warning.description=warning.description.value),warning.nodeData=warning.nodeData.map((problemNode=>(problemNode.text&&("langstring"===problemNode.text.type?problemNode.text=langStrings.get(problemNode.text.value):problemNode.text=problemNode.text.value),problemNode))),warning)));return _templates.default.render("tiny_accessibilitychecker/warning_content",{warnings:warnings})}selectAndScroll(node){this.editor.selection.select(node).scrollIntoView({behavior:"smooth",block:"nearest"})}getWarnings(){const warnings=[];return warnings.push(this.createWarnings("imagesmissingalt",this.checkImage(),!0)),warnings.push(this.createWarnings("needsmorecontrast",this.checkOtherElements(),!1)),this.editor.getContent({format:"text"}).length>1e3&&this.editor.dom.select("h3,h4,h5").length<1&&warnings.push(this.createWarnings("needsmoreheadings",[this.editor],!1)),warnings.push(this.createWarnings("tablesmissingcaption",this.checkTableCaption(),!1)),warnings.push(this.createWarnings("tableswithmergedcells",this.checkTableMergedCells(),!1)),warnings.push(this.createWarnings("tablesmissingheaders",this.checkTableHeaders(),!1)),warnings.filter((warning=>warning.nodeData.length>0))}createWarnings(description,nodes,isImageType){const getTextValue=node=>{if(node===this.editor)return{type:"langstring",value:"entiredocument"};const emptyStringValue={type:"langstring",value:"emptytext"};if("innerText"in node){const value=node.innerText.trim();return value.length?{type:"raw",value:value}:emptyStringValue}if("textContent"in node){const value=node.textContent.trim();return value.length?{type:"raw",value:value}:emptyStringValue}return{type:"raw",value:node.nodeName}},getEventualNode=node=>{if(node!==this.editor)return node;const childNodes=node.dom.select("body")[0].childNodes;return childNodes.length?document.body:childNodes},warning={description:{type:"langstring",value:description},nodeData:[]};return warning.nodeData=[...nodes].filter((node=>node!==this.editor&&this.placeholderSelectors?!1===node.matches(this.placeholderSelectors):node)).map((node=>{const describedNode=getEventualNode(node),nodeIndex=this.editor.dom.select(describedNode.nodeName).indexOf(describedNode),warning={src:null,text:null,nodeName:describedNode.nodeName,nodeIndex:nodeIndex};return isImageType?warning.src=node.getAttribute("src"):warning.text=getTextValue(node),warning})),warning}checkImage(){const problemNodes=[];return this.editor.dom.select("img").forEach((img=>{img.getAttribute("alt")||"presentation"===img.getAttribute("role")||problemNodes.push(img)})),problemNodes}checkTableCaption(){const problemNodes=[];return this.editor.dom.select("table").forEach((table=>{const caption=table.querySelector("caption");null!=caption&&caption.textContent.trim()||problemNodes.push(table)})),problemNodes}checkOtherElements(){const problemNodes=[];return this.editor.dom.select("body *").filter((node=>node.hasChildNodes()&&null!==node.childNodes[0].nodeValue)).forEach((node=>{const foreground=this.colorBase.fromArray(this.getComputedBackgroundColor(node,window.getComputedStyle(node,null).getPropertyValue("color")),this.colorBase.TYPES.RGBA),background=this.colorBase.fromArray(this.getComputedBackgroundColor(node),this.colorBase.TYPES.RGBA),lum1=this.getLuminanceFromCssColor(foreground),lum2=this.getLuminanceFromCssColor(background),ratio=((lum1,lum2)=>lum1>lum2?(lum1+.05)/(lum2+.05):(lum2+.05)/(lum1+.05))(lum1,lum2);ratio<=4.5&&(window.console.log("\n Contrast ratio is too low: ".concat(ratio,"\n Colour 1: ").concat(foreground,"\n Colour 2: ").concat(background,"\n Luminance 1: ").concat(lum1,"\n Luminance 2: ").concat(lum2,"\n ")),problemNodes.find((existingProblemNode=>existingProblemNode.contains(node)))||problemNodes.push(node))})),problemNodes}checkTableMergedCells(){const problemNodes=[];return this.editor.dom.select("table").forEach((table=>{table.querySelectorAll("[colspan], [rowspan]").length&&problemNodes.push(table)})),problemNodes}checkTableHeaders(){const problemNodes=[];return this.editor.dom.select("table").forEach((table=>{if(table.querySelector("tr").querySelector("td")){[...table.querySelectorAll("tr")].some((row=>{const header=row.querySelector("th");return!header||!header.textContent.trim()}))&&problemNodes.push(table)}else[...table.querySelectorAll("tr th")].some((header=>!header.textContent.trim()))&&problemNodes.push(table)})),problemNodes}getLuminanceFromCssColor(colortext){"transparent"===colortext&&(colortext="#ffffff");const color=this.colorBase.toArray(this.colorBase.toRGB(colortext)),part1=a=>((a=parseInt(a,10)/255)<=.03928?a/=12.92:a=Math.pow((a+.055)/1.055,2.4),a);return.2126*part1(color[0])+.7152*part1(color[1])+.0722*part1(color[2])}getComputedBackgroundColor(node,color){if(!node.parentNode)return this.colorBase.toArray("rgba(255, 255, 255, 1)");"rgba(0, 0, 0, 0)"!==(color=color||window.getComputedStyle(node,null).getPropertyValue("background-color")).toLowerCase()&&"transparent"!==color.toLowerCase()||(color="rgba(1, 1, 1, 0)");const colorParts=this.colorBase.toArray(color),alpha=colorParts[3];if(1===alpha)return colorParts;const parentColor=this.getComputedBackgroundColor(node.parentNode);return[(1-alpha)*parentColor[0]+alpha*colorParts[0],(1-alpha)*parentColor[1]+alpha*colorParts[1],(1-alpha)*parentColor[2]+alpha*colorParts[2],1]}},_exports.default})); //# sourceMappingURL=checker.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js.map b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js.map index 8909b432f6b..2cd2ce46b66 100644 --- a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js.map +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js.map @@ -1 +1 @@ -{"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 .\n\n/*\n * @package tiny_accessibilitychecker\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport {component} from './common';\nimport * as Modal from 'core/modal_factory';\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 type: Modal.types.DEFAULT,\n large: true,\n title: getString('pluginname', component),\n body: this.getDialogueContent()\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 this.modal.show();\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} 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 this.editor.dom.select('body > *')\n .filter((node) => node.textContent?.trim().length)\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","type","types","DEFAULT","large","title","component","body","getDialogueContent","getRoot","on","ModalEvents","hidden","addEventListener","event","faultLink","target","closest","preventDefault","nodeName","dataset","selectedNode","includes","dom","select","nodeIndex","toUpperCase","selectAndScroll","hide","show","keys","stringValues","map","key","Map","index","langStrings","getAllWarningStrings","warnings","getWarnings","warning","description","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","_node$textContent","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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"} \ No newline at end of file +{"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 .\n\n/*\n * @package tiny_accessibilitychecker\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport {component} from './common';\nimport * as Modal from 'core/modal_factory';\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 type: Modal.types.DEFAULT,\n large: true,\n title: getString('pluginname', component),\n body: this.getDialogueContent()\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 this.modal.show();\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} 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","type","types","DEFAULT","large","title","component","body","getDialogueContent","getRoot","on","ModalEvents","hidden","addEventListener","event","faultLink","target","closest","preventDefault","nodeName","dataset","selectedNode","includes","dom","select","nodeIndex","toUpperCase","selectAndScroll","hide","show","keys","stringValues","map","key","Map","index","langStrings","getAllWarningStrings","warnings","getWarnings","warning","description","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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"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js index 126c814c3e2..fb43a63df8c 100644 --- a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js @@ -333,8 +333,9 @@ export default class { return (lum2 + 0.05) / (lum1 + 0.05); } }; - this.editor.dom.select('body > *') - .filter((node) => node.textContent?.trim().length) + + this.editor.dom.select('body *') + .filter((node) => node.hasChildNodes() && node.childNodes[0].nodeValue !== null) .forEach((node) => { const foreground = this.colorBase.fromArray( this.getComputedBackgroundColor( diff --git a/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache b/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache index 7b094747c5b..40fb4127eb1 100644 --- a/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache +++ b/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache @@ -36,10 +36,9 @@ }}
{{#warnings}} -

- {{{description}}} - {{#nodeData}} -

    +

    {{{description}}}

    +
      + {{#nodeData}}
    1. {{#text}} {{text}} @@ -49,9 +48,8 @@ {{/src}} - {{#str}} viewissue, tiny_accessibilitychecker {{/str}}
    2. -
    - {{/nodeData}} -

    + {{/nodeData}} +
{{/warnings}} {{^warnings}}

{{#str}} nowarnings, tiny_accessibilitychecker {{/str}}

From 6be248016ff3ba997af5a05e1eef7ad4008ce2e0 Mon Sep 17 00:00:00 2001 From: Meirza Date: Fri, 28 Apr 2023 19:51:55 +0700 Subject: [PATCH 2/2] MDL-76673 atto: Check color contrast only on elements that have text If the content in Atto is wrapped by an element with only a background color, without text color, the accessibility checker will use the parent text-color as the foreground color, which is owned by form-control, it becomes a problem if the wrapped element background using #34274f and the parent text-color is #495057 then the low contrast as a result. To avoid such a situation, the patch will only calculate color contrast if only the element has text, so it will give results per element if the accessibility checker is unsatisfied. --- .../moodle-atto_accessibilitychecker-button-debug.js | 2 +- .../moodle-atto_accessibilitychecker-button-min.js | 2 +- .../moodle-atto_accessibilitychecker-button.js | 2 +- .../plugins/accessibilitychecker/yui/src/button/js/button.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js index c8d7bb63bce..dc0202fd036 100644 --- a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js +++ b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js @@ -128,7 +128,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M. lum2; // Check for non-empty text. - if (Y.Lang.trim(node.get('text')) !== '') { + if (node.hasChildNodes() && Y.Lang.trim(node._node.childNodes[0].nodeValue) !== '') { foreground = Y.Color.fromArray( this._getComputedBackgroundColor(node, node.getComputedStyle('color')), Y.Color.TYPES.RGBA diff --git a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js index e59fd350162..32092df4f6a 100644 --- a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js +++ b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js @@ -1 +1 @@ -YUI.add("moodle-atto_accessibilitychecker-button",function(d,t){var g="atto_accessibilitychecker";d.namespace("M.atto_accessibilitychecker").Button=d.Base.create("button",d.M.editor_atto.EditorPlugin,[],{initializer:function(){this.addButton({icon:"e/accessibility_checker",callback:this._displayDialogue})},_displayDialogue:function(){this.getDialogue({headerContent:M.util.get_string("pluginname",g),width:"500px",focusAfterHide:!0}).set("bodyContent",this._getDialogueContent()).show()},_getDialogueContent:function(){var t=d.Node.create('
');return t.append(this._getWarnings()),t.delegate("click",function(t){t.preventDefault();var e=this.get("host"),t=t.currentTarget.getData("sourceNode"),i=this.getDialogue();t?(i.set("focusAfterHide",this.editor).hide(),e.setSelection(e.getSelectionFromNode(t))):i.hide()},"a",this),t},_getWarnings:function(){var t=d.Node.create("
"),n=[];return this.editor.all("img").each(function(t){var e=t.getAttribute("alt");void 0!==e&&""!==e||"presentation"!==t.getAttribute("role")&&n.push(t)},this),this._addWarnings(t,M.util.get_string("imagesmissingalt",g),n,!0),n=[],this.editor.all("*").each(function(t){var e,i,r,o;if(""!==d.Lang.trim(t.get("text"))&&(e=d.Color.fromArray(this._getComputedBackgroundColor(t,t.getComputedStyle("color")),d.Color.TYPES.RGBA),i=d.Color.fromArray(this._getComputedBackgroundColor(t),d.Color.TYPES.RGBA),e=this._getLuminanceFromCssColor(e),((i=this._getLuminanceFromCssColor(i))"+M.util.get_string("nowarnings",g)+"

"),t},_addWarnings:function(t,e,i,r){var o,n,a,s,l;if(0"+e+"

"),o=d.Node.create('
    '),n=n=0;n"),(l=r?(a=i[n].getAttribute("src"),d.Node.create(' '+a+"")):(a="innerText"in i[n]?"innerText":"textContent",""===(l=i[n].get(a).trim())&&(l=M.util.get_string("emptytext",g)),i[n]===this.editor&&(l=M.util.get_string("entiredocument",g)),d.Node.create(''+l+""))).setData("sourceNode",i[n]),s.append(l),o.append(s);e.append(o),t.append(e)}},_getLuminanceFromCssColor:function(t){var e;return.2126*(e=function(t){return(t=parseInt(t,10)/255)<=.03928?t/=12.92:t=Math.pow((t+.055)/1.055,2.4),t})((t=d.Color.toArray(d.Color.toRGB(t="transparent"===t?"#ffffff":t)))[0])+.7152*e(t[1])+.0722*e(t[2])},_getComputedBackgroundColor:function(t,e){var i;return"transparent"===(e=e||t.getComputedStyle("backgroundColor")).toLowerCase()&&(e="rgba(1, 1, 1, 0)"),1===(i=(e=d.Color.toArray(e))[3])?e:[(1-i)*(t=this._getComputedBackgroundColor(t.get("parentNode")))[0]+i*e[0],(1-i)*t[1]+i*e[1],(1-i)*t[2]+i*e[2],1]}})},"@VERSION@",{requires:["color-base","moodle-editor_atto-plugin"]}); \ No newline at end of file +YUI.add("moodle-atto_accessibilitychecker-button",function(d,t){var g="atto_accessibilitychecker";d.namespace("M.atto_accessibilitychecker").Button=d.Base.create("button",d.M.editor_atto.EditorPlugin,[],{initializer:function(){this.addButton({icon:"e/accessibility_checker",callback:this._displayDialogue})},_displayDialogue:function(){this.getDialogue({headerContent:M.util.get_string("pluginname",g),width:"500px",focusAfterHide:!0}).set("bodyContent",this._getDialogueContent()).show()},_getDialogueContent:function(){var t=d.Node.create('
    ');return t.append(this._getWarnings()),t.delegate("click",function(t){t.preventDefault();var e=this.get("host"),t=t.currentTarget.getData("sourceNode"),i=this.getDialogue();t?(i.set("focusAfterHide",this.editor).hide(),e.setSelection(e.getSelectionFromNode(t))):i.hide()},"a",this),t},_getWarnings:function(){var t=d.Node.create("
    "),n=[];return this.editor.all("img").each(function(t){var e=t.getAttribute("alt");void 0!==e&&""!==e||"presentation"!==t.getAttribute("role")&&n.push(t)},this),this._addWarnings(t,M.util.get_string("imagesmissingalt",g),n,!0),n=[],this.editor.all("*").each(function(t){var e,i,o,r;if(t.hasChildNodes()&&""!==d.Lang.trim(t._node.childNodes[0].nodeValue)&&(e=d.Color.fromArray(this._getComputedBackgroundColor(t,t.getComputedStyle("color")),d.Color.TYPES.RGBA),i=d.Color.fromArray(this._getComputedBackgroundColor(t),d.Color.TYPES.RGBA),e=this._getLuminanceFromCssColor(e),((i=this._getLuminanceFromCssColor(i))"+M.util.get_string("nowarnings",g)+"

    "),t},_addWarnings:function(t,e,i,o){var r,n,a,s,l;if(0"+e+"

    "),r=d.Node.create('
      '),n=n=0;n"),(l=o?(a=i[n].getAttribute("src"),d.Node.create(' '+a+"")):(a="innerText"in i[n]?"innerText":"textContent",""===(l=i[n].get(a).trim())&&(l=M.util.get_string("emptytext",g)),i[n]===this.editor&&(l=M.util.get_string("entiredocument",g)),d.Node.create(''+l+""))).setData("sourceNode",i[n]),s.append(l),r.append(s);e.append(r),t.append(e)}},_getLuminanceFromCssColor:function(t){var e;return.2126*(e=function(t){return(t=parseInt(t,10)/255)<=.03928?t/=12.92:t=Math.pow((t+.055)/1.055,2.4),t})((t=d.Color.toArray(d.Color.toRGB(t="transparent"===t?"#ffffff":t)))[0])+.7152*e(t[1])+.0722*e(t[2])},_getComputedBackgroundColor:function(t,e){var i;return"transparent"===(e=e||t.getComputedStyle("backgroundColor")).toLowerCase()&&(e="rgba(1, 1, 1, 0)"),1===(i=(e=d.Color.toArray(e))[3])?e:[(1-i)*(t=this._getComputedBackgroundColor(t.get("parentNode")))[0]+i*e[0],(1-i)*t[1]+i*e[1],(1-i)*t[2]+i*e[2],1]}})},"@VERSION@",{requires:["color-base","moodle-editor_atto-plugin"]}); \ No newline at end of file diff --git a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js index 89d365cda5b..cdfd16e63a2 100644 --- a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js +++ b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js @@ -128,7 +128,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M. lum2; // Check for non-empty text. - if (Y.Lang.trim(node.get('text')) !== '') { + if (node.hasChildNodes() && Y.Lang.trim(node._node.childNodes[0].nodeValue) !== '') { foreground = Y.Color.fromArray( this._getComputedBackgroundColor(node, node.getComputedStyle('color')), Y.Color.TYPES.RGBA diff --git a/lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js b/lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js index ce6a4e20212..043cfa17373 100644 --- a/lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js +++ b/lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js @@ -126,7 +126,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M. lum2; // Check for non-empty text. - if (Y.Lang.trim(node.get('text')) !== '') { + if (node.hasChildNodes() && Y.Lang.trim(node._node.childNodes[0].nodeValue) !== '') { foreground = Y.Color.fromArray( this._getComputedBackgroundColor(node, node.getComputedStyle('color')), Y.Color.TYPES.RGBA