From b7f2bbb08016f353a322c9388d4867932a80d2b1 Mon Sep 17 00:00:00 2001 From: Stevani Andolo Date: Wed, 14 Sep 2022 18:30:36 +0800 Subject: [PATCH] MDL-75262 tiny_accessibility: Implement Accessibility Checker Co-authored by Andrew Lyons Part of MDL-75966 --- lib/classes/plugin_manager.php | 1 + .../amd/build/checker.min.js | 8 + .../amd/build/checker.min.js.map | 1 + .../amd/build/colorbase.min.js | 8 + .../amd/build/colorbase.min.js.map | 1 + .../amd/build/commands.min.js | 10 + .../amd/build/commands.min.js.map | 1 + .../amd/build/common.min.js | 3 + .../amd/build/common.min.js.map | 1 + .../amd/build/configuration.min.js | 3 + .../amd/build/configuration.min.js.map | 1 + .../amd/build/plugin.min.js | 10 + .../amd/build/plugin.min.js.map | 1 + .../accessibilitychecker/amd/src/checker.js | 484 ++++++++++++++++++ .../accessibilitychecker/amd/src/colorbase.js | 296 +++++++++++ .../accessibilitychecker/amd/src/commands.js | 59 +++ .../accessibilitychecker/amd/src/common.js | 29 ++ .../amd/src/configuration.js | 34 ++ .../accessibilitychecker/amd/src/plugin.js | 50 ++ .../classes/plugininfo.php | 61 +++ .../classes/privacy/provider.php | 35 ++ .../lang/en/tiny_accessibilitychecker.php | 38 ++ .../plugins/accessibilitychecker/pix/icon.svg | 23 + .../plugins/accessibilitychecker/styles.css | 7 + .../templates/warning_content.mustache | 59 +++ .../plugins/accessibilitychecker/version.php | 29 ++ 26 files changed, 1253 insertions(+) create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js.map create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js.map create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js.map create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js.map create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js.map create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js.map create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/src/colorbase.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/src/commands.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/src/common.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/src/configuration.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/amd/src/plugin.js create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/classes/plugininfo.php create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/classes/privacy/provider.php create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/lang/en/tiny_accessibilitychecker.php create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/pix/icon.svg create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/styles.css create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache create mode 100644 lib/editor/tiny/plugins/accessibilitychecker/version.php diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index ca1a97208a2..5e9fdde9291 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -2029,6 +2029,7 @@ class core_plugin_manager { ), 'tiny' => [ + 'accessibilitychecker', 'autosave', 'h5p', 'media', diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js new file mode 100644 index 00000000000..2d3b1ea3be9 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js @@ -0,0 +1,8 @@ +define("tiny_accessibilitychecker/checker",["exports","core/templates","core/str","./common","core/modal_factory","core/modal_events","./colorbase"],(function(_exports,_templates,_str,_common,Modal,ModalEvents,_colorbase){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/* + * @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})); + +//# 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 new file mode 100644 index 00000000000..2ee0b69a535 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/checker.min.js.map @@ -0,0 +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":";;;;;qSA8CIA,YAAYC,aACHA,OAASA,YACTC,UAAY,IAAIC,wBAChBC,MAAQ,KAGjBC,iBACWC,KAAKL,cACLK,KAAKJ,eAEPE,MAAMC,iBACJC,KAAKF,mCAIPA,YAAcG,MAAMC,OAAO,CAC5BC,KAAMF,MAAMG,MAAMC,QAClBC,OAAO,EACPC,OAAO,mBAAU,aAAcC,mBAC/BC,KAAMT,KAAKU,4BAIVZ,MAAMa,UAAUC,GAAGC,YAAYC,QAAQ,IAAMd,KAAKD,iBAElDD,MAAMa,UAAU,GAAGI,iBAAiB,SAAUC,cACzCC,UAAYD,MAAME,OAAOC,QAAQ,sCAClCF,iBAILD,MAAMI,uBAEAC,SAAWJ,UAAUK,QAAQD,aAC/BE,aAAe,QACfF,YACIA,SAASG,SAAS,MAAqB,SAAbH,SAC1BE,aAAevB,KAAKL,OAAO8B,IAAIC,OAAO,QAAQ,OAC3C,iCACGC,wCAAYV,UAAUK,QAAQK,iEAAa,EACjDJ,aAAevB,KAAKL,OAAO8B,IAAIC,OAAOL,UAAUM,WAIpDJ,cAAwD,SAAxCA,aAAaF,SAASO,oBACjCC,gBAAgBN,mBAGpBzB,MAAMgC,eAGVhC,MAAMiC,0CAILC,KAAO,CACT,YACA,iBACA,mBACA,oBACA,oBACA,aACA,uBACA,uBACA,uBACA,yBAGEC,mBAAqB,oBAAWD,KAAKE,KAAKC,OAAUA,IAAAA,IAAK3B,UAAAA,8BACxD,IAAI4B,IAAIJ,KAAKE,KAAI,CAACC,IAAKE,QAAW,CAACF,IAAKF,aAAaI,4CAStDC,kBAAoBtC,KAAKuC,uBAGzBC,SAAWxC,KAAKyC,cAAcP,KAAKQ,UACjCA,QAAQC,cACyB,eAA7BD,QAAQC,YAAYxC,KACpBuC,QAAQC,YAAcL,YAAYM,IAAIF,QAAQC,YAAYE,OAE1DH,QAAQC,YAAcD,QAAQC,YAAYE,OAIlDH,QAAQI,SAAWJ,QAAQI,SAASZ,KAAKa,cACjCA,YAAYC,OACkB,eAA1BD,YAAYC,KAAK7C,KACjB4C,YAAYC,KAAOV,YAAYM,IAAIG,YAAYC,KAAKH,OAEpDE,YAAYC,KAAOD,YAAYC,KAAKH,OAIrCE,eAGJL,kBAGJO,mBAAUC,OAAO,4CAA6C,CACjEV,SAAAA,WASRX,gBAAgBsB,WACPxD,OAAOyD,UAAU1B,OAAOyB,MAAME,eAAe,CAC9CC,SAAU,SACVC,MAAO,YASfd,oBACUD,SAAW,UAGjBA,SAASgB,KAAKxD,KAAKyD,eAAe,mBAAoBzD,KAAK0D,cAAc,IACzElB,SAASgB,KAAKxD,KAAKyD,eAAe,oBAAqBzD,KAAK2D,sBAAsB,IAG9E3D,KAAKL,OAAOiE,WAAW,CAACC,OAAQ,SAASC,OAAS,KAAQ9D,KAAKL,OAAO8B,IAAIC,OAAO,YAAYoC,OAAS,GACtGtB,SAASgB,KAAKxD,KAAKyD,eAAe,oBAAqB,CAACzD,KAAKL,SAAS,IAI1E6C,SAASgB,KAAKxD,KAAKyD,eAAe,uBAAwBzD,KAAK+D,qBAAqB,IAGpFvB,SAASgB,KAAKxD,KAAKyD,eAAe,wBAAyBzD,KAAKgE,yBAAyB,IAGzFxB,SAASgB,KAAKxD,KAAKyD,eAAe,uBAAwBzD,KAAKiE,qBAAqB,IAE7EzB,SAAS0B,QAAQxB,SAAYA,QAAQI,SAASgB,OAAS,IAWlEL,eAAed,YAAawB,MAAOC,mBACzBC,aAAgBlB,UACdA,OAASnD,KAAKL,aACP,CACHQ,KAAM,aACN0C,MAAO,wBAITyB,iBAAmB,CACrBnE,KAAM,aACN0C,MAAO,gBAEP,cAAeM,KAAM,OACfN,MAAQM,KAAKoB,UAAUC,cACtB3B,MAAMiB,OAAS,CAAC3D,KAAM,MAAO0C,MAAAA,OAASyB,iBAC1C,GAAI,gBAAiBnB,KAAM,OACxBN,MAAQM,KAAKsB,YAAYD,cACxB3B,MAAMiB,OAAS,CAAC3D,KAAM,MAAO0C,MAAAA,OAASyB,uBAG1C,CAACnE,KAAM,MAAO0C,MAAOM,KAAK9B,WAG/BqD,gBAAmBvB,UACjBA,OAASnD,KAAKL,cACPwD,WAELwB,WAAaxB,KAAK1B,IAAIC,OAAO,QAAQ,GAAGiD,kBAC1CA,WAAWb,OACJc,SAASnE,KAETkE,YAITjC,QAAU,CACZC,YAAa,CACTxC,KAAM,aACN0C,MAAOF,aAEXG,SAAU,WAGdJ,QAAQI,SAAW,IAAIqB,OAAOjC,KAAKiB,aACzB0B,cAAgBH,gBAAgBvB,MAIhCxB,UAAY3B,KAAKL,OAAO8B,IAAIC,OAAOmD,cAAcxD,UAAUyD,QAAQD,eACnEnC,QAAU,CACZqC,IAAK,KACL/B,KAAM,KACN3B,SAAUwD,cAAcxD,SACxBM,UAAAA,kBAGAyC,YACA1B,QAAQqC,IAAM5B,KAAK6B,aAAa,OAEhCtC,QAAQM,KAAOqB,aAAalB,MAGzBT,WAGJA,QAQXgB,mBACUuB,aAAe,eAChBtF,OAAO8B,IAAIC,OAAO,OAAOwD,SAASC,MACvBA,IAAIH,aAAa,QACY,iBAA7BG,IAAIH,aAAa,SACzBC,aAAazB,KAAK2B,QAGnBF,aAQXlB,0BACUkB,aAAe,eAChBtF,OAAO8B,IAAIC,OAAO,SAASwD,SAASE,cAC/BC,QAAUD,MAAME,cAAc,WAC/BD,MAAAA,SAAAA,QAASZ,YAAYD,QACtBS,aAAazB,KAAK4B,UAInBH,aASXtB,2BACUsB,aAAe,eAUhBtF,OAAO8B,IAAIC,OAAO,YAClBwC,QAAQf,8DAASA,KAAKsB,gDAALc,kBAAkBf,OAAOV,UAC1CoB,SAAS/B,aACAqC,WAAaxF,KAAKJ,UAAU6F,UAC9BzF,KAAK0F,2BACDvC,KACAwC,OAAOC,iBAAiBzC,KAAM,MAAM0C,iBAAiB,UAEzD7F,KAAKJ,UAAUkG,MAAMC,MAEnBC,WAAahG,KAAKJ,UAAU6F,UAC9BzF,KAAK0F,2BACDvC,MAEJnD,KAAKJ,UAAUkG,MAAMC,MAGnBE,KAAOjG,KAAKkG,yBAAyBV,YACrCW,KAAOnG,KAAKkG,yBAAyBF,YACrCI,MA3BG,EAACH,KAAME,OAEhBF,KAAOE,MACCF,KAAO,MAASE,KAAO,MAEvBA,KAAO,MAASF,KAAO,KAsBjBI,CAASJ,KAAME,MAEzBC,OAAS,MACTT,OAAOW,QAAQC,mEACkBH,qDACjBZ,0DACAQ,6DACGC,uDACAE,gCAIdlB,aAAauB,MAAMC,qBAAwBA,oBAAoBC,SAASvD,SACzE8B,aAAazB,KAAKL,UAI3B8B,aASXjB,8BACUiB,aAAe,eAChBtF,OAAO8B,IAAIC,OAAO,SAASwD,SAASE,QAClBA,MAAMuB,iBAAiB,wBAC3B7C,QACXmB,aAAazB,KAAK4B,UAGnBH,aASXhB,0BACUgB,aAAe,eAEhBtF,OAAO8B,IAAIC,OAAO,SAASwD,SAASE,WACjCA,MAAME,cAAc,MAAMA,cAAc,MAAO,CAEzB,IAAIF,MAAMuB,iBAAiB,OAAOC,MAAMC,YACpDC,OAASD,IAAIvB,cAAc,aAC5BwB,SAIAA,OAAOrC,YAAYD,WAQxBS,aAAazB,KAAK4B,WAIlB,IAAIA,MAAMuB,iBAAiB,UAAUC,MAAME,SAAYA,OAAOrC,YAAYD,UAC1ES,aAAazB,KAAK4B,UAIvBH,aAUXiB,yBAAyBa,WACH,gBAAdA,YACAA,UAAY,iBAEVC,MAAQhH,KAAKJ,UAAUqH,QAAQjH,KAAKJ,UAAUsH,MAAMH,YAGpDI,MAASC,KACXA,EAAIC,SAASD,EAAG,IAAM,MACb,OACLA,GAAQ,MAERA,EAAIE,KAAKC,KAAMH,EAAI,MAAS,MAAQ,KAEjCA,SAOJ,MAJID,MAAMH,MAAM,IAIF,MAHVG,MAAMH,MAAM,IAGY,MAFxBG,MAAMH,MAAM,IAa3BtB,2BAA2BvC,KAAM6D,OAED,iBAD5BA,MAAQA,OAAgBrB,OAAOC,iBAAiBzC,KAAM,MAAM0C,iBAAiB,qBACnE2B,gBACNR,MAAQ,0BAINS,WAAazH,KAAKJ,UAAUqH,QAAQD,OACpCU,MAAQD,WAAW,MAEX,IAAVC,aAEOD,iBAILE,YAAc3H,KAAK0F,2BAA2BvC,KAAKyE,kBAClD,EAEF,EAAIF,OAASC,YAAY,GAAKD,MAAQD,WAAW,IACjD,EAAIC,OAASC,YAAY,GAAKD,MAAQD,WAAW,IACjD,EAAIC,OAASC,YAAY,GAAKD,MAAQD,WAAW,GAElD"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js new file mode 100644 index 00000000000..74595fc17e6 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js @@ -0,0 +1,8 @@ +define("tiny_accessibilitychecker/colorbase",["exports"],(function(_exports){function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} +/* + * @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;return _exports.default=class{constructor(){_defineProperty(this,"REGEX_HEX",/^#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})(\ufffe)?/),_defineProperty(this,"REGEX_HEX3",/^#?([\da-fA-F]{1})([\da-fA-F]{1})([\da-fA-F]{1})(\ufffe)?/),_defineProperty(this,"REGEX_RGB",/rgba?\(([\d]{1,3}), ?([\d]{1,3}), ?([\d]{1,3}),? ?([.\d]*)?\)/),_defineProperty(this,"TYPES",{HEX:"hex",RGB:"rgb",RGBA:"rgba"}),_defineProperty(this,"KEYWORDS",{black:"000",silver:"c0c0c0",gray:"808080",white:"fff",maroon:"800000",red:"f00",purple:"800080",fuchsia:"f0f",green:"008000",lime:"0f0",olive:"808000",yellow:"ff0",navy:"000080",blue:"00f",teal:"008080",aqua:"0ff"}),_defineProperty(this,"STR_HEX","#{*}{*}{*}"),_defineProperty(this,"STR_RGB","rgb({*}, {*}, {*})"),_defineProperty(this,"STR_RGBA","rgba({*}, {*}, {*}, {*})"),_defineProperty(this,"toHex",(str=>{var clr=this._convertTo(str,"hex"),isTransparent="transparent"===clr.toLowerCase();return"#"===clr.charAt(0)||isTransparent||(clr="#"+clr),isTransparent?clr.toLowerCase():clr.toUpperCase()})),_defineProperty(this,"toRGB",(str=>this._convertTo(str,"rgb").toLowerCase())),_defineProperty(this,"toRGBA",(str=>this._convertTo(str,"rgba").toLowerCase())),_defineProperty(this,"toArray",(str=>{var regex,arr,length,type=this.findType(str).toUpperCase();return"HEX"===type&&str.length<5&&(type="HEX3"),"A"===type.charAt(type.length-1)&&(type=type.slice(0,-1)),(regex=this._getRightValue("REGEX_"+type))&&(length=(arr=regex.exec(str)||[]).length)&&(arr.shift(),length--,"HEX3"===type&&(arr[0]+=arr[0],arr[1]+=arr[1],arr[2]+=arr[2]),arr[length-1]||(arr[length-1]=1)),arr})),_defineProperty(this,"fromArray",((arr,template)=>{if(arr=arr.concat(),void 0===template)return arr.join(", ");for(template=this._getRightValue("STR_"+template.toUpperCase()),3===arr.length&&4===template.match(/\{\*\}/g).length&&arr.push(1);template.indexOf("{*}")>=0&&arr.length>0;)template=template.replace("{*}",arr.shift());return template})),_defineProperty(this,"findType",(str=>{if(this.KEYWORDS[str])return"keyword";var key,index=str.indexOf("(");return index>0&&(key=str.substr(0,index)),key&&this.TYPES[key.toUpperCase()]?this.TYPES[key.toUpperCase()]:"hex"})),_defineProperty(this,"_getAlpha",(clr=>{var alpha,arr=this.toArray(clr);return arr.length>3&&(alpha=arr.pop()),+alpha||1})),_defineProperty(this,"_keywordToHex",(clr=>{var keyword=this.KEYWORDS[clr];return keyword||keyword})),_defineProperty(this,"_convertTo",((clr,to)=>{if("transparent"===clr)return clr;var needsAlpha,alpha,method,ucTo,from=this.findType(clr),originalTo=to;return"keyword"===from&&(clr=this._keywordToHex(clr),from="hex"),"hex"===from&&clr.length<5&&("#"===clr.charAt(0)&&(clr=clr.substr(1)),clr="#"+clr.charAt(0)+clr.charAt(0)+clr.charAt(1)+clr.charAt(1)+clr.charAt(2)+clr.charAt(2)),from===to||("a"===from.charAt(from.length-1)&&(from=from.slice(0,-1)),(needsAlpha="a"===to.charAt(to.length-1))&&(to=to.slice(0,-1),alpha=this._getAlpha(clr)),ucTo=to.charAt(0).toUpperCase()+to.substr(1).toLowerCase(),(method=window["_"+from+"To"+ucTo])||"rgb"!==from&&"rgb"!==to&&(clr=window["_"+from+"ToRgb"](clr),from="rgb",method=window["_"+from+"To"+ucTo]),method&&(clr=method(clr,needsAlpha)),needsAlpha&&(Array.isArray(clr)||(clr=this.toArray(clr)),clr.push(alpha),clr=this.fromArray(clr,originalTo.toUpperCase()))),clr})),_defineProperty(this,"_hexToRgb",((str,array)=>{var r,g,b;return"#"===str.charAt(0)&&(str=str.substr(1)),r=(str=parseInt(str,16))>>16,g=str>>8&255,b=255&str,array?[r,g,b]:"rgb("+r+", "+g+", "+b+")"})),_defineProperty(this,"_rgbToHex",(str=>{var rgb=this.toArray(str),hex=rgb[2]|rgb[1]<<8|rgb[0]<<16;for(hex=(+hex).toString(16);hex.length<6;)hex="0"+hex;return"#"+hex})),_defineProperty(this,"_getRightValue",(string=>{let regex=null;return"REGEX_RGB"===string?regex=this.REGEX_RGB:"REGEX_HEX"===string?regex=this.REGEX_HEX:"REGEX_HEX3"===string?regex=this.REGEX_HEX3:"STR_HEX"===string?regex=this.STR_HEX:"STR_RGB"===string?regex=this.STR_RGB:"STR_RGBA"===string&&(regex=this.STR_RGBA),regex}))}},_exports.default})); + +//# sourceMappingURL=colorbase.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js.map b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js.map new file mode 100644 index 00000000000..05bd4d8701c --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/colorbase.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"colorbase.min.js","sources":["../src/colorbase.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\nexport default class {\n\n REGEX_HEX = /^#?([\\da-fA-F]{2})([\\da-fA-F]{2})([\\da-fA-F]{2})(\\ufffe)?/;\n REGEX_HEX3 = /^#?([\\da-fA-F]{1})([\\da-fA-F]{1})([\\da-fA-F]{1})(\\ufffe)?/;\n REGEX_RGB = /rgba?\\(([\\d]{1,3}), ?([\\d]{1,3}), ?([\\d]{1,3}),? ?([.\\d]*)?\\)/;\n\n TYPES = {\n HEX: 'hex',\n RGB: 'rgb',\n RGBA: 'rgba'\n };\n\n KEYWORDS = {\n black: '000',\n silver: 'c0c0c0',\n gray: '808080',\n white: 'fff',\n maroon: '800000',\n red: 'f00',\n purple: '800080',\n fuchsia: 'f0f',\n green: '008000',\n lime: '0f0',\n olive: '808000',\n yellow: 'ff0',\n navy: '000080',\n blue: '00f',\n teal: '008080',\n aqua: '0ff'\n };\n\n STR_HEX = '#{*}{*}{*}';\n STR_RGB = 'rgb({*}, {*}, {*})';\n STR_RGBA = 'rgba({*}, {*}, {*}, {*})';\n\n toHex = (str) => {\n var clr = this._convertTo(str, 'hex'),\n isTransparent = clr.toLowerCase() === 'transparent';\n\n if (clr.charAt(0) !== '#' && !isTransparent) {\n clr = '#' + clr;\n }\n\n return isTransparent ? clr.toLowerCase() : clr.toUpperCase();\n };\n\n toRGB = (str) => {\n var clr = this._convertTo(str, 'rgb');\n return clr.toLowerCase();\n };\n\n toRGBA = (str) => {\n var clr = this._convertTo(str, 'rgba');\n return clr.toLowerCase();\n };\n\n toArray = (str) => {\n // Parse with regex and return \"matches\" array.\n var type = this.findType(str).toUpperCase(),\n regex,\n arr,\n length,\n lastItem;\n\n if (type === 'HEX' && str.length < 5) {\n type = 'HEX3';\n }\n\n if (type.charAt(type.length - 1) === 'A') {\n type = type.slice(0, -1);\n }\n\n regex = this._getRightValue('REGEX_' + type);\n\n if (regex) {\n arr = regex.exec(str) || [];\n length = arr.length;\n\n if (length) {\n arr.shift();\n length--;\n\n if (type === 'HEX3') {\n arr[0] += arr[0];\n arr[1] += arr[1];\n arr[2] += arr[2];\n }\n lastItem = arr[length - 1];\n\n if (!lastItem) {\n arr[length - 1] = 1;\n }\n }\n }\n return arr;\n };\n\n fromArray = (arr, template) => {\n arr = arr.concat();\n\n if (typeof template === 'undefined') {\n return arr.join(', ');\n }\n\n var replace = '{*}';\n template = this._getRightValue('STR_' + template.toUpperCase());\n\n if (arr.length === 3 && template.match(/\\{\\*\\}/g).length === 4) {\n arr.push(1);\n }\n\n while (template.indexOf(replace) >= 0 && arr.length > 0) {\n template = template.replace(replace, arr.shift());\n }\n return template;\n };\n\n findType = (str) => {\n if (this.KEYWORDS[str]) {\n return 'keyword';\n }\n\n var index = str.indexOf('('),\n key;\n\n if (index > 0) {\n key = str.substr(0, index);\n }\n\n if (key && this.TYPES[key.toUpperCase()]) {\n return this.TYPES[key.toUpperCase()];\n }\n return 'hex';\n };\n\n _getAlpha = (clr) => {\n var alpha,\n arr = this.toArray(clr);\n\n if (arr.length > 3) {\n alpha = arr.pop();\n }\n return +alpha || 1;\n };\n\n _keywordToHex = (clr) => {\n var keyword = this.KEYWORDS[clr];\n\n if (keyword) {\n return keyword;\n }\n return keyword;\n };\n\n _convertTo = (clr, to) => {\n if (clr === 'transparent') {\n return clr;\n }\n\n var from = this.findType(clr),\n originalTo = to,\n needsAlpha,\n alpha,\n method,\n ucTo;\n\n if (from === 'keyword') {\n clr = this._keywordToHex(clr);\n from = 'hex';\n }\n\n if (from === 'hex' && clr.length < 5) {\n if (clr.charAt(0) === '#') {\n clr = clr.substr(1);\n }\n\n clr = '#' + clr.charAt(0) + clr.charAt(0) +\n clr.charAt(1) + clr.charAt(1) +\n clr.charAt(2) + clr.charAt(2);\n }\n\n if (from === to) {\n return clr;\n }\n\n if (from.charAt(from.length - 1) === 'a') {\n from = from.slice(0, -1);\n }\n\n needsAlpha = (to.charAt(to.length - 1) === 'a');\n if (needsAlpha) {\n to = to.slice(0, -1);\n alpha = this._getAlpha(clr);\n }\n\n ucTo = to.charAt(0).toUpperCase() + to.substr(1).toLowerCase();\n method = window['_' + from + 'To' + ucTo];\n\n // Check to see if need conversion to rgb first.\n // Check to see if there is a direct conversion method.\n // Convertions are: hex <-> rgb <-> hsl.\n if (!method) {\n if (from !== 'rgb' && to !== 'rgb') {\n clr = window['_' + from + 'ToRgb'](clr);\n from = 'rgb';\n method = window['_' + from + 'To' + ucTo];\n }\n }\n\n if (method) {\n clr = ((method)(clr, needsAlpha));\n }\n\n // Process clr from arrays to strings after conversions if alpha is needed.\n if (needsAlpha) {\n if (!Array.isArray(clr)) {\n clr = this.toArray(clr);\n }\n clr.push(alpha);\n clr = this.fromArray(clr, originalTo.toUpperCase());\n }\n return clr;\n };\n\n _hexToRgb = (str, array) => {\n var r, g, b;\n\n /* jshint bitwise:false */\n if (str.charAt(0) === '#') {\n str = str.substr(1);\n }\n\n /* eslint no-bitwise: */\n str = parseInt(str, 16);\n r = str >> 16;\n g = str >> 8 & 0xFF;\n b = str & 0xFF;\n\n if (array) {\n return [r, g, b];\n }\n return 'rgb(' + r + ', ' + g + ', ' + b + ')';\n };\n\n _rgbToHex = (str) => {\n /* jshint bitwise:false */\n var rgb = this.toArray(str),\n hex = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);\n\n hex = (+hex).toString(16);\n\n while (hex.length < 6) {\n hex = '0' + hex;\n }\n return '#' + hex;\n };\n\n _getRightValue = (string) => {\n let regex = null;\n if (string === 'REGEX_RGB') {\n regex = this.REGEX_RGB;\n } else if (string === 'REGEX_HEX') {\n regex = this.REGEX_HEX;\n } else if (string === 'REGEX_HEX3') {\n regex = this.REGEX_HEX3;\n } else if (string === 'STR_HEX') {\n regex = this.STR_HEX;\n } else if (string === 'STR_RGB') {\n regex = this.STR_RGB;\n } else if (string === 'STR_RGBA') {\n regex = this.STR_RGBA;\n }\n return regex;\n };\n}\n"],"names":["HEX","RGB","RGBA","black","silver","gray","white","maroon","red","purple","fuchsia","green","lime","olive","yellow","navy","blue","teal","aqua","str","clr","this","_convertTo","isTransparent","toLowerCase","charAt","toUpperCase","regex","arr","length","type","findType","slice","_getRightValue","exec","shift","template","concat","join","match","push","indexOf","replace","KEYWORDS","key","index","substr","TYPES","alpha","toArray","pop","keyword","to","needsAlpha","method","ucTo","from","originalTo","_keywordToHex","_getAlpha","window","Array","isArray","fromArray","array","r","g","b","parseInt","rgb","hex","toString","string","REGEX_RGB","REGEX_HEX","REGEX_HEX3","STR_HEX","STR_RGB","STR_RGBA"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js new file mode 100644 index 00000000000..7bd3e12a926 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js @@ -0,0 +1,10 @@ +define("tiny_accessibilitychecker/commands",["exports","core/str","./common","./checker"],(function(_exports,_str,_common,_checker){var obj; +/** + * Tiny Media Manager commands. + * + * @module tiny_accessibilitychecker/commands + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0,_checker=(obj=_checker)&&obj.__esModule?obj:{default:obj};_exports.getSetup=async()=>{const[buttonTooltip]=await Promise.all([(0,_str.get_string)("pluginname",_common.component)]);return editor=>{editor.ui.registry.addButton(_common.accessbilityButtonName,{icon:_common.icon,tooltip:buttonTooltip,onAction:()=>{new _checker.default(editor).displayDialogue()}}),editor.ui.registry.addMenuItem(_common.accessbilityButtonName,{icon:_common.icon,text:buttonTooltip,onAction:()=>{new _checker.default(editor).displayDialogue()}})}}})); + +//# sourceMappingURL=commands.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js.map b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js.map new file mode 100644 index 00000000000..fb7418a0e0a --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/commands.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.min.js","sources":["../src/commands.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 * Tiny Media Manager commands.\n *\n * @module tiny_accessibilitychecker/commands\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {get_string as getString} from 'core/str';\nimport {\n component,\n accessbilityButtonName,\n icon,\n} from './common';\nimport Checker from './checker';\n\nexport const getSetup = async() => {\n const [\n buttonTooltip,\n ] = await Promise.all([\n getString('pluginname', component),\n ]);\n\n return (editor) => {\n // Register the Menu Button as a toggle.\n editor.ui.registry.addButton(accessbilityButtonName, {\n icon,\n tooltip: buttonTooltip,\n onAction: () => {\n const checker = new Checker(editor);\n checker.displayDialogue();\n }\n });\n\n editor.ui.registry.addMenuItem(accessbilityButtonName, {\n icon,\n text: buttonTooltip,\n onAction: () => {\n const checker = new Checker(editor);\n checker.displayDialogue();\n }\n });\n };\n};\n"],"names":["async","buttonTooltip","Promise","all","component","editor","ui","registry","addButton","accessbilityButtonName","icon","tooltip","onAction","Checker","displayDialogue","addMenuItem","text"],"mappings":";;;;;;;kKA+BwBA,gBAEhBC,qBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,4BAGpBC,SAEJA,OAAOC,GAAGC,SAASC,UAAUC,+BAAwB,CACjDC,KAAAA,aACAC,QAASV,cACTW,SAAU,KACU,IAAIC,iBAAQR,QACpBS,qBAIhBT,OAAOC,GAAGC,SAASQ,YAAYN,+BAAwB,CACnDC,KAAAA,aACAM,KAAMf,cACNW,SAAU,KACU,IAAIC,iBAAQR,QACpBS"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js new file mode 100644 index 00000000000..45510a8308d --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js @@ -0,0 +1,3 @@ +define("tiny_accessibilitychecker/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_accessibilitychecker/plugin",component:"tiny_accessibilitychecker",accessbilityButtonName:"tiny_accessibilitychecker",icon:"accessibility-check"},_exports.default})); + +//# sourceMappingURL=common.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js.map b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js.map new file mode 100644 index 00000000000..ec197868e5d --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/common.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"common.min.js","sources":["../src/common.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 * Tiny Media common values.\n *\n * @module tiny_accessibilitychecker/common\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_accessibilitychecker/plugin',\n component: 'tiny_accessibilitychecker',\n accessbilityButtonName: 'tiny_accessibilitychecker',\n icon: 'accessibility-check',\n};\n"],"names":["pluginName","component","accessbilityButtonName","icon"],"mappings":"kLAuBe,CACXA,WAAY,mCACZC,UAAW,4BACXC,uBAAwB,4BACxBC,KAAM"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js new file mode 100644 index 00000000000..13c3b992fd3 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js @@ -0,0 +1,3 @@ +define("tiny_accessibilitychecker/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>({menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"tools",_common.accessbilityButtonName)})})); + +//# sourceMappingURL=configuration.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js.map new file mode 100644 index 00000000000..fb0ad3d3163 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/configuration.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configuration.min.js","sources":["../src/configuration.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 * Tiny Media Manager configuration.\n *\n * @module tiny_accessibilitychecker/configuration\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {accessbilityButtonName} from './common';\nimport {\n addMenubarItem,\n} from 'editor_tiny/utils';\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Tools menu.\n return {\n menu: addMenubarItem(instanceConfig.menu, 'tools', accessbilityButtonName)\n };\n};\n"],"names":["instanceConfig","menu","accessbilityButtonName"],"mappings":"oOA4B0BA,iBAEf,CACHC,MAAM,yBAAeD,eAAeC,KAAM,QAASC"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js new file mode 100644 index 00000000000..34b9f3d93f8 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js @@ -0,0 +1,10 @@ +define("tiny_accessibilitychecker/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration"],(function(_exports,_loader,_utils,_common,Commands,Configuration){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * Tiny Media Manager plugin for Moodle. + * + * @module tiny_accessibilitychecker/plugin + * @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,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration);var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(setupCommands(editor),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js.map b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js.map new file mode 100644 index 00000000000..87ef18160dd --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/build/plugin.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"plugin.min.js","sources":["../src/plugin.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 * Tiny Media Manager plugin for Moodle.\n *\n * @module tiny_accessibilitychecker/plugin\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from './common';\nimport * as Commands from './commands';\nimport * as Configuration from './configuration';\n\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n setupCommands,\n pluginMetadata,\n ] = await Promise.all([\n getTinyMCE(),\n Commands.getSetup(),\n getPluginMetadata(component, pluginName),\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n return pluginMetadata;\n });\n\n // Resolve the Media Plugin and include configuration.\n resolve([`${component}/plugin`, Configuration]);\n});\n"],"names":["Promise","async","tinyMCE","setupCommands","pluginMetadata","all","Commands","getSetup","component","pluginName","PluginManager","add","editor","resolve","Configuration"],"mappings":";;;;;;;kMA6Be,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CT,cAAcS,QAEPR,kBAIXS,QAAQ,WAAIL,6BAAoBM"} \ 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 new file mode 100644 index 00000000000..9f24324400e --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/checker.js @@ -0,0 +1,484 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/* + * @package tiny_accessibilitychecker + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; +import {get_string as getString, get_strings as getStrings} from 'core/str'; +import {component} from './common'; +import * as Modal from 'core/modal_factory'; +import * as ModalEvents from 'core/modal_events'; +import ColorBase from './colorbase'; + +/** + * @typedef ProblemDetail + * @type {object} + * @param {string} description The description of the problem + * @param {ProblemNode[]} problemNodes The list of affected nodes + */ + +/** + * @typedef ProblemNode + * @type {object} + * @param {string} nodeName The node name for the affected node + * @param {string} nodeIndex The indexd of the node + * @param {string} text A description of the issue + * @param {string} src The source of the image + */ + +export default class { + + constructor(editor) { + this.editor = editor; + this.colorBase = new ColorBase(); + 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: true, + title: getString('pluginname', component), + body: this.getDialogueContent() + }); + + // Destroy the class when hiding the modal. + 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(',') || nodeName === 'body') { + selectedNode = this.editor.dom.select('body')[0]; + } else { + const nodeIndex = faultLink.dataset.nodeIndex ?? 0; + selectedNode = this.editor.dom.select(nodeName)[nodeIndex]; + } + } + + if (selectedNode && selectedNode.nodeName.toUpperCase() !== 'BODY') { + this.selectAndScroll(selectedNode); + } + + this.modal.hide(); + }); + + this.modal.show(); + } + + async getAllWarningStrings() { + const keys = [ + 'emptytext', + 'entiredocument', + 'imagesmissingalt', + 'needsmorecontrast', + 'needsmoreheadings', + 'nowarnings', + 'nowarningsonselected', + 'tablesmissingcaption', + 'tablesmissingheaders', + 'tableswithmergedcells', + ]; + + const stringValues = await getStrings(keys.map((key) => ({key, component}))); + return new Map(keys.map((key, index) => ([key, stringValues[index]]))); + } + + /** + * Return the dialogue content. + * + * @return {Promise} A template promise containing the rendered dialogue content. + */ + async getDialogueContent() { + const langStrings = await this.getAllWarningStrings(); + + // Translate langstrings into real strings. + const warnings = this.getWarnings().map((warning) => { + if (warning.description) { + if (warning.description.type === 'langstring') { + warning.description = langStrings.get(warning.description.value); + } else { + warning.description = warning.description.value; + } + } + + warning.nodeData = warning.nodeData.map((problemNode) => { + if (problemNode.text) { + if (problemNode.text.type === 'langstring') { + problemNode.text = langStrings.get(problemNode.text.value); + } else { + problemNode.text = problemNode.text.value; + } + } + + return problemNode; + }); + + return warning; + }); + + return Templates.render('tiny_accessibilitychecker/warning_content', { + warnings + }); + } + + /** + * Set the selection and scroll to the selected element. + * + * @param {node} node + */ + selectAndScroll(node) { + this.editor.selection.select(node).scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + } + + /** + * Find all problems with the content editable region. + * + * @return {ProblemDetail[]} A complete list of all warnings and problems. + */ + getWarnings() { + const warnings = []; + + // Check Images with no alt text or dodgy alt text. + warnings.push(this.createWarnings('imagesmissingalt', this.checkImage(), true)); + warnings.push(this.createWarnings('needsmorecontrast', this.checkOtherElements(), false)); + + // Check for no headings. + if (this.editor.getContent({format: 'text'}).length > 1000 && this.editor.dom.select('h3,h4,h5').length < 1) { + warnings.push(this.createWarnings('needsmoreheadings', [this.editor], false)); + } + + // Check for tables with no captions. + warnings.push(this.createWarnings('tablesmissingcaption', this.checkTableCaption(), false)); + + // Check for tables with merged cells. + warnings.push(this.createWarnings('tableswithmergedcells', this.checkTableMergedCells(), false)); + + // Check for tables with no row/col headers. + warnings.push(this.createWarnings('tablesmissingheaders', this.checkTableHeaders(), false)); + + return warnings.filter((warning) => warning.nodeData.length > 0); + } + + /** + * Generate the data that describes the issues found. + * + * @param {String} description Description of this failure. + * @param {HTMLElement[]} nodes An array of failing nodes. + * @param {boolean} isImageType Whether the warnings are related to image type checks + * @return {ProblemDetail[]} A set of problem details + */ + 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} : emptyStringValue; + } else if ('textContent' in node) { + const value = node.textContent.trim(); + return value.length ? {type: 'raw', value} : emptyStringValue; + } + + return {type: 'raw', value: node.nodeName}; + }; + + const getEventualNode = (node) => { + if (node !== this.editor) { + return node; + } + const childNodes = node.dom.select('body')[0].childNodes; + if (childNodes.length) { + return document.body; + } else { + return childNodes; + } + }; + + const warning = { + description: { + type: 'langstring', + value: description, + }, + nodeData: [], + }; + + warning.nodeData = [...nodes].map((node) => { + const describedNode = getEventualNode(node); + + // Find the index of the node within the type of node. + // This is used to select the correct node when the user selects it. + const nodeIndex = this.editor.dom.select(describedNode.nodeName).indexOf(describedNode); + const warning = { + src: null, + text: null, + nodeName: describedNode.nodeName, + nodeIndex, + }; + + if (isImageType) { + warning.src = node.getAttribute('src'); + } else { + warning.text = getTextValue(node); + } + + return warning; + }); + + return warning; + } + + /** + * Check accessiblity issue only for img type. + * + * @return {Node} A complete list of all warnings and problems. + */ + checkImage() { + const problemNodes = []; + this.editor.dom.select('img').forEach((img) => { + const alt = img.getAttribute('alt'); + if (!alt && img.getAttribute('role') !== 'presentation') { + problemNodes.push(img); + } + }); + return problemNodes; + } + + /** + * Look for any table without a caption. + * + * @return {Node} A complete list of all warnings and problems. + */ + checkTableCaption() { + const problemNodes = []; + this.editor.dom.select('table').forEach((table) => { + const caption = table.querySelector('caption'); + if (!caption?.textContent.trim()) { + problemNodes.push(table); + } + }); + + return problemNodes; + } + + /** + * Check accessiblity issue for not img and table only. + * + * @return {Node} A complete list of all warnings and problems. + * @private + */ + checkOtherElements() { + const problemNodes = []; + + const getRatio = (lum1, lum2) => { + // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html". + if (lum1 > lum2) { + return (lum1 + 0.05) / (lum2 + 0.05); + } else { + return (lum2 + 0.05) / (lum1 + 0.05); + } + }; + this.editor.dom.select('body > *') + .filter((node) => node.textContent?.trim().length) + .forEach((node) => { + const foreground = this.colorBase.fromArray( + this.getComputedBackgroundColor( + node, + window.getComputedStyle(node, null).getPropertyValue('color') + ), + this.colorBase.TYPES.RGBA + ); + const background = this.colorBase.fromArray( + this.getComputedBackgroundColor( + node + ), + this.colorBase.TYPES.RGBA + ); + + const lum1 = this.getLuminanceFromCssColor(foreground); + const lum2 = this.getLuminanceFromCssColor(background); + const ratio = getRatio(lum1, lum2); + + if (ratio <= 4.5) { + window.console.log(` + Contrast ratio is too low: ${ratio} + Colour 1: ${foreground} + Colour 2: ${background} + Luminance 1: ${lum1} + Luminance 2: ${lum2} + `); + + // We only want the highest node with dodgy contrast reported. + if (!problemNodes.find((existingProblemNode) => existingProblemNode.contains(node))) { + problemNodes.push(node); + } + } + }); + return problemNodes; + } + + /** + * Check accessiblity issue only for table with merged cells. + * + * @return {Node} A complete list of all warnings and problems. + * @private + */ + checkTableMergedCells() { + const problemNodes = []; + this.editor.dom.select('table').forEach((table) => { + const rowcolspan = table.querySelectorAll('[colspan], [rowspan]'); + if (rowcolspan.length) { + problemNodes.push(table); + } + }); + return problemNodes; + } + + /** + * Check accessiblity issue only for table with no headers. + * + * @return {Node} A complete list of all warnings and problems. + * @private + */ + checkTableHeaders() { + const problemNodes = []; + + this.editor.dom.select('table').forEach((table) => { + if (table.querySelector('tr').querySelector('td')) { + // The first row has a non-header cell, so all rows must have at least one header. + const missingHeader = [...table.querySelectorAll('tr')].some((row) => { + const header = row.querySelector('th'); + if (!header) { + return true; + } + + if (!header.textContent.trim()) { + return true; + } + + return false; + }); + if (missingHeader) { + // At least one row is missing the header, or it is empty. + problemNodes.push(table); + } + } else { + // Every header must have some content. + if ([...table.querySelectorAll('tr th')].some((header) => !header.textContent.trim())) { + problemNodes.push(table); + } + } + }); + return problemNodes; + } + + /** + * Convert a CSS color to a luminance value. + * + * @param {String} colortext The Hex value for the colour + * @return {Number} The luminance value. + * @private + */ + getLuminanceFromCssColor(colortext) { + if (colortext === 'transparent') { + colortext = '#ffffff'; + } + const color = this.colorBase.toArray(this.colorBase.toRGB(colortext)); + + // Algorithm from "http://www.w3.org/TR/WCAG20-GENERAL/G18.html". + const part1 = (a) => { + a = parseInt(a, 10) / 255.0; + if (a <= 0.03928) { + a = a / 12.92; + } else { + a = Math.pow(((a + 0.055) / 1.055), 2.4); + } + return a; + }; + + const r1 = part1(color[0]); + const g1 = part1(color[1]); + const b1 = part1(color[2]); + + return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1; + } + + /** + * Get the computed RGB converted to full alpha value, considering the node hierarchy. + * + * @param {Node} node + * @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node. + * @return {Array} Colour in Array form (RGBA) + * @private + */ + getComputedBackgroundColor(node, color) { + color = color ? color : window.getComputedStyle(node, null).getPropertyValue('background-color'); + if (color.toLowerCase() === 'transparent') { + color = 'rgba(1, 1, 1, 0)'; + } + + // Convert the colour to its constituent parts in RGBA format, then fetch the alpha. + const colorParts = this.colorBase.toArray(color); + const alpha = colorParts[3]; + + if (alpha === 1) { + // If the alpha of the background is already 1, then the parent background colour does not change anything. + return colorParts; + } + + // Fetch the computed background colour of the parent and use it to calculate the RGB of this item. + const parentColor = this.getComputedBackgroundColor(node.parentNode); + return [ + // RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour). + (1 - alpha) * parentColor[0] + alpha * colorParts[0], + (1 - alpha) * parentColor[1] + alpha * colorParts[1], + (1 - alpha) * parentColor[2] + alpha * colorParts[2], + // We always return a colour with full alpha. + 1 + ]; + } +} diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/colorbase.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/colorbase.js new file mode 100644 index 00000000000..f83d68dd562 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/colorbase.js @@ -0,0 +1,296 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/* + * @package tiny_accessibilitychecker + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default class { + + REGEX_HEX = /^#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})(\ufffe)?/; + REGEX_HEX3 = /^#?([\da-fA-F]{1})([\da-fA-F]{1})([\da-fA-F]{1})(\ufffe)?/; + REGEX_RGB = /rgba?\(([\d]{1,3}), ?([\d]{1,3}), ?([\d]{1,3}),? ?([.\d]*)?\)/; + + TYPES = { + HEX: 'hex', + RGB: 'rgb', + RGBA: 'rgba' + }; + + KEYWORDS = { + black: '000', + silver: 'c0c0c0', + gray: '808080', + white: 'fff', + maroon: '800000', + red: 'f00', + purple: '800080', + fuchsia: 'f0f', + green: '008000', + lime: '0f0', + olive: '808000', + yellow: 'ff0', + navy: '000080', + blue: '00f', + teal: '008080', + aqua: '0ff' + }; + + STR_HEX = '#{*}{*}{*}'; + STR_RGB = 'rgb({*}, {*}, {*})'; + STR_RGBA = 'rgba({*}, {*}, {*}, {*})'; + + toHex = (str) => { + var clr = this._convertTo(str, 'hex'), + isTransparent = clr.toLowerCase() === 'transparent'; + + if (clr.charAt(0) !== '#' && !isTransparent) { + clr = '#' + clr; + } + + return isTransparent ? clr.toLowerCase() : clr.toUpperCase(); + }; + + toRGB = (str) => { + var clr = this._convertTo(str, 'rgb'); + return clr.toLowerCase(); + }; + + toRGBA = (str) => { + var clr = this._convertTo(str, 'rgba'); + return clr.toLowerCase(); + }; + + toArray = (str) => { + // Parse with regex and return "matches" array. + var type = this.findType(str).toUpperCase(), + regex, + arr, + length, + lastItem; + + if (type === 'HEX' && str.length < 5) { + type = 'HEX3'; + } + + if (type.charAt(type.length - 1) === 'A') { + type = type.slice(0, -1); + } + + regex = this._getRightValue('REGEX_' + type); + + if (regex) { + arr = regex.exec(str) || []; + length = arr.length; + + if (length) { + arr.shift(); + length--; + + if (type === 'HEX3') { + arr[0] += arr[0]; + arr[1] += arr[1]; + arr[2] += arr[2]; + } + lastItem = arr[length - 1]; + + if (!lastItem) { + arr[length - 1] = 1; + } + } + } + return arr; + }; + + fromArray = (arr, template) => { + arr = arr.concat(); + + if (typeof template === 'undefined') { + return arr.join(', '); + } + + var replace = '{*}'; + template = this._getRightValue('STR_' + template.toUpperCase()); + + if (arr.length === 3 && template.match(/\{\*\}/g).length === 4) { + arr.push(1); + } + + while (template.indexOf(replace) >= 0 && arr.length > 0) { + template = template.replace(replace, arr.shift()); + } + return template; + }; + + findType = (str) => { + if (this.KEYWORDS[str]) { + return 'keyword'; + } + + var index = str.indexOf('('), + key; + + if (index > 0) { + key = str.substr(0, index); + } + + if (key && this.TYPES[key.toUpperCase()]) { + return this.TYPES[key.toUpperCase()]; + } + return 'hex'; + }; + + _getAlpha = (clr) => { + var alpha, + arr = this.toArray(clr); + + if (arr.length > 3) { + alpha = arr.pop(); + } + return +alpha || 1; + }; + + _keywordToHex = (clr) => { + var keyword = this.KEYWORDS[clr]; + + if (keyword) { + return keyword; + } + return keyword; + }; + + _convertTo = (clr, to) => { + if (clr === 'transparent') { + return clr; + } + + var from = this.findType(clr), + originalTo = to, + needsAlpha, + alpha, + method, + ucTo; + + if (from === 'keyword') { + clr = this._keywordToHex(clr); + from = 'hex'; + } + + if (from === 'hex' && clr.length < 5) { + if (clr.charAt(0) === '#') { + clr = clr.substr(1); + } + + clr = '#' + clr.charAt(0) + clr.charAt(0) + + clr.charAt(1) + clr.charAt(1) + + clr.charAt(2) + clr.charAt(2); + } + + if (from === to) { + return clr; + } + + if (from.charAt(from.length - 1) === 'a') { + from = from.slice(0, -1); + } + + needsAlpha = (to.charAt(to.length - 1) === 'a'); + if (needsAlpha) { + to = to.slice(0, -1); + alpha = this._getAlpha(clr); + } + + ucTo = to.charAt(0).toUpperCase() + to.substr(1).toLowerCase(); + method = window['_' + from + 'To' + ucTo]; + + // Check to see if need conversion to rgb first. + // Check to see if there is a direct conversion method. + // Convertions are: hex <-> rgb <-> hsl. + if (!method) { + if (from !== 'rgb' && to !== 'rgb') { + clr = window['_' + from + 'ToRgb'](clr); + from = 'rgb'; + method = window['_' + from + 'To' + ucTo]; + } + } + + if (method) { + clr = ((method)(clr, needsAlpha)); + } + + // Process clr from arrays to strings after conversions if alpha is needed. + if (needsAlpha) { + if (!Array.isArray(clr)) { + clr = this.toArray(clr); + } + clr.push(alpha); + clr = this.fromArray(clr, originalTo.toUpperCase()); + } + return clr; + }; + + _hexToRgb = (str, array) => { + var r, g, b; + + /* jshint bitwise:false */ + if (str.charAt(0) === '#') { + str = str.substr(1); + } + + /* eslint no-bitwise: */ + str = parseInt(str, 16); + r = str >> 16; + g = str >> 8 & 0xFF; + b = str & 0xFF; + + if (array) { + return [r, g, b]; + } + return 'rgb(' + r + ', ' + g + ', ' + b + ')'; + }; + + _rgbToHex = (str) => { + /* jshint bitwise:false */ + var rgb = this.toArray(str), + hex = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16); + + hex = (+hex).toString(16); + + while (hex.length < 6) { + hex = '0' + hex; + } + return '#' + hex; + }; + + _getRightValue = (string) => { + let regex = null; + if (string === 'REGEX_RGB') { + regex = this.REGEX_RGB; + } else if (string === 'REGEX_HEX') { + regex = this.REGEX_HEX; + } else if (string === 'REGEX_HEX3') { + regex = this.REGEX_HEX3; + } else if (string === 'STR_HEX') { + regex = this.STR_HEX; + } else if (string === 'STR_RGB') { + regex = this.STR_RGB; + } else if (string === 'STR_RGBA') { + regex = this.STR_RGBA; + } + return regex; + }; +} diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/commands.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/commands.js new file mode 100644 index 00000000000..42b544bd350 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/commands.js @@ -0,0 +1,59 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny Media Manager commands. + * + * @module tiny_accessibilitychecker/commands + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {get_string as getString} from 'core/str'; +import { + component, + accessbilityButtonName, + icon, +} from './common'; +import Checker from './checker'; + +export const getSetup = async() => { + const [ + buttonTooltip, + ] = await Promise.all([ + getString('pluginname', component), + ]); + + return (editor) => { + // Register the Menu Button as a toggle. + editor.ui.registry.addButton(accessbilityButtonName, { + icon, + tooltip: buttonTooltip, + onAction: () => { + const checker = new Checker(editor); + checker.displayDialogue(); + } + }); + + editor.ui.registry.addMenuItem(accessbilityButtonName, { + icon, + text: buttonTooltip, + onAction: () => { + const checker = new Checker(editor); + checker.displayDialogue(); + } + }); + }; +}; diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/common.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/common.js new file mode 100644 index 00000000000..d8fb293d7b2 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/common.js @@ -0,0 +1,29 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny Media common values. + * + * @module tiny_accessibilitychecker/common + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default { + pluginName: 'tiny_accessibilitychecker/plugin', + component: 'tiny_accessibilitychecker', + accessbilityButtonName: 'tiny_accessibilitychecker', + icon: 'accessibility-check', +}; diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/configuration.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/configuration.js new file mode 100644 index 00000000000..5acea834a06 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/configuration.js @@ -0,0 +1,34 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny Media Manager configuration. + * + * @module tiny_accessibilitychecker/configuration + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {accessbilityButtonName} from './common'; +import { + addMenubarItem, +} from 'editor_tiny/utils'; + +export const configure = (instanceConfig) => { + // Update the instance configuration to add the Tools menu. + return { + menu: addMenubarItem(instanceConfig.menu, 'tools', accessbilityButtonName) + }; +}; diff --git a/lib/editor/tiny/plugins/accessibilitychecker/amd/src/plugin.js b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/plugin.js new file mode 100644 index 00000000000..00cf71f5bc1 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/amd/src/plugin.js @@ -0,0 +1,50 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny Media Manager plugin for Moodle. + * + * @module tiny_accessibilitychecker/plugin + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import {getTinyMCE} from 'editor_tiny/loader'; +import {getPluginMetadata} from 'editor_tiny/utils'; + +import {component, pluginName} from './common'; +import * as Commands from './commands'; +import * as Configuration from './configuration'; + +export default new Promise(async(resolve) => { + const [ + tinyMCE, + setupCommands, + pluginMetadata, + ] = await Promise.all([ + getTinyMCE(), + Commands.getSetup(), + getPluginMetadata(component, pluginName), + ]); + + tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => { + // Setup the Commands (buttons, menu items, and so on). + setupCommands(editor); + + return pluginMetadata; + }); + + // Resolve the Media Plugin and include configuration. + resolve([`${component}/plugin`, Configuration]); +}); diff --git a/lib/editor/tiny/plugins/accessibilitychecker/classes/plugininfo.php b/lib/editor/tiny/plugins/accessibilitychecker/classes/plugininfo.php new file mode 100644 index 00000000000..9d5b821380c --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/classes/plugininfo.php @@ -0,0 +1,61 @@ +. + +namespace tiny_accessibilitychecker; + +use context; +use editor_tiny\editor; +use editor_tiny\plugin; +use editor_tiny\plugin_with_buttons; +use editor_tiny\plugin_with_configuration; +use editor_tiny\plugin_with_menuitems; + +/** + * Tiny media manager plugin. + * + * @package tiny_accessibilitychecker + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems, plugin_with_configuration { + + public static function get_available_buttons(): array { + return [ + 'tiny_accessibilitychecker/tiny_accessibilitychecker_image', + ]; + } + + public static function get_available_menuitems(): array { + return [ + 'tiny_accessibilitychecker/tiny_accessibilitychecker_image', + ]; + } + + public static function get_plugin_configuration_for_context( + context $context, + array $options, + array $fpoptions, + ?editor $editor = null + ): array { + $permissions = [ + 'upload' => true, + ]; + return [ + 'permissions' => $permissions, + 'storeinrepo' => true, + ]; + } +} diff --git a/lib/editor/tiny/plugins/accessibilitychecker/classes/privacy/provider.php b/lib/editor/tiny/plugins/accessibilitychecker/classes/privacy/provider.php new file mode 100644 index 00000000000..0ad54397bdf --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/classes/privacy/provider.php @@ -0,0 +1,35 @@ +. + +namespace tiny_accessibilitychecker\privacy; + +/** + * Privacy Subsystem implementation for the accessibilitychecker plugin for TinyMCE. + * + * @package tiny_accessibilitychecker + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + /** + * Return the langstring identifier for the reason that no privacy provider needs to be implemented for this plugin. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/lib/editor/tiny/plugins/accessibilitychecker/lang/en/tiny_accessibilitychecker.php b/lib/editor/tiny/plugins/accessibilitychecker/lang/en/tiny_accessibilitychecker.php new file mode 100644 index 00000000000..bda2ab5f64c --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/lang/en/tiny_accessibilitychecker.php @@ -0,0 +1,38 @@ +. + +/** + * Strings for component 'tiny_accessibilitychecker', language 'en'. + * + * @package tiny_accessibilitychecker + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['emptytext'] = 'Empty text'; +$string['entiredocument'] = 'Entire document'; +$string['imagesmissingalt'] = 'Images require alternative text. To fix this warning, add an alt attribute to your img tags. An empty alt attribute may be used, but only when the image is purely decorative and carries no information.'; +$string['needsmorecontrast'] = 'The colours of the foreground and background text do not have enough contrast. To fix this warning, change either foreground or background colour of the text so that it is easier to read.'; +$string['needsmoreheadings'] = 'There is a lot of text with no headings. Headings will allow screen reader users to navigate through the page easily and will make the page more usable for everyone.'; +$string['nowarnings'] = 'Congratulations, no accessibility issues found!'; +$string['pluginname'] = 'Accessibility checker'; +$string['report'] = 'Accessibility report:'; +$string['tablesmissingcaption'] = 'Tables should have captions. While it is not necessary for each table to have a caption, a caption is generally very helpful.'; +$string['tablesmissingheaders'] = 'Tables should use row and/or column headers.'; +$string['tableswithmergedcells'] = 'Tables should not contain merged cells. Despite being standard markup for tables for many years, some screen readers still do not fully support complex tables. When possible, try to "flatten" the table and avoid merged cells.'; +$string['privacy:metadata'] = 'The tiny_accessibilitychecker plugin does not store any personal data.'; +$string['viewissue'] = 'View'; +$string['helplinktext'] = 'Moodle Accessibility helper'; diff --git a/lib/editor/tiny/plugins/accessibilitychecker/pix/icon.svg b/lib/editor/tiny/plugins/accessibilitychecker/pix/icon.svg new file mode 100644 index 00000000000..3679e2b36c8 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/pix/icon.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/lib/editor/tiny/plugins/accessibilitychecker/styles.css b/lib/editor/tiny/plugins/accessibilitychecker/styles.css new file mode 100644 index 00000000000..b23127d1f77 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/styles.css @@ -0,0 +1,7 @@ +.warning-desc { + word-wrap: break-word; +} + +.warning-desc li { + margin-bottom: 10px; +} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache b/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache new file mode 100644 index 00000000000..8f62f93946c --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/templates/warning_content.mustache @@ -0,0 +1,59 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_accessibilitychecker/warning_contenet + + Display a content warning message + + Example context (json): + { + "warnings": [ + { + "description": "This is a warning message", + "nodeData": [{ + "text": "Text of content", + "src": "https://example.com/image.png", + "nodeName": "IMG", + "nodeIndex": 0 + }] + } + ] + } +}} +
+ {{#warnings}} +

+ {{{description}}} + {{#nodeData}} +

    +
  1. + {{#text}} + {{text}} + {{/text}} + {{#src}} + + {{/src}} + - {{#str}} viewissue, tiny_accessibilitychecker {{/str}} +
  2. +
+ {{/nodeData}} +

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

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

+ {{/warnings}} +
diff --git a/lib/editor/tiny/plugins/accessibilitychecker/version.php b/lib/editor/tiny/plugins/accessibilitychecker/version.php new file mode 100644 index 00000000000..c0bb671d723 --- /dev/null +++ b/lib/editor/tiny/plugins/accessibilitychecker/version.php @@ -0,0 +1,29 @@ +. + +/** + * Tiny media plugin version details. + * + * @package tiny_accessibilitychecker + * @copyright 2022, Stevani Andolo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2022062700; +$plugin->requires = 2020061500; +$plugin->component = 'tiny_accessibilitychecker';