From e82229eea63a8e14fc05dd5bbc5d89800afcc611 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 18 Nov 2022 11:34:08 +0800 Subject: [PATCH] MDL-75982 tiny_accessibilitychecker: Fall back to opaque white as top background --- .../accessibilitychecker/amd/build/checker.min.js | 2 +- .../accessibilitychecker/amd/build/checker.min.js.map | 2 +- .../tiny/plugins/accessibilitychecker/amd/src/checker.js | 9 ++++++++- 3 files changed, 10 insertions(+), 3 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 2d3b1ea3be9..525dc9bceb1 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}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","nowarnings","nowarningsonselected","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].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){"transparent"===(color=color||window.getComputedStyle(node,null).getPropertyValue("background-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}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","nowarnings","nowarningsonselected","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].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})); //# 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 2ee0b69a535..fdf6fc8b409 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';\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 }\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 'nowarnings',\n 'nowarningsonselected',\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].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 color = color ? color : window.getComputedStyle(node, null).getPropertyValue('background-color');\n if (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","destroy","this","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","length","checkTableCaption","checkTableMergedCells","checkTableHeaders","filter","nodes","isImageType","getTextValue","emptyStringValue","innerText","trim","textContent","getEventualNode","childNodes","document","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","toLowerCase","colorParts","alpha","parentColor","parentNode"],"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';\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 }\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 'nowarnings',\n 'nowarningsonselected',\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].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","destroy","this","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","length","checkTableCaption","checkTableMergedCells","checkTableHeaders","filter","nodes","isImageType","getTextValue","emptyStringValue","innerText","trim","textContent","getEventualNode","childNodes","document","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 diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js index 9f24324400e..9d813409439 100644 --- a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js @@ -456,8 +456,15 @@ export default class { * @private */ getComputedBackgroundColor(node, color) { + if (!node.parentNode) { + // This is the document node and has no colour. + // We cannot use window.getComputedStyle on the document. + // If we got here, then the document has no background colour. Fall back to white. + return this.colorBase.toArray('rgba(255, 255, 255, 1)'); + } color = color ? color : window.getComputedStyle(node, null).getPropertyValue('background-color'); - if (color.toLowerCase() === 'transparent') { + + if (color.toLowerCase() === 'rgba(0, 0, 0, 0)' || color.toLowerCase() === 'transparent') { color = 'rgba(1, 1, 1, 0)'; }