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

Hard to read

| + | There is a lot of text with no headings. |

Sweet roll oat cake jelly-o macaroon donut oat cake. Caramels macaroon cookie sweet roll croissant cheesecake candy jelly-o. Gummies sugar plum sugar plum gingerbread dessert. Tiramisu bonbon jujubes danish marshmallow cookie chocolate cake cupcake tiramisu. Bear claw oat cake chocolate bar croissant. Lollipop cookie topping liquorice croissant. Brownie cookie cupcake lollipop cupcake cupcake. Fruitcake dessert sweet biscuit dragée caramels marzipan brownie. Chupa chups gingerbread apple pie cookie liquorice caramels carrot cake cookie gingerbread. Croissant candy jelly beans. Tiramisu apple pie dessert apple pie macaroon soufflé. Brownie powder carrot cake chocolate. Tart applicake croissant dragée macaroon chocolate donut.

Jelly beans gingerbread tootsie roll. Sugar plum tiramisu cotton candy toffee pie cotton candy tiramisu. Carrot cake chocolate bar sesame snaps cupcake cake dessert sweet fruitcake wafer. Marshmallow cupcake gingerbread pie sweet candy canes powder gummi bears. Jujubes cake muffin marshmallow candy jelly beans tootsie roll pie. Gummi bears applicake chocolate cake sweet jelly sesame snaps lollipop lollipop carrot cake. Marshmallow cake jelly beans. Jelly beans sesame snaps muffin halvah cookie ice cream candy canes carrot cake. Halvah donut marshmallow tiramisu. Cookie dessert gummi bears. Sugar plum apple pie jelly beans gummi bears tart chupa chups. Liquorice macaroon gummi bears gummies macaroon marshmallow sweet roll cake topping. Lemon drops caramels pie icing danish. Chocolate cake oat cake dessert halvah danish carrot cake apple pie.

| + | Tables should not contain merged cells. |
Dogs that look good in pants
BreedCoolness
PoodleNOT COOL
Doberman
| + | Tables should use row and/or column headers. |
Dogs that look good in pants
BreedCoolness
PoodleNOT COOL
DobermanCOOL
| + | Tables should have captions. |
BreedCoolness
PoodleNOT COOL
DobermanCOOL
| + + @javascript + Scenario: Perform accessibility validation on images with no alt attribute + Given I log in as "admin" + And I open my profile in edit mode + And I set the field "Description" to "

Some plain text

Some more text

" + And I click on the "Tools > Accessibility checker" menu item for the "Description" TinyMCE editor + And I should see "Images require alternative text." in the "Accessibility checker" "dialogue" + And I click on "View" "link" in the "Accessibility checker" "dialogue" + And I click on the "Image" button for the "Description" TinyMCE editor + And the field "Enter URL" matches value "/broken-image" + And I set the field "Describe this image for someone who cannot see it" to "No more warning!" + And I press "Save image" + And I click on the "Tools > Accessibility checker" menu item for the "Description" TinyMCE editor + And I should see "Congratulations, no accessibility issues found!" in the "Accessibility checker" "dialogue" + And I click on "Close" "button" in the "Accessibility checker" "dialogue" + And I select the "img" element in position "2" of the "Description" TinyMCE editor + And I click on the "Image" button for the "Description" TinyMCE editor + And I set the field "Enter URL" to "/decorative-image.png" + And I set the field "Describe this image for someone who cannot see it" to "" + And I set the field "Width" to "1" + And I set the field "Height" to "1" + And I click on "This image is decorative only" "checkbox" + When I press "Save image" + And I click on the "Tools > Accessibility checker" menu item for the "Description" TinyMCE editor + Then I should see "Congratulations, no accessibility issues found!" in the "Accessibility checker" "dialogue" diff --git a/lib/editor/tiny/plugins/autosave/tests/behat/autosave.feature b/lib/editor/tiny/plugins/autosave/tests/behat/autosave.feature new file mode 100644 index 00000000000..32449db5076 --- /dev/null +++ b/lib/editor/tiny/plugins/autosave/tests/behat/autosave.feature @@ -0,0 +1,71 @@ +@editor @editor_tiny @_file_upload +Feature: Tiny editor autosave + In order to prevent data loss + As a content creator + I need my content to be saved automatically + + Background: + Given the following "courses" exist: + | fullname | shortname | category | groupmode | description | summaryformat | + | Course 1 | C1 | 0 | 1 | | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | teacher2 | Teacher | 2 | teacher2@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | private_files | System | 1 | my-index | side-post | + + @javascript + Scenario: Restore a draft on user profile page + Given I log in as "teacher1" + And I open my profile in edit mode + And I set the field "Description" to "This is my draft" + And I log out + When I log in as "teacher1" + And I open my profile in edit mode + Then the field "Description" matches value "This is my draft" + + @javascript + Scenario: Do not restore a draft if files have been modified + Given I am on the "Course 1" course page logged in as teacher1 + And I navigate to "Settings" in current page administration + And I set the field "Course summary" to "This is my draft" + And I log out + And I log in as "teacher2" + And I follow "Manage private files..." + And I upload "/lib/editor/tiny/tests/behat/fixtures/tinyscreenshot.png" file to "Files" filemanager + And I click on "Save changes" "button" + And I am on "Course 1" course homepage + And I navigate to "Settings" in current page administration + And I set the field "Course summary" to "

Image test

" + And I select the "p" element in position "1" of the "Course summary" TinyMCE editor + And I click on the "Image" button for the "Course summary" TinyMCE editor + And I click on "Browse repositories..." "button" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "tinyscreenshot.png" "link" + And I click on "Select this file" "button" + And I set the field "Describe this image" to "It's the Moodle" + And I click on "Save image" "button" + And I click on "Save and display" "button" + When I am on the "Course 1" course page logged in as teacher1 + And I navigate to "Settings" in current page administration + Then I should not see "This is my draft" + + @javascript + Scenario: Do not restore a draft if text has been modified + Given I am on the "Course 1" course page logged in as teacher1 + And I navigate to "Settings" in current page administration + And I set the field "Course summary" to "This is my draft" + And I am on the "Course 1" course page logged in as teacher2 + And I navigate to "Settings" in current page administration + And I set the field "Course summary" to "Modified text" + And I click on "Save and display" "button" + When I am on the "Course 1" course page logged in as teacher1 + And I navigate to "Settings" in current page administration + Then I should not see "This is my draft" in the "#id_summary_editor" "css_element" + And the field "Course summary" matches value "

Modified text

" diff --git a/lib/editor/tiny/plugins/equation/tests/behat/equation.feature b/lib/editor/tiny/plugins/equation/tests/behat/equation.feature new file mode 100644 index 00000000000..82bf233c5ce --- /dev/null +++ b/lib/editor/tiny/plugins/equation/tests/behat/equation.feature @@ -0,0 +1,36 @@ +@editor @editor_tiny @tiny_equation +Feature: Tiny equation editor + To teach maths to students, I need to write equations + + @javascript + Scenario: Create an equation using TinyMCE + Given I log in as "admin" + When I open my profile in edit mode + And I set the field "Description" to "

Equation test

" + # Set field on the bottom of page, so equation editor dialogue is visible. + And I expand all fieldsets + And I set the field "Picture description" to "Test" + And I expand all toolbars for the "Description" TinyMCE editor + And I click on the "Equation editor" button for the "Description" TinyMCE editor + And the "class" attribute of "Edit equation using" "field" should contain "text-ltr" + And I set the field "Edit equation using" to " = 1 \div 0" + And I click on "\infty" "button" + And I click on "Save equation" "button" + And I click on "Update profile" "button" + And I follow "Profile" in the user menu + Then "\infty" "text" should exist + + @javascript + Scenario: Edit an equation using TinyMCE + Given I log in as "admin" + When I open my profile in edit mode + And I set the field "Description" to "

\( \pi \)

" + # Set field on the bottom of page, so equation editor dialogue is visible. + And I expand all fieldsets + And I set the field "Picture description" to "Test" + And I expand all toolbars for the "Description" TinyMCE editor + And I click on the "Equation editor" button for the "Description" TinyMCE editor + And the "class" attribute of "Edit equation using" "field" should contain "text-ltr" + Then the field "Edit equation using" matches value " \pi " + And I click on "Save equation" "button" + And the field "Description" matches value "

\( \pi \)

" diff --git a/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js b/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js index 89ae0b09c20..2bfa17747ff 100644 --- a/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js +++ b/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js @@ -1,3 +1,3 @@ -define("tiny_h5p/commands",["exports","editor_tiny/utils","./ui","core/str","./common"],(function(_exports,_utils,_ui,_str,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[buttonText,buttonImage]=await Promise.all([(0,_str.get_string)("buttontitle",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addToggleButton(_common.buttonName,{icon:_common.icon,tooltip:buttonText,onAction:()=>(0,_ui.handleAction)(editor),onSetup:api=>{api.setActive(editor.formatter.match("h5p"));const changed=editor.formatter.formatChanged("h5p",(state=>api.setActive(state)));return()=>changed.unbind()}}),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.icon,text:buttonText,onAction:()=>(0,_ui.handleAction)(editor)})}}})); +define("tiny_h5p/commands",["exports","editor_tiny/utils","./ui","core/str","./common","./options"],(function(_exports,_utils,_ui,_str,_common,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[buttonText,buttonImage]=await Promise.all([(0,_str.get_string)("buttontitle",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{(0,_options.hasAnyH5PPermission)(editor)&&(editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addToggleButton(_common.buttonName,{icon:_common.icon,tooltip:buttonText,onAction:()=>(0,_ui.handleAction)(editor),onSetup:api=>{api.setActive(editor.formatter.match("h5p"));const changed=editor.formatter.formatChanged("h5p",(state=>api.setActive(state)));return()=>changed.unbind()}}),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.icon,text:buttonText,onAction:()=>(0,_ui.handleAction)(editor)}))}}})); //# sourceMappingURL=commands.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js.map b/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js.map index fd2305c7fa0..6146bd4953a 100644 --- a/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js.map +++ b/lib/editor/tiny/plugins/h5p/amd/build/commands.min.js.map @@ -1 +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 H5P Content configuration.\n *\n * @module tiny_h5p/commands\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getButtonImage} from 'editor_tiny/utils';\nimport {handleAction} from './ui';\nimport {get_string as getString} from 'core/str';\nimport {\n component,\n buttonName,\n icon,\n} from './common';\n\nexport const getSetup = async() => {\n const [\n buttonText,\n buttonImage,\n ] = await Promise.all([\n getString('buttontitle', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n // Register the H5P Icon.\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n // Register the Menu Button as a toggle.\n // This means that when highlighted over an existing H5P element it will show as toggled on.\n editor.ui.registry.addToggleButton(buttonName, {\n icon,\n tooltip: buttonText,\n onAction: () => handleAction(editor),\n onSetup: (api) => {\n // Set the button to be active if the current selection matches the h5p formatter registered above during PreInit.\n api.setActive(editor.formatter.match('h5p'));\n const changed = editor.formatter.formatChanged('h5p', (state) => api.setActive(state));\n return () => changed.unbind();\n },\n });\n\n // Add the H5P Menu Item.\n // This allows it to be added to a standard menu, or a context menu.\n editor.ui.registry.addMenuItem(buttonName, {\n icon,\n text: buttonText,\n onAction: () => handleAction(editor),\n });\n };\n};\n"],"names":["async","buttonText","buttonImage","Promise","all","component","editor","ui","registry","addIcon","icon","html","addToggleButton","buttonName","tooltip","onAction","onSetup","api","setActive","formatter","match","changed","formatChanged","state","unbind","addMenuItem","text"],"mappings":"uOAgCwBA,gBAEhBC,WACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,cAAeC,oBACzB,yBAAe,OAAQA,4BAGnBC,SAEJA,OAAOC,GAAGC,SAASC,QAAQC,aAAMR,YAAYS,MAI7CL,OAAOC,GAAGC,SAASI,gBAAgBC,mBAAY,CAC3CH,KAAAA,aACAI,QAASb,WACTc,SAAU,KAAM,oBAAaT,QAC7BU,QAAUC,MAENA,IAAIC,UAAUZ,OAAOa,UAAUC,MAAM,cAC/BC,QAAUf,OAAOa,UAAUG,cAAc,OAAQC,OAAUN,IAAIC,UAAUK,eACxE,IAAMF,QAAQG,YAM7BlB,OAAOC,GAAGC,SAASiB,YAAYZ,mBAAY,CACvCH,KAAAA,aACAgB,KAAMzB,WACNc,SAAU,KAAM,oBAAaT"} \ No newline at end of file +{"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 H5P Content configuration.\n *\n * @module tiny_h5p/commands\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getButtonImage} from 'editor_tiny/utils';\nimport {handleAction} from './ui';\nimport {get_string as getString} from 'core/str';\nimport {\n component,\n buttonName,\n icon,\n} from './common';\nimport {hasAnyH5PPermission} from './options';\n\nexport const getSetup = async() => {\n const [\n buttonText,\n buttonImage,\n ] = await Promise.all([\n getString('buttontitle', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n if (!hasAnyH5PPermission(editor)) {\n return;\n }\n // Register the H5P Icon.\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n // Register the Menu Button as a toggle.\n // This means that when highlighted over an existing H5P element it will show as toggled on.\n editor.ui.registry.addToggleButton(buttonName, {\n icon,\n tooltip: buttonText,\n onAction: () => handleAction(editor),\n onSetup: (api) => {\n // Set the button to be active if the current selection matches the h5p formatter registered above during PreInit.\n api.setActive(editor.formatter.match('h5p'));\n const changed = editor.formatter.formatChanged('h5p', (state) => api.setActive(state));\n return () => changed.unbind();\n },\n });\n\n // Add the H5P Menu Item.\n // This allows it to be added to a standard menu, or a context menu.\n editor.ui.registry.addMenuItem(buttonName, {\n icon,\n text: buttonText,\n onAction: () => handleAction(editor),\n });\n };\n};\n"],"names":["async","buttonText","buttonImage","Promise","all","component","editor","ui","registry","addIcon","icon","html","addToggleButton","buttonName","tooltip","onAction","onSetup","api","setActive","formatter","match","changed","formatChanged","state","unbind","addMenuItem","text"],"mappings":"4PAiCwBA,gBAEhBC,WACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,cAAeC,oBACzB,yBAAe,OAAQA,4BAGnBC,UACC,gCAAoBA,UAIzBA,OAAOC,GAAGC,SAASC,QAAQC,aAAMR,YAAYS,MAI7CL,OAAOC,GAAGC,SAASI,gBAAgBC,mBAAY,CAC3CH,KAAAA,aACAI,QAASb,WACTc,SAAU,KAAM,oBAAaT,QAC7BU,QAAUC,MAENA,IAAIC,UAAUZ,OAAOa,UAAUC,MAAM,cAC/BC,QAAUf,OAAOa,UAAUG,cAAc,OAAQC,OAAUN,IAAIC,UAAUK,eACxE,IAAMF,QAAQG,YAM7BlB,OAAOC,GAAGC,SAASiB,YAAYZ,mBAAY,CACvCH,KAAAA,aACAgB,KAAMzB,WACNc,SAAU,KAAM,oBAAaT"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/h5p/amd/build/options.min.js b/lib/editor/tiny/plugins/h5p/amd/build/options.min.js index 52511a008c3..ed9c1135b26 100644 --- a/lib/editor/tiny/plugins/h5p/amd/build/options.min.js +++ b/lib/editor/tiny/plugins/h5p/amd/build/options.min.js @@ -1,4 +1,4 @@ -define("tiny_h5p/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=void 0; +define("tiny_h5p/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.hasAnyH5PPermission=_exports.getPermissions=void 0; /** * Options helper for Tiny H5P plugin. * @@ -6,6 +6,6 @@ define("tiny_h5p/options",["exports","editor_tiny/options","./common"],(function * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions");_exports.register=editor=>{(0,editor.options.register)(permissionsName,{processor:"object",default:{upload:!1,embed:!1}})};_exports.getPermissions=editor=>editor.options.get(permissionsName)})); +const permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions");_exports.register=editor=>{(0,editor.options.register)(permissionsName,{processor:"object",default:{upload:!1,embed:!1}})};const getPermissions=editor=>editor.options.get(permissionsName);_exports.getPermissions=getPermissions;_exports.hasAnyH5PPermission=editor=>{const permissions=getPermissions(editor);return permissions.upload||permissions.embed}})); //# sourceMappingURL=options.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/h5p/amd/build/options.min.js.map b/lib/editor/tiny/plugins/h5p/amd/build/options.min.js.map index d2346ff9373..3f252b324a9 100644 --- a/lib/editor/tiny/plugins/h5p/amd/build/options.min.js.map +++ b/lib/editor/tiny/plugins/h5p/amd/build/options.min.js.map @@ -1 +1 @@ -{"version":3,"file":"options.min.js","sources":["../src/options.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 * Options helper for Tiny H5P plugin.\n *\n * @module tiny_h5p/options\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\n\n/**\n * Register the options for the Tiny H5P plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n upload: false,\n embed: false,\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny H5P plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n"],"names":["permissionsName","pluginName","editor","registerOption","options","register","processor","upload","embed","get"],"mappings":";;;;;;;;MA0BMA,iBAAkB,gCAAoBC,mBAAY,iCAO/BC,UAGrBC,EAFuBD,OAAOE,QAAQC,UAEvBL,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,QAAQ,EACRC,OAAO,8BAWYN,QAAWA,OAAOE,QAAQK,IAAIT"} \ No newline at end of file +{"version":3,"file":"options.min.js","sources":["../src/options.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 * Options helper for Tiny H5P plugin.\n *\n * @module tiny_h5p/options\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\n\n/**\n * Register the options for the Tiny H5P plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n upload: false,\n embed: false,\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny H5P plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n\n/**\n * Check whether any H5P Permission is available.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const hasAnyH5PPermission = (editor) => {\n const permissions = getPermissions(editor);\n return permissions.upload || permissions.embed;\n};\n"],"names":["permissionsName","pluginName","editor","registerOption","options","register","processor","upload","embed","getPermissions","get","permissions"],"mappings":";;;;;;;;MA0BMA,iBAAkB,gCAAoBC,mBAAY,iCAO/BC,UAGrBC,EAFuBD,OAAOE,QAAQC,UAEvBL,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,QAAQ,EACRC,OAAO,YAWNC,eAAkBP,QAAWA,OAAOE,QAAQM,IAAIV,qFAQzBE,eAC1BS,YAAcF,eAAeP,eAC5BS,YAAYJ,QAAUI,YAAYH"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/h5p/amd/src/commands.js b/lib/editor/tiny/plugins/h5p/amd/src/commands.js index 4c487525491..5e1ec1c3a73 100644 --- a/lib/editor/tiny/plugins/h5p/amd/src/commands.js +++ b/lib/editor/tiny/plugins/h5p/amd/src/commands.js @@ -29,6 +29,7 @@ import { buttonName, icon, } from './common'; +import {hasAnyH5PPermission} from './options'; export const getSetup = async() => { const [ @@ -40,6 +41,9 @@ export const getSetup = async() => { ]); return (editor) => { + if (!hasAnyH5PPermission(editor)) { + return; + } // Register the H5P Icon. editor.ui.registry.addIcon(icon, buttonImage.html); diff --git a/lib/editor/tiny/plugins/h5p/amd/src/options.js b/lib/editor/tiny/plugins/h5p/amd/src/options.js index 8a32b20909f..74835c75110 100644 --- a/lib/editor/tiny/plugins/h5p/amd/src/options.js +++ b/lib/editor/tiny/plugins/h5p/amd/src/options.js @@ -50,3 +50,14 @@ export const register = (editor) => { * @returns {object} */ export const getPermissions = (editor) => editor.options.get(permissionsName); + +/** + * Check whether any H5P Permission is available. + * + * @param {TinyMCE} editor + * @returns {boolean} + */ +export const hasAnyH5PPermission = (editor) => { + const permissions = getPermissions(editor); + return permissions.upload || permissions.embed; +}; diff --git a/lib/editor/tiny/plugins/h5p/tests/behat/h5p.feature b/lib/editor/tiny/plugins/h5p/tests/behat/h5p.feature new file mode 100644 index 00000000000..a6f48937f4a --- /dev/null +++ b/lib/editor/tiny/plugins/h5p/tests/behat/h5p.feature @@ -0,0 +1,210 @@ +@editor @editor_tiny @tiny_media @javascript @_file_upload +Feature: Use the TinyMCE editor to upload an h5p package + In order to work with h5p + As a content creator + I need to be able to embed H5P packages + + Background: + Given the following "courses" exist: + | shortname | fullname | + | C1 | Course 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | intro | introformat | course | content | contentformat | idnumber | + | page | PageName1 | PageDesc1 | 1 | C1 | H5Ptest | 1 | 1 | + And the "displayh5p" filter is "on" + And the following config values are set as admin: + | allowedsources | https://moodle.h5p.com/content/[id] | filter_displayh5p | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | private_files | System | 1 | my-index | side-post | + + @javascript @external + Scenario: TinyMCE can be used to embed an H5P activity + Given I change window size to "large" + And I am on the PageName1 "page activity editing" page logged in as admin + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I set the field "H5P URL or file upload" to "https://moodle.h5p.com/content/1290772960722742119" + And I click on "Insert H5P content" "button" in the "Insert H5P content" "dialogue" + When I click on "Save and display" "button" + Then ".h5p-placeholder" "css_element" should exist + And I switch to "h5pcontent" iframe + And I should see "Lorum ipsum" + + @javascript + Scenario: TinyMCE can be used to upload and embed an H5P activity + Given I log in as "admin" + And I change window size to "large" + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + And I am on the "PageName1" "page activity editing" page + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "guess-the-answer.h5p" "link" + And I click on "Select this file" "button" + And I click on "Insert H5P content" "button" in the "Insert H5P content" "dialogue" + When I click on "Save and display" "button" + Then ".h5p-placeholder" "css_element" should exist + + @javascript + Scenario: When a user does not have any H5P capabilities, they cannot embed H5P content with TinyMCE + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | tiny/h5p:addembed | Prohibit | editingteacher | Course | C1 | + | moodle/h5p:deploy | Prohibit | editingteacher | Course | C1 | + When I am on the PageName1 "page activity editing" page logged in as teacher1 + Then "Configure H5P content" "button" should not exist + + @javascript + Scenario: When a user does not have the Embed H5P capability, they cannot embed H5P content with TinyMCE + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | tiny/h5p:addembed | Prohibit | editingteacher | Course | C1 | + And I am on the PageName1 "page activity editing" page logged in as teacher1 + And I click on "Configure H5P" "button" + Then I should not see "H5P URL" in the "Insert H5P content" "dialogue" + And I should see "H5P file upload" in the "Insert H5P content" "dialogue" + And I should see "H5P options" in the "Insert H5P content" "dialogue" + + @javascript + Scenario: When a user does not have the Upload H5P capability, they can embed but not upload H5P content with TinyMCE + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/h5p:deploy | Prohibit | editingteacher | Course | C1 | + When I am on the PageName1 "page activity editing" page logged in as teacher1 + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + Then I should not see "H5P file upload" in the "Insert H5P content" "dialogue" + And I should see "H5P URL" in the "Insert H5P content" "dialogue" + And I should not see "H5P options" in the "Insert H5P content" "dialogue" + + @javascript @external + Scenario: A user can edit H5P content embedding with TinyMCE + Given I log in as "admin" + And I follow "Manage private files..." + And I upload "lib/editor/atto/tests/fixtures/drag.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + And I am on the PageName1 "page activity editing" page + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "drag" "link" + And I click on "Select this file" "button" + And I click on "Insert H5P content" "button" in the "Insert H5P content" "dialogue" + When I click on "Save and display" "button" + And I switch to "h5pcontent" iframe + And I switch to "h5p-iframe" class iframe + Then I should not see "reveal" + And I should see "Cloudberries" + And I switch to the main frame + And I navigate to "Settings" in current page administration + And I select the ".h5p-placeholder" "css_element" in the "Page content" TinyMCE editor + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I set the field "H5P URL or file upload" to "https://moodle.h5p.com/content/1290772960722742119" + And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" + And I wait "1" seconds + And I click on "Save and display" "button" + And I switch to "h5pcontent" iframe + And I should see "Lorum ipsum" + And I should not see "Cloudberries" + + @javascript + Scenario: Enable/disable H5P options + Given I log in as "admin" + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + And I am on the PageName1 "page activity editing" page + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "guess-the-answer.h5p" "link" + And I click on "Select this file" "button" + And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" + When I click on "Save and display" "button" + And I switch to "h5pcontent" iframe + And I switch to "h5p-iframe" class iframe + Then I should not see "Reuse" + And I should not see "Embed" + And I should not see "Rights of use" + And I switch to the main frame + And I navigate to "Settings" in current page administration + And I select the ".h5p-placeholder" "css_element" in the "Page content" TinyMCE editor + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "H5P options" "link" + And I click on "Allow download" "checkbox" + And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" + And I wait "1" seconds + And I click on "Save and display" "button" + And I switch to "h5pcontent" iframe + And I switch to "h5p-iframe" class iframe + And I should see "Reuse" + And I should not see "Embed" + And I should not see "Rights of use" + And I switch to the main frame + And I navigate to "Settings" in current page administration + And I select the ".h5p-placeholder" "css_element" in the "Page content" TinyMCE editor + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "Allow download" "checkbox" + And I click on "Embed button" "checkbox" + And I click on "Copyright button" "checkbox" + And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" + And I wait "1" seconds + And I click on "Save and display" "button" + And I switch to "h5pcontent" iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Reuse" + And I should see "Embed" + And I should see "Rights of use" + + @javascript @external + Scenario: H5P options are ignored for H5P URLs + Given I change window size to "large" + And I am on the PageName1 "page activity editing" page logged in as admin + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I set the field "H5P URL or file upload" to "https://moodle.h5p.com/content/1291366510035871129" + And I click on "H5P options" "link" + And I click on "Embed button" "checkbox" + And I click on "Insert H5P content" "button" in the "Insert H5P content" "dialogue" + When I click on "Save and display" "button" + Then ".h5p-placeholder" "css_element" should exist + And I switch to "h5pcontent" iframe + And I should see "Far far away" + And I should not see "Embed" + And I switch to the main frame + And I navigate to "Settings" in current page administration + And I select the ".h5p-placeholder" "css_element" in the "Page content" TinyMCE editor + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "H5P options" "link" + And "input[aria-label=\"Embed button\"]:not([checked=checked])" "css_element" should exist + + @javascript + Scenario: Private H5P files are shown to students + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + And I log in as "admin" + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/guess-the-answer.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + And I am on the PageName1 "page activity editing" page + And I click on the "Configure H5P content" button for the "Page content" TinyMCE editor + And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "guess-the-answer.h5p" "link" + And I click on "Select this file" "button" + And I click on "Insert H5P content" "button" in the "Insert H5P content" "dialogue" + And I click on "Save and display" "button" + When I am on the PageName1 "page activity" page logged in as student1 + Then I switch to "h5pcontent" iframe + And I switch to "h5p-iframe" class iframe + And I should see "reveal" diff --git a/lib/editor/tiny/plugins/media/tests/behat/image.feature b/lib/editor/tiny/plugins/media/tests/behat/image.feature new file mode 100644 index 00000000000..fc9cb148f5b --- /dev/null +++ b/lib/editor/tiny/plugins/media/tests/behat/image.feature @@ -0,0 +1,28 @@ +@editor @editor_tiny @tiny_media @javascript +Feature: Use the TinyMCE editor to upload an image + In order to work with images + As a user + I need to be able to upload and manipulate images + + Scenario: Clicking on the Image button in the TinyMCE editor opens the image dialog + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + Then "Image properties" "dialogue" should exist + + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Image properties" "dialogue" + Then "File picker" "dialogue" should exist + + @_file_upload @test_tiny + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Image properties" "dialogue" + And I upload "/lib/editor/tiny/tests/behat/fixtures/tinyscreenshot.png" to the file picker for TinyMCE + # Note: This needs to be replaced with a label. + Then ".tiny_image_preview" "css_element" should be visible diff --git a/lib/editor/tiny/plugins/media/tests/behat/video.feature b/lib/editor/tiny/plugins/media/tests/behat/video.feature new file mode 100644 index 00000000000..6fa0f4d2126 --- /dev/null +++ b/lib/editor/tiny/plugins/media/tests/behat/video.feature @@ -0,0 +1,29 @@ +@editor @editor_tiny @tiny_media @javascript +Feature: Use the TinyMCE editor to upload a video + In order to work with videos + As a user + I need to be able to upload and manipulate videos + + Scenario: Clicking on the Video button in the TinyMCE editor opens the video dialog + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Multimedia" button for the "Description" TinyMCE editor + Then "Insert media" "dialogue" should exist + + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Multimedia" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Insert media" "dialogue" + Then "File picker" "dialogue" should exist + + @_file_upload + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Multimedia" button for the "Description" TinyMCE editor + And I follow "Video" + And I click on "Browse repositories..." "button" in the "#id_description_editor_video .tiny_media_source.tiny_media_media_source" "css_element" + And I upload "/lib/editor/tiny/tests/behat/fixtures/moodle-logo.mp4" to the file picker for TinyMCE + When I click on "Insert media" "button" + And I select the "video" element in position "1" of the "Description" TinyMCE editor diff --git a/lib/editor/tiny/tests/behat/behat_editor_tiny.php b/lib/editor/tiny/tests/behat/behat_editor_tiny.php index b6741a0cca3..f0213ba7649 100644 --- a/lib/editor/tiny/tests/behat/behat_editor_tiny.php +++ b/lib/editor/tiny/tests/behat/behat_editor_tiny.php @@ -24,6 +24,9 @@ */ use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Mink\Element\NodeElement; +use Behat\Mink\Exception\DriverException; +use Behat\Mink\Exception\ExpectationException; // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../behat/behat_base.php'); @@ -36,6 +39,52 @@ require_once(__DIR__ . '/../../../../behat/behat_base.php'); * @copyright 2022 Andrew Lyons */ class behat_editor_tiny extends behat_base implements \core_behat\settable_editor { + /** + * Execute some JavaScript for a particular Editor instance. + * + * The editor instance is available on the 'instnace' variable. + * + * @param string $editorid The ID of the editor + * @param string $code The code to execute + */ + protected function execute_javascript_for_editor(string $editorid, string $code): void { + $js = << { + const instance = editor.getInstanceForElementId('${editorid}'); + {$code} + }); + EOF; + + $this->execute_script($js); + } + + /** + * Resolve some JavaScript for a particular Editor instance. + * + * The editor instance is available on the 'instnace' variable. + * The code should return a value by passing it to the `resolve` function. + * + * @param string $editorid The ID of the editor + * @param string $code The code to evaluate + * @return string|null|array + */ + protected function evaluate_javascript_for_editor(string $editorid, string $code) { + $js = << { + require(['editor_tiny/editor'], (editor) => { + const instance = editor.getInstanceForElementId('${editorid}'); + if (!instance) { + reject("Instance '${editorid}' not found"); + } + + {$code} + }); + }); + EOF; + + return $this->evaluate_script($js); + } + /** * Set the value for the editor. * @@ -51,17 +100,19 @@ class behat_editor_tiny extends behat_base implements \core_behat\settable_edito return; } - $js = << { - const instance = editor.getInstanceForElementId('${editorid}'); - if (instance) { - instance.setContent('${value}'); - instance.undoManager.add(); - } - }); - EOF; + $this->execute_javascript_for_editor($editorid, <<execute_script($js); + /** + * Store the current value of the editor, if it is a Tiny editor, to the textarea. + * + * @param string $editorid The ID of the editor. + */ + public function store_current_value(string $editorid): void { + $this->execute_javascript_for_editor($editorid, "instance?.save();"); } /** @@ -92,4 +143,322 @@ class behat_editor_tiny extends behat_base implements \core_behat\settable_edito $this->execute('behat_general::the_default_editor_is_set_to', ['tiny']); } + + /** + * Ensure that the editor_tiny tag is in use. + * + * This function should be used for any step defined in this file. + * + * @throws DriverException Thrown if the editor_tiny tag is not specified for this file + */ + protected function require_tiny_tags(): void { + // Ensure that this step only runs in TinyMCE tags. + if (!$this->has_tag('editor_tiny')) { + throw new DriverException( + 'TinyMCE tests using this step must have the @editor_tiny tag on either the scenario or feature.' + ); + } + } + + /** + * Get the Mink NodeElement of the